mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
854973a9f1
* fix(tailwind): make tailwind.config.js loadable by a plain CommonJS require
`tailwind.config.js` is a CommonJS module, but it `require()`d two TypeScript
sources:
require('./src/utils/tailwind') -> src/utils/tailwind.ts
require('./src/tailwind/container-queries') -> src/tailwind/container-queries.ts
Node's CJS resolver does not resolve `.ts`, so a plain `require()` of the config
threw `Cannot find module './src/utils/tailwind'`.
This was latent rather than user-visible: tailwind v3's `loadConfig()` routes the
config through its bundled jiti, and jiti *does* resolve `.ts`, so Next and the
PostCSS pipeline kept loading the real theme. (Verified: building CSS through the
repo's own postcss.config.js resolves `theme('colors.blue.8')` and
`theme('screens.xs')` correctly both before and after this change.)
The problem is that the project theme silently depended on being loaded by a
TypeScript-aware loader. Any consumer that plain-`require()`s the config — a
script, a lint rule, a tool that inlines it — and any tailwind version that stops
routing through jiti gets tailwind's *stock* theme instead of this project's.
Stock tailwind has no `xs` screen and no numeric colour scale, while ~80
stylesheets use `theme('screens.…')` / `theme('colors.<name>.<digit>')`.
Fix, keeping `breakpoints` single-sourced:
- Move the breakpoints literal to `src/utils/breakpoints.json`. JSON is the one
format Node's CJS resolver, jiti, webpack and Vite all load natively, so both
module systems can read the same file.
- `src/utils/tailwind.ts` re-exports it, so the two importers
(`ContainerGrid.tsx`, `ImageCarousel.tsx`) keep their import path unchanged.
`resolveJsonModule` keeps the inferred type byte-identical to the old inline
literal — verified with a type-equality probe that passes against both the old
and the new module, and fails when the expected type is perturbed.
- Convert `src/tailwind/container-queries.ts` to CommonJS `.js` (types preserved
as JSDoc). It is loaded only by the tailwind config, so it has no ESM consumers.
Adds `src/utils/tailwind.test.ts`, which loads the config through `createRequire`
— a genuine Node CJS require, no jiti, no TS loader — and asserts the project
screens resolve. Red before this change with the original
`Cannot find module './src/utils/tailwind'`; green after.
* fix(tailwind): keep container-queries.js inside tsc, and tighten the plugin assertion
Both findings from the adversarial audit of this PR.
1. 🔴 The .ts -> .js conversion silently dropped this file out of the type system.
tsconfig.json:10 sets `allowJs: true` but leaves `checkJs` UNSET, so a .js file
under src/ is compiled by tsc and type-checked by nothing — the JSDoc @typedef
/@param annotations added in the previous commit were pure decoration. Adding
`// @ts-check` restores checking for this one file without touching the global
config.
Verified by mutation in BOTH directions, not asserted:
- with `// @ts-check`, no mutation ....... 13 errors (baseline, file clean)
- with `// @ts-check`, `(42).toUpperCase()` injected
....... 14 errors, and the new one is
src/tailwind/container-queries.js(20,31)
TS2339 — this file, this reason
- WITHOUT `// @ts-check`, same mutation ... 13 errors, ZERO in this file
(i.e. genuinely invisible)
Every run carried a negative control: the known pre-existing
packages/civitai-db-schema/.../updated-at-tables.ts(7,38) TS2769 was present in
all three, proving tsc actually ran. That control matters here — tsc OOM-crashes
on this repo at the default heap and then reports zero `error TS` lines, which is
indistinguishable from clean. All runs used --max_old_space_size=8192.
2. The `eslint-disable-next-line @typescript-eslint/no-var-requires` was INERT:
.eslintignore line 1 is `*.js`, so this file is not linted at all and the
suppression documented a rule that never runs. Replaced with a note recording
that fact and what to do if `*.js` is ever removed from .eslintignore.
3. `expect(config.plugins.length).toBeGreaterThanOrEqual(2)` -> `toBe(2)`. The
loose bound still passed if a plugin were dropped, which is exactly the
regression the test exists to catch. plugins.length is exactly 2.
src/utils/tailwind.test.ts: 4/4 still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
3.8 KiB
JavaScript
129 lines
3.8 KiB
JavaScript
// @ts-check
|
|
//
|
|
// `@ts-check` above is load-bearing, not decorative. tsconfig sets `allowJs: true`
|
|
// but leaves `checkJs` unset, so a `.js` file under src/ is COMPILED BY tsc and
|
|
// TYPE-CHECKED BY NOTHING — the JSDoc annotations below would be pure decoration
|
|
// without it. Verified by mutation: injecting `(42).toUpperCase()` here is
|
|
// invisible to `tsc --noEmit` when this directive is absent, and raises TS2339
|
|
// when it is present. Do not remove it when editing this file.
|
|
//
|
|
// CommonJS on purpose. This plugin is loaded exclusively by `tailwind.config.js`, a
|
|
// CJS module, via `require()`. Node's CJS resolver cannot resolve `.ts`, so authoring
|
|
// this as TypeScript made a plain `require()` of the tailwind config throw — the whole
|
|
// project theme then silently depended on the config being loaded by a TypeScript-aware
|
|
// loader (tailwind's bundled jiti). Types are kept as JSDoc.
|
|
// NOTE: no eslint-disable here on purpose. `.eslintignore` line 1 is `*.js`, so this
|
|
// file is not linted at all and a suppression comment would be inert — documenting a
|
|
// rule that never runs. If `*.js` is ever removed from `.eslintignore`, this `require`
|
|
// will need `@typescript-eslint/no-var-requires` disabled.
|
|
const plugin = require('tailwindcss/plugin');
|
|
|
|
/**
|
|
* @typedef {{ value: string; modifier: string | null }} VariantSortProps
|
|
*/
|
|
|
|
module.exports = plugin(
|
|
function containerQueries({ matchUtilities, matchVariant, theme }) {
|
|
/** @type {Record<string, string>} */
|
|
const values = theme('containers') ?? {};
|
|
|
|
/** @param {string} value */
|
|
function parseValue(value) {
|
|
const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null;
|
|
if (numericValue === null) return null;
|
|
|
|
return parseFloat(value);
|
|
}
|
|
|
|
/**
|
|
* @param {VariantSortProps} aVariant
|
|
* @param {VariantSortProps} zVariant
|
|
*/
|
|
function sort(aVariant, zVariant) {
|
|
const a = parseFloat(aVariant.value);
|
|
const z = parseFloat(zVariant.value);
|
|
|
|
if (a === null || z === null) return 0;
|
|
|
|
// Sort values themselves regardless of unit
|
|
if (a - z !== 0) return a - z;
|
|
|
|
const aLabel = aVariant.modifier ?? '';
|
|
const zLabel = zVariant.modifier ?? '';
|
|
|
|
// Explicitly move empty labels to the end
|
|
if (aLabel === '' && zLabel !== '') {
|
|
return 1;
|
|
} else if (aLabel !== '' && zLabel === '') {
|
|
return -1;
|
|
}
|
|
|
|
// Sort labels alphabetically in the English locale
|
|
// We are intentionally overriding the locale because we do not want the sort to
|
|
// be affected by the machine's locale (be it a developer or CI environment)
|
|
return aLabel.localeCompare(zLabel, 'en', { numeric: true });
|
|
}
|
|
|
|
matchUtilities(
|
|
{
|
|
'@container': (value, { modifier }) => {
|
|
return {
|
|
'container-type': value,
|
|
'container-name': modifier,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
values: {
|
|
DEFAULT: 'inline-size',
|
|
normal: 'normal',
|
|
},
|
|
modifiers: 'any',
|
|
}
|
|
);
|
|
|
|
matchVariant(
|
|
'@',
|
|
(value = '', { modifier }) => {
|
|
const parsed = parseValue(value);
|
|
|
|
return parsed !== null ? `@container ${modifier ?? ''} (min-width: ${value})` : [];
|
|
},
|
|
{
|
|
values,
|
|
sort,
|
|
}
|
|
);
|
|
|
|
matchVariant(
|
|
'@max',
|
|
(value = '', { modifier }) => {
|
|
const parsed = parseValue(value);
|
|
|
|
return parsed !== null ? `@container ${modifier ?? ''} (width < ${value})` : [];
|
|
},
|
|
{
|
|
values,
|
|
sort,
|
|
}
|
|
);
|
|
},
|
|
{
|
|
theme: {
|
|
containers: {
|
|
xs: '20rem',
|
|
sm: '24rem',
|
|
md: '28rem',
|
|
lg: '32rem',
|
|
xl: '36rem',
|
|
'2xl': '42rem',
|
|
'3xl': '48rem',
|
|
'4xl': '56rem',
|
|
'5xl': '64rem',
|
|
'6xl': '72rem',
|
|
'7xl': '80rem',
|
|
},
|
|
},
|
|
}
|
|
);
|