basalt-ui 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -5
- package/CHANGELOG.md +19 -0
- package/MIGRATING.md +147 -49
- package/README.md +42 -8
- package/agent/rules/basalt-app.md +41 -0
- package/agent/rules/basalt-charts.md +15 -12
- package/agent/rules/basalt-content.md +4 -0
- package/agent/rules/basalt-router.md +57 -5
- package/agent/rules/basalt-state.md +21 -3
- package/agent/rules/basalt-tokens.md +73 -16
- package/agent/skills/basalt-app/SKILL.md +4 -1
- package/agent/skills/basalt-charts/SKILL.md +5 -3
- package/agent/templates/CLAUDE-block.md.tpl +11 -5
- package/configs/lefthook.yml +48 -15
- package/configs/oxlint-plugin.js +181 -68
- package/configs/oxlint-plugin.test.ts +215 -9
- package/dist/charts/kinds/Bars.d.ts +1 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +1 -1
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +1 -1
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +1 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +1 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/cli/index.d.ts +48 -14
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +328 -66
- package/dist/cli/waiver-audit.test.d.ts +2 -0
- package/dist/cli/waiver-audit.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +47 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +176 -31
- package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +4 -1
- package/dist/router-tanstack/search-param-store.d.ts +29 -5
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +31 -3
- package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
- package/dist/shell/index.d.ts +12 -5
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +24 -6
- package/dist/tokens/index.d.ts +3 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +5 -1
- package/dist/tokens/palette.d.ts +2 -2
- package/dist/tokens/palette.js +2 -2
- package/dist/tokens/text-scale.test.d.ts +2 -0
- package/dist/tokens/text-scale.test.d.ts.map +1 -0
- package/dist/tokens.css +4 -2
- package/dist/vite.d.ts +24 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +54 -13
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/kinds/Bars.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +1 -1
- package/src/charts/kinds/DualPanel.tsx +2 -2
- package/src/charts/kinds/Heatmap.tsx +1 -1
- package/src/charts/kinds/MultiLine.tsx +1 -1
- package/src/charts/kinds/StackedArea.tsx +1 -1
- package/src/charts/kinds/ZonedLine.tsx +1 -1
- package/src/cli/index.ts +607 -128
- package/src/cli/init-sync.test.ts +2 -0
- package/src/cli/template-ledger.test.ts +1 -1
- package/src/cli/toolchain-wiring.test.ts +162 -0
- package/src/cli/waiver-audit.test.ts +131 -0
- package/src/guard/check-source.test.ts +441 -1
- package/src/guard/index.ts +430 -60
- package/src/router-tanstack/multi-search-param-store.ts +16 -1
- package/src/router-tanstack/search-param-store.test.ts +135 -0
- package/src/router-tanstack/search-param-store.ts +90 -8
- package/src/shell/app-mobile-nav.tsx +1 -1
- package/src/shell/index.test.tsx +52 -0
- package/src/shell/index.tsx +64 -13
- package/src/tokens/index.ts +24 -1
- package/src/tokens/palette.ts +2 -2
- package/src/tokens/text-scale.test.ts +51 -0
- package/src/vite.test.ts +125 -6
- package/src/vite.ts +148 -20
package/dist/cli/index.js
CHANGED
|
@@ -14,12 +14,31 @@ import {
|
|
|
14
14
|
checkSource,
|
|
15
15
|
DEFAULT_GUARD_CONFIG,
|
|
16
16
|
GENERATED_HEADER_LINE,
|
|
17
|
+
guardKindRemedy,
|
|
17
18
|
GUARD_RULES,
|
|
18
|
-
|
|
19
|
+
guardWaiverHint,
|
|
20
|
+
TOKENS_ONLY_DISABLED_KINDS,
|
|
21
|
+
unmatchedExemptPatterns
|
|
19
22
|
} from "../guard/index.js";
|
|
20
23
|
import { evaluateGuardHook } from "../guard/guard-hook.js";
|
|
21
24
|
import { RULE_NAMES, SKILL_NAMES, SURFACES, TOKEN_LAYER_BOUNDARY_SURFACES } from "../surfaces.js";
|
|
22
25
|
import { buildPaletteCss, deriveSpacing } from "../tokens/index.js";
|
|
26
|
+
function exemptRulePaths(entry) {
|
|
27
|
+
if (entry === void 0) return [];
|
|
28
|
+
return Array.isArray(entry) ? entry : entry.paths;
|
|
29
|
+
}
|
|
30
|
+
function exemptRuleReason(entry) {
|
|
31
|
+
if (entry === void 0 || Array.isArray(entry)) return null;
|
|
32
|
+
return entry.reason.trim().length > 0 ? entry.reason.trim() : null;
|
|
33
|
+
}
|
|
34
|
+
function resolveExemptRules(cfg) {
|
|
35
|
+
if (cfg.exemptRules === void 0) return DEFAULT_GUARD_CONFIG.exemptRules;
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [kind, entry] of Object.entries(cfg.exemptRules)) {
|
|
38
|
+
out[kind] = exemptRulePaths(entry);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
23
42
|
const DEFAULT_ROOT = "src";
|
|
24
43
|
const DEFAULT_ROOTS = [DEFAULT_ROOT];
|
|
25
44
|
function resolveRoots(cfg) {
|
|
@@ -89,25 +108,6 @@ function walkSourceFiles(dir, out = []) {
|
|
|
89
108
|
}
|
|
90
109
|
return out;
|
|
91
110
|
}
|
|
92
|
-
const GUARD_KIND_EXPLANATIONS = {
|
|
93
|
-
"raw-hex": "raw-hex: route color through VX.* / the Mantine theme instead of a hardcoded hex literal.",
|
|
94
|
-
"raw-color-fn": "raw-color-fn: route color through VX.* / the Mantine theme instead of a hardcoded rgb()/hsl() literal.",
|
|
95
|
-
"off-identity-accent": "off-identity-accent: use blue/gray or a status hue (red/green/orange/yellow) instead.",
|
|
96
|
-
"raw-spacing": "raw-spacing: use the spacing scale token instead of a raw literal.",
|
|
97
|
-
"raw-radius": "raw-radius: use the radius scale token instead of a raw literal.",
|
|
98
|
-
"raw-surface": "raw-surface: use `VX.surface.*` / a `radius` token / `var(--vx-radius-card)` instead of an inline border/radius/shadow.",
|
|
99
|
-
"card-with-border": "card-with-border: `withBorder` on a Card/Paper double-draws the edge \u2014 card depth is `--vx-shadow-card`, which already bakes a 1px ring into the shadow. Drop the prop.",
|
|
100
|
-
"off-system-surface-var": "off-system-surface-var: raw Mantine ramp step bypasses the basalt surface tokens \u2014 use VX.surface.* / --vx-surface-* (border/panel/subtle/bg) instead.",
|
|
101
|
-
"raw-html-layout": "raw-html-layout: raw HTML element with inline layout/surface styling \u2014 use a Mantine layout primitive (Box/Flex/Grid/Stack/Group).",
|
|
102
|
-
"inline-spacing": "inline-spacing: inline spacing literal \u2014 use the Mantine spacing prop/token (p/m/gap with xs..xl).",
|
|
103
|
-
"inline-display": "inline-display: use <Flex>/<Grid>/<Group> instead of an inline display:flex/grid.",
|
|
104
|
-
"raw-visx-axis": "raw-visx-axis: raw <AxisLeft>/<AxisBottom>/<AxisRight> in a chart file \u2014 use AxisLeftNumeric / AxisBottomDate / AxisRightNumeric from the charts primitives.",
|
|
105
|
-
"raw-motion-value": "raw-motion-value: hardcoded duration/spring/ease in `transition={{...}}` \u2014 use MOTION_DURATION / MOTION_SPRING / MOTION_EASE_STANDARD instead.",
|
|
106
|
-
"unframed-chart": "unframed-chart: hand-rolled ChartLegend built from an inline array literal \u2014 pass a derived legend (deriveLegend(series)), or compose ChartFrame, which derives it for you.",
|
|
107
|
-
"raw-form-control": "raw-form-control: raw <input>/<select>/<textarea> bypasses the entire theme \u2014 use TextInput / NumberInput / Select / Textarea from @mantine/core.",
|
|
108
|
-
"sub-16-input-font": "sub-16-input-font: fontSize below 16 on a form control is dead code against the `!important` iOS floor in styles.css \u2014 drop the override or be honest about 16px.",
|
|
109
|
-
"raw-font-family": "raw-font-family: route the font stack through createBasaltTheme(overrides, { fonts }) / the --basalt-font-sans|head|mono vars instead of a hardcoded fontFamily literal."
|
|
110
|
-
};
|
|
111
111
|
function relativePosix(fromDir, toPath) {
|
|
112
112
|
const rel = relative(fromDir, toPath).replace(/\\/g, "/");
|
|
113
113
|
if (rel === "") return ".";
|
|
@@ -265,7 +265,7 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
265
265
|
rawFormControl: cfg.rawFormControl ?? DEFAULT_GUARD_CONFIG.rawFormControl,
|
|
266
266
|
sub16InputFont: cfg.sub16InputFont ?? DEFAULT_GUARD_CONFIG.sub16InputFont,
|
|
267
267
|
allowComment: "theme-allow",
|
|
268
|
-
exemptRules: cfg
|
|
268
|
+
exemptRules: resolveExemptRules(cfg),
|
|
269
269
|
severity: cfg.severity ?? DEFAULT_GUARD_CONFIG.severity,
|
|
270
270
|
...profile === "tokens-only" ? { profile: "tokens-only" } : {}
|
|
271
271
|
};
|
|
@@ -286,6 +286,12 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
286
286
|
}
|
|
287
287
|
return 1;
|
|
288
288
|
}
|
|
289
|
+
if (flags.includes("--audit-allows")) return auditAllows(cwd, cfg, guardCfg, scanned);
|
|
290
|
+
for (const unmatched of unmatchedExemptPatterns(guardCfg, scanned)) {
|
|
291
|
+
console.error(
|
|
292
|
+
`\u26A0 basalt-ui check-theme: "basalt.exemptRules" entry ${unmatched.kind}: "${unmatched.pattern}" ` + (unmatched.reason === "unknown-kind" ? "names no guard kind \u2014 it exempts nothing at all (a typo, or a kind that was renamed)." : "matched none of the scanned files \u2014 it suppresses nothing, and reads as coverage in a config review while enforcing as much as an empty object.")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
289
295
|
if (findings.length === 0) {
|
|
290
296
|
console.log("\u2713 Theme guard: no off-palette colors.");
|
|
291
297
|
return 0;
|
|
@@ -322,14 +328,9 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
322
328
|
report(errors, `\u2716 Theme guard: ${errors.length} off-palette / off-identity violation(s)
|
|
323
329
|
`);
|
|
324
330
|
}
|
|
325
|
-
const presentKinds = new Set(findings.map((f) => f.kind));
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
"Fix:",
|
|
329
|
-
...explanations,
|
|
330
|
-
"Add a `theme-allow` comment for a deliberate exception."
|
|
331
|
-
];
|
|
332
|
-
console.error(fixLine.join(" "));
|
|
331
|
+
const presentKinds = [...new Set(findings.map((f) => f.kind))].toSorted();
|
|
332
|
+
const waiverHints = [...new Set(findings.map((f) => guardWaiverHint(f.relPath)))].toSorted();
|
|
333
|
+
console.error(["Fix:", ...presentKinds.map(guardKindRemedy), ...waiverHints].join(" "));
|
|
333
334
|
if (findings.some((f) => SERIES_MODULE_HINT_RE.test(f.relPath))) {
|
|
334
335
|
console.error(
|
|
335
336
|
'Hint: if a flagged file IS your palette/series source, exempt it via "basalt": { "exempt": ["<path>"] } in package.json.'
|
|
@@ -337,6 +338,130 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
337
338
|
}
|
|
338
339
|
return errors.length > 0 ? 1 : 0;
|
|
339
340
|
}
|
|
341
|
+
const AUDIT_NEUTRALIZED_TOKEN = "basalt-audit-neutralized";
|
|
342
|
+
function findingKey(f) {
|
|
343
|
+
return `${f.relPath}:${f.line}:${f.kind}`;
|
|
344
|
+
}
|
|
345
|
+
const AUDIT_ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
|
|
346
|
+
function annotationRuleIds(rest) {
|
|
347
|
+
const head = (rest.replace(/^-file\b/, "").split(/—|–|:|\s-\s/)[0] ?? "").trim();
|
|
348
|
+
return head.split(",").map((word) => word.trim().replace(/^basalt\//, "")).filter((word) => /^[a-z][a-z0-9-]*$/.test(word));
|
|
349
|
+
}
|
|
350
|
+
function isAllowAnnotationLine(line, token) {
|
|
351
|
+
const at = line.indexOf(token);
|
|
352
|
+
if (at === -1) return false;
|
|
353
|
+
if (line.includes(`"basalt:${token}`)) return true;
|
|
354
|
+
return AUDIT_ANNOTATION_PREFIX.test(line.slice(0, at));
|
|
355
|
+
}
|
|
356
|
+
function auditAllows(cwd, cfg, guardCfg, scanned) {
|
|
357
|
+
const token = guardCfg.allowComment;
|
|
358
|
+
const lines = [`
|
|
359
|
+
basalt-ui check-theme --audit-allows \u2014 ${cwd}
|
|
360
|
+
`];
|
|
361
|
+
let dead = 0;
|
|
362
|
+
let live = 0;
|
|
363
|
+
let outOfReach = 0;
|
|
364
|
+
let unaccountable = 0;
|
|
365
|
+
const waiverLines = [];
|
|
366
|
+
const sources = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const rel of scanned) sources.set(rel, readFileSync(resolve(cwd, rel), "utf8"));
|
|
368
|
+
for (const rel of scanned) {
|
|
369
|
+
const text = sources.get(rel) ?? "";
|
|
370
|
+
if (!text.includes(token)) continue;
|
|
371
|
+
const baseline = checkSource(text, rel, guardCfg);
|
|
372
|
+
const baselineKeys = new Set(
|
|
373
|
+
baseline.filter((f) => f.kind !== "theme-allow-unscoped").map(findingKey)
|
|
374
|
+
);
|
|
375
|
+
const unscopedAt = new Map(
|
|
376
|
+
baseline.filter((f) => f.kind === "theme-allow-unscoped").map((f) => [f.line, f.token])
|
|
377
|
+
);
|
|
378
|
+
const fileLines = text.split("\n");
|
|
379
|
+
for (const [index, line] of fileLines.entries()) {
|
|
380
|
+
if (!isAllowAnnotationLine(line ?? "", token)) continue;
|
|
381
|
+
const probe = [...fileLines];
|
|
382
|
+
probe[index] = (line ?? "").replaceAll(token, AUDIT_NEUTRALIZED_TOKEN);
|
|
383
|
+
const revealed = checkSource(probe.join("\n"), rel, guardCfg).filter((f) => f.kind !== "theme-allow-unscoped" && !baselineKeys.has(findingKey(f))).map((f) => `${f.kind}@${f.line}`);
|
|
384
|
+
const note = unscopedAt.get(index + 1);
|
|
385
|
+
if (note !== void 0) unaccountable++;
|
|
386
|
+
const site = ` ${rel}:${index + 1}`.padEnd(52);
|
|
387
|
+
const suffix = note === void 0 ? "" : ` [${note}]`;
|
|
388
|
+
const ids = annotationRuleIds((line ?? "").slice((line ?? "").indexOf(token) + token.length));
|
|
389
|
+
if (revealed.length > 0) {
|
|
390
|
+
live++;
|
|
391
|
+
waiverLines.push(`${site} suppresses ${[...new Set(revealed)].join(", ")}${suffix}`);
|
|
392
|
+
} else if (ids.length > 0 && !ids.some((id) => Object.hasOwn(GUARD_RULES, id))) {
|
|
393
|
+
outOfReach++;
|
|
394
|
+
waiverLines.push(
|
|
395
|
+
`${site} scoped to ${ids.join(", ")} \u2014 not a check-theme kind, so this audit cannot judge it (an oxlint plugin rule, or a typo theme-allow-unscoped would report)${suffix}`
|
|
396
|
+
);
|
|
397
|
+
} else {
|
|
398
|
+
dead++;
|
|
399
|
+
waiverLines.push(`${site} SUPPRESSES NOTHING \u2014 dead, delete it${suffix}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
lines.push(`${token} annotations (${waiverLines.length}):`);
|
|
404
|
+
lines.push(...waiverLines.length === 0 ? [" (none)"] : waiverLines);
|
|
405
|
+
const exemptLines = [];
|
|
406
|
+
let exemptCount = 0;
|
|
407
|
+
const declared = Object.entries(cfg.exemptRules ?? {});
|
|
408
|
+
if (declared.length > 0) {
|
|
409
|
+
const baselineKeys = /* @__PURE__ */ new Set();
|
|
410
|
+
for (const rel of scanned) {
|
|
411
|
+
for (const f of checkSource(sources.get(rel) ?? "", rel, guardCfg))
|
|
412
|
+
baselineKeys.add(findingKey(f));
|
|
413
|
+
}
|
|
414
|
+
for (const [kind, entry] of declared) {
|
|
415
|
+
const reason = exemptRuleReason(entry);
|
|
416
|
+
for (const pattern of exemptRulePaths(entry)) {
|
|
417
|
+
exemptCount++;
|
|
418
|
+
const probeCfg = {
|
|
419
|
+
...guardCfg,
|
|
420
|
+
exemptRules: {
|
|
421
|
+
...guardCfg.exemptRules,
|
|
422
|
+
[kind]: exemptRulePaths(entry).filter((p) => p !== pattern)
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const revealed = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const rel of scanned) {
|
|
427
|
+
for (const f of checkSource(sources.get(rel) ?? "", rel, probeCfg)) {
|
|
428
|
+
if (!baselineKeys.has(findingKey(f))) revealed.add(f.relPath);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const site = ` ${kind}: "${pattern}"`.padEnd(52);
|
|
432
|
+
const why = reason === null ? " [no reason recorded]" : ` \u2014 ${reason}`;
|
|
433
|
+
if (revealed.size === 0) {
|
|
434
|
+
dead++;
|
|
435
|
+
const unmatched = unmatchedExemptPatterns(guardCfg, scanned).find(
|
|
436
|
+
(u) => u.kind === kind && u.pattern === pattern
|
|
437
|
+
);
|
|
438
|
+
exemptLines.push(
|
|
439
|
+
`${site} SUPPRESSES NOTHING \u2014 ` + (unmatched === void 0 ? "the files it matches have no such finding; delete it" : unmatched.reason === "unknown-kind" ? "it names no guard kind (a typo, or a renamed kind)" : "it matches no scanned file at all") + why
|
|
440
|
+
);
|
|
441
|
+
} else {
|
|
442
|
+
live++;
|
|
443
|
+
exemptLines.push(
|
|
444
|
+
`${site} suppresses findings in ${[...revealed].toSorted().join(", ")}${why}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
lines.push(`
|
|
451
|
+
basalt.exemptRules entries (${exemptCount}):`);
|
|
452
|
+
lines.push(...exemptLines.length === 0 ? [" (none)"] : exemptLines);
|
|
453
|
+
lines.push(
|
|
454
|
+
`
|
|
455
|
+
${live} live, ${dead} dead, ${outOfReach} outside check-theme's reach, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
|
|
456
|
+
);
|
|
457
|
+
if (dead > 0) {
|
|
458
|
+
lines.push(
|
|
459
|
+
"A dead waiver is not harmless: it is an exception nobody re-checked, and it will silently cover the next real finding on that line."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
console.log(lines.join("\n"));
|
|
463
|
+
return dead > 0 ? 1 : 0;
|
|
464
|
+
}
|
|
340
465
|
const BLOCK_BEGIN_PREFIX = "<!-- basalt:begin";
|
|
341
466
|
const BLOCK_END = "<!-- basalt:end -->";
|
|
342
467
|
const MANIFEST_PATH = ".basalt/manifest.json";
|
|
@@ -531,9 +656,24 @@ function managedFiles(peers, placement = ROOT_PLACEMENT) {
|
|
|
531
656
|
# The path is RESOLVED from where basalt-ui actually installed, not assumed at the
|
|
532
657
|
# repo root \u2014 under an isolated linker it lives beside the package that depends on it.
|
|
533
658
|
# Run \`lefthook install\` once: the file is inert until the git hooks are written.
|
|
659
|
+
#
|
|
660
|
+
# NOTE: an extends target WINS on a colliding key \u2014 a \`run:\`/\`glob:\` you write under one of
|
|
661
|
+
# the preset's command names is discarded silently. \`env:\`, \`exclude:\`, \`skip:\` and your own
|
|
662
|
+
# command names merge. See the preset itself for the sanctioned override seams.
|
|
534
663
|
extends:
|
|
535
664
|
- ${ctx.vars.LEFTHOOK_PRESET_PATH.replace(/^\.\//, "")}
|
|
536
|
-
`
|
|
665
|
+
` + // The one seam that has to be RENDERED rather than documented: the preset is read in place
|
|
666
|
+
// out of node_modules, so it cannot template the resolved bin the way check.yml does. Its
|
|
667
|
+
// default (`bunx --no-install basalt-ui`) fails loudly rather than fetching a stranger's
|
|
668
|
+
// copy; this pins it to the local one whenever basalt actually resolved.
|
|
669
|
+
(ctx.vars.BASALT_BIN.startsWith("bunx ") ? "" : `
|
|
670
|
+
# The theme guard runs the LOCAL bin, not a copy \`bunx\` fetches from npm.
|
|
671
|
+
pre-commit:
|
|
672
|
+
commands:
|
|
673
|
+
check-theme:
|
|
674
|
+
env:
|
|
675
|
+
BASALT_BIN: ${ctx.vars.BASALT_BIN}
|
|
676
|
+
`)
|
|
537
677
|
};
|
|
538
678
|
const ci = {
|
|
539
679
|
dest: ".github/workflows/check.yml",
|
|
@@ -690,6 +830,60 @@ function detectRoots(cwd) {
|
|
|
690
830
|
}
|
|
691
831
|
return existsSync(resolve(cwd, DEFAULT_ROOT)) ? [DEFAULT_ROOT] : [];
|
|
692
832
|
}
|
|
833
|
+
function rootCovers(root, detected) {
|
|
834
|
+
const r = root.replace(/\/+$/, "");
|
|
835
|
+
return detected === r || detected.startsWith(`${r}/`);
|
|
836
|
+
}
|
|
837
|
+
function reconcileRoots(cwd, opts) {
|
|
838
|
+
const declared = readBasaltConfig(cwd).roots;
|
|
839
|
+
const detected = detectRoots(cwd);
|
|
840
|
+
if (declared !== void 0) {
|
|
841
|
+
const unscanned = detected.filter((d) => !declared.some((r) => rootCovers(r, d)));
|
|
842
|
+
return { kind: "declared", declared, unscanned };
|
|
843
|
+
}
|
|
844
|
+
if (detected.length === 0) return { kind: "undetectable" };
|
|
845
|
+
if (!opts.write) return { kind: "declared", declared: [], unscanned: detected };
|
|
846
|
+
const wrote = patchPackageJson(cwd, (pkg) => {
|
|
847
|
+
const basalt = pkg["basalt"] ?? {};
|
|
848
|
+
if (basalt["roots"] !== void 0) return false;
|
|
849
|
+
pkg["basalt"] = { ...basalt, roots: detected };
|
|
850
|
+
return true;
|
|
851
|
+
});
|
|
852
|
+
return wrote ? { kind: "wrote", roots: detected } : { kind: "undetectable" };
|
|
853
|
+
}
|
|
854
|
+
function rootsNotices(state) {
|
|
855
|
+
if (state.kind === "wrote") {
|
|
856
|
+
return {
|
|
857
|
+
log: [
|
|
858
|
+
`wrote "basalt": { "roots": ${JSON.stringify(state.roots)} } to package.json \u2014 check-theme, the CI oxfmt globs and the default scan exemption all derive from it. Correct it if your sources live elsewhere.`
|
|
859
|
+
],
|
|
860
|
+
error: []
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (state.kind === "undetectable") {
|
|
864
|
+
return {
|
|
865
|
+
log: [],
|
|
866
|
+
error: [
|
|
867
|
+
'could not infer "basalt.roots" \u2014 no src/ here and no workspace package with one. Set "basalt": { "roots": [...] } in package.json by hand, or check-theme will fail with "0 files scanned".'
|
|
868
|
+
]
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (state.unscanned.length === 0) return { log: [], error: [] };
|
|
872
|
+
if (state.declared.length === 0) {
|
|
873
|
+
return {
|
|
874
|
+
log: [],
|
|
875
|
+
error: [
|
|
876
|
+
`"basalt.roots" is not set \u2014 the guard is running on the built-in default ("${DEFAULT_ROOT}"). This layout suggests ${JSON.stringify(state.unscanned)}; declare it in package.json.`
|
|
877
|
+
]
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
log: [
|
|
882
|
+
`keeping your "basalt.roots" (${JSON.stringify(state.declared)}) \u2014 never overwritten. The layout also has ${JSON.stringify(state.unscanned)}, which nothing scans; add it if it should be guarded, or leave it if the exclusion is deliberate.`
|
|
883
|
+
],
|
|
884
|
+
error: []
|
|
885
|
+
};
|
|
886
|
+
}
|
|
693
887
|
function stripJsonc(text) {
|
|
694
888
|
let out = "";
|
|
695
889
|
let inString = false;
|
|
@@ -733,10 +927,47 @@ function parseJsonc(text) {
|
|
|
733
927
|
return null;
|
|
734
928
|
}
|
|
735
929
|
}
|
|
736
|
-
function
|
|
737
|
-
|
|
930
|
+
function basaltPresetEntry(entries) {
|
|
931
|
+
if (!Array.isArray(entries)) return null;
|
|
932
|
+
const match = entries.find(
|
|
738
933
|
(entry) => typeof entry === "string" && entry.endsWith("basalt-ui/configs/oxlint.json")
|
|
739
934
|
);
|
|
935
|
+
return match ?? null;
|
|
936
|
+
}
|
|
937
|
+
const LEFTHOOK_CONFIG_NAMES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
|
|
938
|
+
function findLefthookConfig(dir) {
|
|
939
|
+
return LEFTHOOK_CONFIG_NAMES.find((name) => existsSync(resolve(dir, name))) ?? null;
|
|
940
|
+
}
|
|
941
|
+
function yamlUnquote(raw) {
|
|
942
|
+
return raw.trim().replace(/^['"]|['"]$/g, "");
|
|
943
|
+
}
|
|
944
|
+
function yamlExtendsEntries(text) {
|
|
945
|
+
const lines = text.split("\n");
|
|
946
|
+
const out = [];
|
|
947
|
+
for (const [index, line] of lines.entries()) {
|
|
948
|
+
const inline = /^extends:\s*\[(.*)\]\s*$/.exec(line ?? "");
|
|
949
|
+
if (inline !== null) {
|
|
950
|
+
out.push(...(inline[1] ?? "").split(",").map(yamlUnquote).filter(Boolean));
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (!/^extends:\s*(#.*)?$/.test(line ?? "")) continue;
|
|
954
|
+
for (const next of lines.slice(index + 1)) {
|
|
955
|
+
if (/^\s*(#.*)?$/.test(next ?? "")) continue;
|
|
956
|
+
const item = /^\s*-\s*(.+?)\s*$/.exec(next ?? "");
|
|
957
|
+
if (item === null) break;
|
|
958
|
+
out.push(yamlUnquote(item[1] ?? ""));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return out;
|
|
962
|
+
}
|
|
963
|
+
function inspectLefthookSeam(repoRoot) {
|
|
964
|
+
const file = findLefthookConfig(repoRoot);
|
|
965
|
+
if (file === null) return { kind: "no-file" };
|
|
966
|
+
const raw = readIfExists(resolve(repoRoot, file));
|
|
967
|
+
if (raw === null) return { kind: "no-file" };
|
|
968
|
+
const entry = yamlExtendsEntries(raw).find((e) => e.endsWith("basalt-ui/configs/lefthook.yml"));
|
|
969
|
+
if (entry === void 0) return { kind: "unwired", file };
|
|
970
|
+
return existsSync(resolve(repoRoot, entry)) ? { kind: "ok", file, entry } : { kind: "broken", file, entry };
|
|
740
971
|
}
|
|
741
972
|
function mergeOxlintExtends(cwd, presetPath) {
|
|
742
973
|
const abs = resolve(cwd, ".oxlintrc.json");
|
|
@@ -744,7 +975,7 @@ function mergeOxlintExtends(cwd, presetPath) {
|
|
|
744
975
|
if (raw === null) return "absent";
|
|
745
976
|
const cfg = parseJsonc(raw);
|
|
746
977
|
if (cfg === null) return "unreadable";
|
|
747
|
-
if (
|
|
978
|
+
if (basaltPresetEntry(cfg["extends"]) !== null) return "already";
|
|
748
979
|
if (stripJsonc(raw) !== raw) return "has-comments";
|
|
749
980
|
const existing = Array.isArray(cfg["extends"]) ? cfg["extends"].filter((entry) => typeof entry === "string") : [];
|
|
750
981
|
const merged = "extends" in cfg ? {} : { extends: [presetPath] };
|
|
@@ -769,21 +1000,7 @@ function init(cwd = process.cwd(), scaffoldFlags = {}) {
|
|
|
769
1000
|
`\u26A0 basalt-ui init: basalt-ui does not resolve from ${cwd} or any directory above it \u2014 the seeded \`extends\` paths and CI steps are being written against the repo root by assumption. Install basalt-ui here (or at the repo root) and re-run \`basalt-ui init\` so they resolve.`
|
|
770
1001
|
);
|
|
771
1002
|
}
|
|
772
|
-
|
|
773
|
-
let rootsUndetectable = false;
|
|
774
|
-
if (readBasaltConfig(cwd).roots === void 0) {
|
|
775
|
-
const detected = detectRoots(cwd);
|
|
776
|
-
if (detected.length === 0) {
|
|
777
|
-
rootsUndetectable = true;
|
|
778
|
-
} else if (patchPackageJson(cwd, (pkg) => {
|
|
779
|
-
const basalt = pkg["basalt"] ?? {};
|
|
780
|
-
if (basalt["roots"] !== void 0) return false;
|
|
781
|
-
pkg["basalt"] = { ...basalt, roots: detected };
|
|
782
|
-
return true;
|
|
783
|
-
})) {
|
|
784
|
-
seededRoots = detected;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
1003
|
+
const rootsState = reconcileRoots(cwd, { write: true });
|
|
787
1004
|
const ctx = renderContext(cwd);
|
|
788
1005
|
const lintScript = `oxlint . && ${ctx.vars.BASALT_BIN} check-theme`;
|
|
789
1006
|
const seededLintScript = patchPackageJson(cwd, (pkg) => {
|
|
@@ -847,16 +1064,9 @@ Kept (already present \u2014 basalt did not touch them):`);
|
|
|
847
1064
|
};
|
|
848
1065
|
console.log(`basalt-ui init: --merge-lint \u2014 ${mergeMessage[mergeLint]}`);
|
|
849
1066
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
if (rootsUndetectable) {
|
|
856
|
-
console.error(
|
|
857
|
-
'\u26A0 basalt-ui init: could not infer "basalt.roots" \u2014 no src/ here and no workspace package with one. Set "basalt": { "roots": [...] } in package.json by hand, or check-theme will fail with "0 files scanned".'
|
|
858
|
-
);
|
|
859
|
-
}
|
|
1067
|
+
const rootsMessages = rootsNotices(rootsState);
|
|
1068
|
+
for (const line of rootsMessages.log) console.log(`basalt-ui init: ${line}`);
|
|
1069
|
+
for (const line of rootsMessages.error) console.error(`\u26A0 basalt-ui init: ${line}`);
|
|
860
1070
|
if (seededLintScript) {
|
|
861
1071
|
console.log(`basalt-ui init: added the "lint:basalt" script (${lintScript}) to package.json.`);
|
|
862
1072
|
}
|
|
@@ -922,6 +1132,7 @@ function readOxlintPresetPlugins(pkgRoot) {
|
|
|
922
1132
|
}
|
|
923
1133
|
}
|
|
924
1134
|
function sync(opts = {}, cwd = process.cwd()) {
|
|
1135
|
+
const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
|
|
925
1136
|
const ctx = renderContext(cwd);
|
|
926
1137
|
const manifest = readManifest(cwd);
|
|
927
1138
|
if (!opts.check) migrateLegacyOxfmt(cwd, ctx.pkgRoot, manifest);
|
|
@@ -977,6 +1188,23 @@ function sync(opts = {}, cwd = process.cwd()) {
|
|
|
977
1188
|
`basalt-ui sync: skipped src/query-client.ts (found an existing query client at ${relative(cwd, placement.relocatedQueryClient)}) \u2014 import it from there instead of re-seeding at the original path.`
|
|
978
1189
|
);
|
|
979
1190
|
}
|
|
1191
|
+
const rootsMessages = rootsNotices(rootsState);
|
|
1192
|
+
for (const line of rootsMessages.log) console.log(`basalt-ui sync: ${line}`);
|
|
1193
|
+
for (const line of rootsMessages.error) console.error(`\u26A0 basalt-ui sync: ${line}`);
|
|
1194
|
+
const install = findBasaltInstall(cwd);
|
|
1195
|
+
const lefthookSeam = inspectLefthookSeam(placement.repoRoot);
|
|
1196
|
+
if (lefthookSeam.kind === "broken") {
|
|
1197
|
+
console.error(
|
|
1198
|
+
`\u26A0 basalt-ui sync: ${lefthookSeam.file} extends "${lefthookSeam.entry}", which does not exist \u2014 lefthook merges a missing extends target into ZERO commands and still exits 0, so there is no pre-commit gate. Repoint it at "${shippedAssetPath(install, placement.repoRoot, "configs/lefthook.yml")}".`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const oxlintrcRaw = readIfExists(resolve(cwd, ".oxlintrc.json"));
|
|
1202
|
+
const oxlintEntry = oxlintrcRaw === null ? null : basaltPresetEntry(parseJsonc(oxlintrcRaw)?.["extends"]);
|
|
1203
|
+
if (oxlintEntry !== null && !existsSync(resolve(cwd, oxlintEntry))) {
|
|
1204
|
+
console.error(
|
|
1205
|
+
`\u26A0 basalt-ui sync: .oxlintrc.json extends "${oxlintEntry}", which does not exist \u2014 oxlint refuses to start on a missing extends target (\`NotFound\`). Repoint it at "${shippedAssetPath(install, cwd, "configs/oxlint.json")}".`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
980
1208
|
if (opts.check) {
|
|
981
1209
|
if (driftLines.length > 0) {
|
|
982
1210
|
console.error("basalt-ui sync --check: locally-drifted managed files:");
|
|
@@ -1380,15 +1608,45 @@ basalt-ui doctor \u2014 ${cwd}
|
|
|
1380
1608
|
skip(
|
|
1381
1609
|
".oxlintrc.json does not parse as JSON/JSONC \u2014 cannot tell whether it extends the preset"
|
|
1382
1610
|
);
|
|
1383
|
-
} else if (extendsBasaltPreset(parsed["extends"])) {
|
|
1384
|
-
pass(".oxlintrc.json extends the shipped basalt-ui oxlint preset");
|
|
1385
1611
|
} else {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
)
|
|
1612
|
+
const entry = basaltPresetEntry(parsed["extends"]);
|
|
1613
|
+
const correct = shippedAssetPath(install, cwd, "configs/oxlint.json");
|
|
1614
|
+
if (entry === null) {
|
|
1615
|
+
fail(
|
|
1616
|
+
`.oxlintrc.json does NOT extend the shipped preset \u2014 every basalt/* design rule is off and oxlint reports green without them. Add "extends": ["${correct}"], or re-run \`basalt-ui init --merge-lint\` to splice it in.`
|
|
1617
|
+
);
|
|
1618
|
+
} else if (!existsSync(resolve(cwd, entry)) && entry === correct && install.dir !== null) {
|
|
1619
|
+
fail(
|
|
1620
|
+
`.oxlintrc.json extends "${entry}", which does not exist (${resolve(cwd, entry)}) \u2014 basalt-ui resolves there but ships no \`configs/oxlint.json\` at that path, so oxlint refuses to start (\`NotFound\`) and nothing is linted. Reinstall basalt-ui.`
|
|
1621
|
+
);
|
|
1622
|
+
} else if (!existsSync(resolve(cwd, entry))) {
|
|
1623
|
+
fail(
|
|
1624
|
+
`.oxlintrc.json extends "${entry}", which does not exist (${resolve(cwd, entry)}) \u2014 oxlint refuses to start on a missing extends target (\`NotFound\`), so nothing is linted at all. Repoint it at "${correct}", where basalt-ui actually resolves.`
|
|
1625
|
+
);
|
|
1626
|
+
} else {
|
|
1627
|
+
pass(`.oxlintrc.json extends the shipped basalt-ui oxlint preset (${entry})`);
|
|
1628
|
+
}
|
|
1389
1629
|
}
|
|
1390
1630
|
}
|
|
1391
1631
|
}
|
|
1632
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1633
|
+
const lefthookSeam = profile === "tokens-only" ? { kind: "n/a" } : inspectLefthookSeam(repoRoot);
|
|
1634
|
+
const lefthookCorrect = shippedAssetPath(install, repoRoot, "configs/lefthook.yml");
|
|
1635
|
+
if (lefthookSeam.kind === "n/a") {
|
|
1636
|
+
pass("lefthook-preset: n/a \u2014 a tokens-only consumer wires its own hooks");
|
|
1637
|
+
} else if (lefthookSeam.kind === "no-file") {
|
|
1638
|
+
pass(`lefthook-preset: n/a \u2014 no lefthook config at ${repoRoot}`);
|
|
1639
|
+
} else if (lefthookSeam.kind === "unwired") {
|
|
1640
|
+
warn(
|
|
1641
|
+
`${lefthookSeam.file} does not extend the shipped lefthook preset \u2014 the oxlint / oxfmt / check-theme pre-commit jobs are not wired. Add "extends: [${lefthookCorrect}]" to it.`
|
|
1642
|
+
);
|
|
1643
|
+
} else if (lefthookSeam.kind === "broken") {
|
|
1644
|
+
fail(
|
|
1645
|
+
`${lefthookSeam.file} extends "${lefthookSeam.entry}", which does not exist (${resolve(repoRoot, lefthookSeam.entry)}) \u2014 lefthook merges a missing extends target into ZERO commands and still exits 0, so this repo has no pre-commit gate and \`lefthook dump\` looks clean. Repoint it at "${lefthookCorrect}".`
|
|
1646
|
+
);
|
|
1647
|
+
} else {
|
|
1648
|
+
pass(`${lefthookSeam.file} extends the shipped lefthook preset (${lefthookSeam.entry})`);
|
|
1649
|
+
}
|
|
1392
1650
|
const publicDir = resolve(cwd, "public");
|
|
1393
1651
|
if (profile === "framework" && existsSync(publicDir)) {
|
|
1394
1652
|
const iconFiles = [
|
|
@@ -1541,10 +1799,14 @@ function generatedHeader(version, command, flags) {
|
|
|
1541
1799
|
}
|
|
1542
1800
|
function normalizeColorFunctions(css) {
|
|
1543
1801
|
return css.replace(/\b(rgba?|hsla?)\(([^()]*)\)/g, (_match, fn, args) => {
|
|
1544
|
-
const parts = args.split(",").map((part) => part.trim());
|
|
1802
|
+
const parts = args.split(",").map((part) => normalizeCssNumber(part.trim()));
|
|
1545
1803
|
return `${fn}(${parts.join(", ")})`;
|
|
1546
1804
|
});
|
|
1547
1805
|
}
|
|
1806
|
+
function normalizeCssNumber(arg) {
|
|
1807
|
+
if (!/^\d+\.\d+$/.test(arg)) return arg;
|
|
1808
|
+
return arg.replace(/\.?0+$/, "");
|
|
1809
|
+
}
|
|
1548
1810
|
const SCHEME_CLASS_SENTINEL = "data-basalt-scheme-class";
|
|
1549
1811
|
function emitGeneratedCss(content, flags, cwd, command) {
|
|
1550
1812
|
const out = flagValue(flags, "--out");
|
|
@@ -1719,7 +1981,7 @@ async function guardHook(cwd = process.cwd()) {
|
|
|
1719
1981
|
rawFormControl: cfg.rawFormControl ?? DEFAULT_GUARD_CONFIG.rawFormControl,
|
|
1720
1982
|
sub16InputFont: cfg.sub16InputFont ?? DEFAULT_GUARD_CONFIG.sub16InputFont,
|
|
1721
1983
|
allowComment: "theme-allow",
|
|
1722
|
-
exemptRules: cfg
|
|
1984
|
+
exemptRules: resolveExemptRules(cfg),
|
|
1723
1985
|
severity: cfg.severity ?? DEFAULT_GUARD_CONFIG.severity,
|
|
1724
1986
|
// Same detection check-theme uses: the hook must never block an edit over advice the app
|
|
1725
1987
|
// cannot take ("use @mantine/core's Select") in a repo with no Mantine.
|
|
@@ -1755,7 +2017,7 @@ async function guardHook(cwd = process.cwd()) {
|
|
|
1755
2017
|
}
|
|
1756
2018
|
return 0;
|
|
1757
2019
|
}
|
|
1758
|
-
const USAGE = "Usage: basalt-ui <init [--with-router] [--with-query] [--merge-lint] | sync [--force] [--check] |\n check-theme | check-coverage | info [--json]
|
|
2020
|
+
const USAGE = "Usage: basalt-ui <init [--with-router] [--with-query] [--merge-lint] | sync [--force] [--check] |\n check-theme [--audit-allows] | check-coverage | info [--json] |\n doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\ncheck-theme [--tokens-only|--framework] [--audit-allows]\n --audit-allows reports instead of scanning: every `theme-allow` annotation and every\n `basalt.exemptRules` entry, with what each one still suppresses \u2014 proved by re-running the\n guard with that one waiver neutralized, not by reading its text. Exits 1 on a waiver that\n suppresses nothing (dead), which is what makes it usable as a CI gate.\n\ntokens:css [--out <path>] [--check] [--selector-attribute <attr> | --selector-class <class>]\n [--light-class <class>] [--dark-value <v>] [--light-value <v>]\n [--default-scheme <dark|light|none>] [--media-fallback] [--only <core|all>]\n [--no-legacy-aliases]\n Emit the --vx-* stylesheet (stdout unless --out). Defaults reproduce basalt-ui/tokens.css.\n --selector-class emits `:root.dark` instead of an attribute selector (the Tailwind convention).\n --check writes nothing and exits 1 when --out differs from what would be emitted (a CI gate).\n --no-legacy-aliases drops the deprecated camelCase spellings (--vx-accentFill and friends),\n which are emitted by default as aliases of the canonical kebab-case names.\n\nfonts:css [--out <path>] [--check]\n Emit the shipped --basalt-font-* stacks as plain CSS \u2014 the typeface half of the token layer,\n otherwise reachable only by importing styles.css.\n\ndoctor [--tokens-only|--framework]\n Auto-detects a tokens-only consumer (no manifest + no @mantine/core) and checks only what\n applies. A check that cannot RUN is reported as SKIPPED and exits non-zero.\n\ncheck-theme / doctor honour BASALT_CWD, and relocate to the single workspace package carrying a\nbasalt config when invoked from a repo root that has none \u2014 several carrying one is ambiguous\nand exits 1 in BOTH. --tokens-only and --framework are alternatives; passing both is an error.\n\nEvery subcommand accepts --help / -h to print this message and exit without running.";
|
|
1759
2021
|
function run(argv, cwd = process.cwd()) {
|
|
1760
2022
|
const [cmd, ...flags] = argv;
|
|
1761
2023
|
if (cmd === "help" || cmd === "--help" || cmd === "-h" || flags.includes("--help") || flags.includes("-h")) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waiver-audit.test.d.ts","sourceRoot":"","sources":["../../src/cli/waiver-audit.test.ts"],"names":[],"mappings":""}
|
package/dist/guard/index.d.ts
CHANGED
|
@@ -8,6 +8,26 @@ import type { Finding, GuardConfig, GuardKind, GuardSeverity } from './types.js'
|
|
|
8
8
|
export type { Finding, GuardConfig, GuardKind, GuardSeverity };
|
|
9
9
|
/** Shared default config — CLI and tests import this to avoid duplication. */
|
|
10
10
|
export declare const DEFAULT_GUARD_CONFIG: GuardConfig;
|
|
11
|
+
/** One `exemptRules` entry that suppressed nothing, and why — see {@link unmatchedExemptPatterns}. */
|
|
12
|
+
export type UnmatchedExemptPattern = {
|
|
13
|
+
readonly kind: string;
|
|
14
|
+
readonly pattern: string;
|
|
15
|
+
readonly reason: 'unknown-kind' | 'no-match';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Every `exemptRules` entry that matched no scanned file, so a run can say so instead of passing.
|
|
19
|
+
*
|
|
20
|
+
* An exemption is a claim ("this rule does not apply here"), and a claim that resolves to nothing is
|
|
21
|
+
* not harmless — it reads as coverage in a config review while enforcing exactly as much as an empty
|
|
22
|
+
* object. rollhook's `["public/site.webmanifest"]` was correct, matched nothing under the
|
|
23
|
+
* segments-only matcher, and reported no diagnostic; the finding it was written for kept failing the
|
|
24
|
+
* build with the config sitting right there looking like the answer. A silent no-op exemption is the
|
|
25
|
+
* same false-green family as a guard that reports green while enforcing nothing.
|
|
26
|
+
*
|
|
27
|
+
* Called once per run by the CLI with every relative path it scanned. Kind keys are checked too: a
|
|
28
|
+
* typo'd kind name is an exemption for a rule that does not exist.
|
|
29
|
+
*/
|
|
30
|
+
export declare function unmatchedExemptPatterns(cfg: GuardConfig, scannedRelPaths: readonly string[]): UnmatchedExemptPattern[];
|
|
11
31
|
/**
|
|
12
32
|
* The exact first line of every stylesheet the CLI emits — the contract between `basalt-ui
|
|
13
33
|
* tokens:css` / `fonts:css` and this scanner, and the single source of truth for it (`src/cli`
|
|
@@ -178,13 +198,13 @@ export declare const GUARD_RULES: {
|
|
|
178
198
|
readonly 'theme-allow-unscoped': {
|
|
179
199
|
readonly kind: "theme-allow-unscoped";
|
|
180
200
|
readonly pattern: RegExp;
|
|
181
|
-
readonly message: "theme-allow without a usable rule id and a reason. Write `theme-allow <rule-id> — <why>`: the id scopes the exception to that one kind (a bare comment waives EVERY kind on the line, including ones added later), and the reason is what makes it reviewable in a diff. An id that names no rule is a typo, not a blanket waiver — it suppresses nothing.";
|
|
201
|
+
readonly message: "theme-allow without a usable rule id and a reason. Write `theme-allow <rule-id> — <why>`: the id scopes the exception to that one kind (a bare comment waives EVERY kind on the line, including ones added later), and the reason is what makes it reviewable in a diff. An id that names no rule is a typo, not a blanket waiver — it suppresses nothing. `theme-allow-file <rule-id> — <why>` declares the whole file, and must name its ids: a bare one waives nothing.";
|
|
182
202
|
};
|
|
183
203
|
readonly 'surface-shadow-override': {
|
|
184
204
|
readonly kind: "surface-shadow-override";
|
|
185
205
|
readonly pattern: RegExp;
|
|
186
206
|
readonly enabled: (cfg: GuardConfig) => boolean;
|
|
187
|
-
readonly message: "Token-composed boxShadow that REPLACES
|
|
207
|
+
readonly message: "Token-composed boxShadow that REPLACES surface depth — a card whose shadow drops --vx-shadow-card is the one card in the app with no depth. Compose with the depth token for its tier (`${VX.shadowCard}, <your ring>`; a fixed/floating surface wants --vx-shadow-overlay) rather than instead of it.";
|
|
188
208
|
};
|
|
189
209
|
readonly 'css-raw-surface': {
|
|
190
210
|
readonly kind: "css-raw-surface";
|
|
@@ -206,6 +226,31 @@ export declare const GUARD_RULES: {
|
|
|
206
226
|
readonly message: "Raw font-size literal — route through VX.text.* (numbers) / --vx-text-* (CSS), or the Mantine fz token. Same rule the oxlint plugin enforces as basalt/no-raw-font-size; this is the half a check-theme-only CI can see.";
|
|
207
227
|
};
|
|
208
228
|
};
|
|
229
|
+
/**
|
|
230
|
+
* The one-line remedy for `kind`, as a report reads it: `raw-hex: Route color through VX.* / …`.
|
|
231
|
+
*
|
|
232
|
+
* The registry has always carried a remedy per kind; `check-theme`'s `Fix:` epilogue read a
|
|
233
|
+
* hand-duplicated subset instead, and the subset was missing every kind added at 1.20.0. Since all
|
|
234
|
+
* five of those ship `warn` under grace, the findings whose whole argument is "this looks correct
|
|
235
|
+
* and is not" arrived with no argument at all — just "add a `theme-allow`", which reads as advice to
|
|
236
|
+
* waive. Sourcing the epilogue from here retires the duplicate rather than extending it.
|
|
237
|
+
*/
|
|
238
|
+
export declare function guardKindRemedy(kind: GuardKind): string;
|
|
239
|
+
/**
|
|
240
|
+
* How to waive a finding **in this file's syntax** — the closer a report prints under `Fix:`.
|
|
241
|
+
*
|
|
242
|
+
* `check-theme` printed one closer for every file class, and from 1.20.0 (when the scan reached
|
|
243
|
+
* `.webmanifest` / `.json`) it prescribed a `theme-allow` comment to files that cannot hold a
|
|
244
|
+
* comment. Two consumers read that, found no route, and fell back to a blanket `exemptRules` entry —
|
|
245
|
+
* the un-reviewable shape the release was spent retiring.
|
|
246
|
+
*
|
|
247
|
+
* A manifest gets the sharper answer first, because a hex there can never be *right*: a manifest
|
|
248
|
+
* cannot reference a CSS custom property, so the value is a hand-copy of a token that drifts the day
|
|
249
|
+
* the palette moves (which is exactly what both consumers found — two dead colours). The remedy is
|
|
250
|
+
* to stop hand-writing the file: `basaltAppPlugin` emits it from `SURFACE.bg`. The annotation is the
|
|
251
|
+
* fallback for a manifest basalt does not generate.
|
|
252
|
+
*/
|
|
253
|
+
export declare function guardWaiverHint(relPath: string): string;
|
|
209
254
|
/**
|
|
210
255
|
* Scan ONE file's text for theme-guard violations. Pure: same (text, relPath, cfg) → same Finding[].
|
|
211
256
|
* No FS, no walk, no console. The 3 dynamic regexes (forbiddenAccent, spacing, radius) are derived
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;AAyb9D,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,WAoBlC,CAAA;AAmJD,sGAAsG;AACtG,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,UAAU,CAAA;CAC7C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,WAAW,EAChB,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,sBAAsB,EAAE,CAc1B;AA8WD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,gGACwD,CAAA;AA8F1F;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,SAAS,CAqB5D,CAAA;AA2NF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;gCA0BL,WAAW;;;;;;;gCAcX,WAAW;;;;;gCAMX,WAAW;;;;;;;gCAaX,WAAW;;;;;;;gCAQX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCAaX,WAAW;;;;;;gCAMX,WAAW;;;;;;;gCAaX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;;;;;;gCAgBX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;;;;CAWmB,CAAA;AAEjD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEvD;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgBvD;AAUD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,EAAE,CA0XtF"}
|