From e5ea90004c28a07be716429b353d4799020a823e Mon Sep 17 00:00:00 2001 From: Isaque dos Santos Date: Tue, 4 Aug 2026 07:43:48 -0300 Subject: [PATCH 1/4] fix(theme): [ENG-46996] harden the zero-with-unit token gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the CSS length-unit list (vi/vb, the small/large/dynamic viewport + min/max variants, and rex/rch/ric/rcap — so 0vi no longer slips through), scan the generated stylesheet as a whole string so a calc()/min()/max()/clamp() wrapped across lines is judged in context instead of read as a bare zero, parametrize the reported filename (no hardcoded globals.css), and name the offending custom property in the error so the source token is greppable. Extract the pure logic to zero-unit.mjs so it is unit-testable without running the build; the module regexes stay non-global so .test() is stateless (findZeroMisuse clones with the g flag locally). A theme `test` script runs the suite (node --test). Mirror the completed unit list in the shared webkit token-checks engine (both zero regexes now carry the identical list) and pin both copies with tests. --- packages/theme/package.json | 1 + packages/theme/src/scripts/build-tokens.mjs | 38 +---- packages/theme/src/scripts/zero-unit.mjs | 100 +++++++++++++ packages/theme/test/zero-unit.test.mjs | 137 ++++++++++++++++++ .../webkit/src/eslint-plugin/token-checks.js | 8 +- .../test/eslint-plugin/token-checks.test.mjs | 14 ++ 6 files changed, 258 insertions(+), 40 deletions(-) create mode 100644 packages/theme/src/scripts/zero-unit.mjs create mode 100644 packages/theme/test/zero-unit.test.mjs diff --git a/packages/theme/package.json b/packages/theme/package.json index 8db24655b..4f3caafeb 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -11,6 +11,7 @@ }, "scripts": { "pack:dry": "npm pack --dry-run", + "test": "node --test \"test/**/*.test.mjs\"", "format": "prettier --write scripts/ src/", "build": "pnpm build:tokens", "build:tokens": "node src/scripts/build-tokens.mjs", diff --git a/packages/theme/src/scripts/build-tokens.mjs b/packages/theme/src/scripts/build-tokens.mjs index 90ac34ba4..474c05101 100644 --- a/packages/theme/src/scripts/build-tokens.mjs +++ b/packages/theme/src/scripts/build-tokens.mjs @@ -35,6 +35,7 @@ import { containersData } from '../tokens/semantic/containers.data.js'; import { spacingsData } from '../tokens/semantic/spacings.data.js'; import { textsData } from '../tokens/semantic/texts.data.js'; import { zIndicesData } from '../tokens/semantic/z-indices.data.js'; +import { assertNoZeroWithUnit } from './zero-unit.mjs'; const BREAKPOINT_ORDER = ['sm', 'md', 'lg', 'xl', '2xl']; @@ -359,41 +360,6 @@ const emitCssV4 = () => { ].join('\n'); }; -// Keep in sync with the `zero-with-unit` / `zero-unit-in-calc` checks in -// packages/webkit/src/eslint-plugin/token-checks.js — theme sits below webkit in the -// dependency graph, so the shared engine cannot be imported here. Same known limit: -// the math-function lookbehind balances parens one level deep. -const MATH_FN = '(?:calc|min|max|clamp)\\((?:[^()]|\\([^()]*\\))*'; -const LENGTH_UNITS = - 'px|rem|em|ex|ch|cap|ic|lh|rlh|vw|vh|vmin|vmax|svw|svh|lvw|lvh|dvw|dvh|cqw|cqh|cqi|cqb|cqmin|cqmax|cm|mm|Q|in|pt|pc'; - -const ZERO_WITH_UNIT = new RegExp(`(? { - const lines = cssText.split('\n').map((line, i) => ({ line: line.trim(), n: i + 1 })); - const bare = lines.filter(({ line }) => ZERO_WITH_UNIT.test(line)); - const inMath = lines.filter(({ line }) => ZERO_UNIT_IN_MATH.test(line)); - if (bare.length === 0 && inMath.length === 0) return; - const detail = (rows) => rows.map(({ line, n }) => ` globals.css:${n} ${line}`).join('\n'); - const parts = [`build:tokens — ${bare.length + inMath.length} token value(s) misuse a zero.`]; - if (bare.length > 0) { - parts.push( - `A zero length takes no unit: write '0', not '0px' / '0rem' / '0em'.\n${detail(bare)}`, - ); - } - if (inMath.length > 0) { - parts.push( - `Inside calc()/min()/max()/clamp() the zero needs a unit, and that unit is rem: write '0rem'.\n${detail(inMath)}`, - ); - } - parts.push('Fix the token source under src/tokens/, not the generated CSS.'); - throw new Error(parts.join('\n')); -}; - // ─── 5. Write to disk ────────────────────────────────────────────────────── const __dirname = dirname(fileURLToPath(import.meta.url)); const distRoot = resolve(__dirname, '../../dist'); @@ -415,7 +381,7 @@ const importIdx = rawCss.indexOf(IMPORT_LINE); if (importIdx === -1) throw new Error('emitCssV4 output is missing the tailwind import line'); const afterImport = importIdx + IMPORT_LINE.length; const css = `${rawCss.slice(0, afterImport)}\n\n${fontsCss}${rawCss.slice(afterImport)}`; -assertNoZeroWithUnit(css); +assertNoZeroWithUnit(css, 'globals.css'); await writeFile(resolve(dir, 'globals.css'), css, 'utf8'); await writeFile(resolve(dir, 'globals.scss'), css, 'utf8'); console.log(`✓ v4 → ${dir}`); diff --git a/packages/theme/src/scripts/zero-unit.mjs b/packages/theme/src/scripts/zero-unit.mjs new file mode 100644 index 000000000..d64dcbc6e --- /dev/null +++ b/packages/theme/src/scripts/zero-unit.mjs @@ -0,0 +1,100 @@ +/** + * Zero-with-unit discipline for the compiled token stylesheet. + * + * The tokens of `@aziontech/theme` are authored in JS (`src/tokens/**`) and compiled, so + * no linter ever sees them: stylelint reads CSS/SCSS/`