Skip to content

Commit 8997dd3

Browse files
committed
fix(ui): harden built CSS checks
1 parent 37d1aa9 commit 8997dd3

2 files changed

Lines changed: 16 additions & 3 deletions

File tree

packages/ui/built-css-pitfalls.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ legitimate pattern rather than a mis-scoped default. Every other check applies e
5757

5858
**How to spot it** - Search declarations for the legacy `--ax-` prefix and for `--wb-` properties outside `--wb-ds-`, `--wb-sdk-`, or `--wb-public-`. Unprefixed component-local and third-party properties remain valid. Source Stylelint separately verifies that variable uses resolve and applies the fallback contract from `wb/no-system-token-fallbacks`.
5959

60-
**Automated?** - Yes. `Unsanctioned variable namespace` checks declaration property names without a catalogue of individual variables.
60+
**Automated?** - Yes. `Unsanctioned variable namespace` checks declaration property names without a catalogue of individual variables. It does not inspect custom property names inside `var()` values. This narrowing is safe today because Stylelint's `csstools/value-no-unknown-custom-properties` rule rejects unknown names in source files, and the build does not generate new `var()` references. If a build transform starts rewriting `var()` values, the narrowing is no longer safe and the value check must be restored.
6161

6262
## Built CSS import
6363

packages/ui/scripts/check-built-css.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, globSync, readFileSync, statSync } from 'node:fs';
1+
import { globSync, readFileSync, readdirSync, statSync } from 'node:fs';
22
import path from 'node:path';
33

44
import postcss, { AtRule, type ChildNode, type Declaration, type Node } from 'postcss';
@@ -34,6 +34,19 @@ function add(file: string, node: Node | undefined, message: string): void {
3434
problems.push(`${file}:${node?.source?.start?.line ?? 1} ${message}. See ${pitfalls}.`);
3535
}
3636

37+
function existsWithExactCase(root: string, relativeTarget: string): boolean {
38+
let current = root;
39+
return relativeTarget.split(path.sep).every((segment) => {
40+
try {
41+
if (!readdirSync(current).includes(segment)) return false;
42+
} catch {
43+
return false;
44+
}
45+
current = path.join(current, segment);
46+
return true;
47+
});
48+
}
49+
3750
function isPublicDefault(declaration: Declaration): boolean {
3851
const rule = declaration.parent;
3952
if (
@@ -107,7 +120,7 @@ for (const { path: directory, ownsLayerContract } of directories) {
107120
const relativeTarget = path.relative(directory, target);
108121
const outside =
109122
relativeTarget === '..' || relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget);
110-
if (outside || !existsSync(target) || !statSync(target).isFile())
123+
if (outside || !existsWithExactCase(directory, relativeTarget) || !statSync(target).isFile())
111124
add(file, declaration, 'Missing built URL target');
112125
}
113126
});

0 commit comments

Comments
 (0)