basalt-ui 1.21.0 → 1.22.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 +3 -1
- package/CHANGELOG.md +14 -0
- package/MIGRATING.md +93 -10
- package/README.md +20 -7
- package/agent/rules/basalt-app.md +15 -9
- package/agent/rules/basalt-charts.md +2 -2
- package/agent/rules/basalt-router.md +1 -1
- package/agent/rules/basalt-state.md +4 -4
- package/agent/rules/basalt-tokens.md +9 -4
- package/agent/skills/basalt-app/SKILL.md +4 -3
- package/agent/skills/basalt-charts/SKILL.md +1 -1
- package/configs/oxlint-plugin.js +37 -1
- package/configs/oxlint-plugin.test.ts +116 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +230 -42
- package/dist/guard/index.d.ts +67 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +40 -2
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +21 -13
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/cli/check-theme.test.ts +35 -0
- package/src/cli/help.test.ts +5 -1
- package/src/cli/index.ts +434 -68
- package/src/cli/toolchain-wiring.test.ts +192 -3
- package/src/cli/waiver-audit.test.ts +84 -4
- package/src/guard/check-source.test.ts +197 -0
- package/src/guard/index.ts +129 -6
- package/src/vite.test.ts +33 -0
- package/src/vite.ts +28 -14
|
@@ -44,6 +44,13 @@ beforeEach(() => {
|
|
|
44
44
|
},
|
|
45
45
|
}),
|
|
46
46
|
)
|
|
47
|
+
// `shadow-basalt-export` and `ai-sdk-major` both scope through `isBasaltScopedFile`, so a fixture
|
|
48
|
+
// repo with NO manifest is out of scope for both and every other assertion here would pass
|
|
49
|
+
// vacuously. Written per-test by the `ai-sdk-major` block, which needs its own `ai` pin.
|
|
50
|
+
writeFileSync(
|
|
51
|
+
resolve(dir, 'package.json'),
|
|
52
|
+
JSON.stringify({ name: 'fixture', version: '0.0.0', dependencies: { 'basalt-ui': '^1.21.0' } }),
|
|
53
|
+
)
|
|
47
54
|
})
|
|
48
55
|
|
|
49
56
|
afterEach(() => {
|
|
@@ -918,6 +925,115 @@ describe('basalt/shadow-basalt-export', () => {
|
|
|
918
925
|
expect(code).toBe(0)
|
|
919
926
|
expect(rules).not.toContain('shadow-basalt-export')
|
|
920
927
|
})
|
|
928
|
+
|
|
929
|
+
// The scoping fix, mirroring `ai-sdk-major`'s. obsidian's `packages/obsidian-vault-core` is
|
|
930
|
+
// React-free, carries no basalt-ui dependency, and sits outside the root's declared
|
|
931
|
+
// `basalt.roots` — so it CANNOT import the export it was told it had forked. The rule reported
|
|
932
|
+
// `SlugTracker` there anyway, because it was the one rule in this file that never asked.
|
|
933
|
+
it('does NOT flag a package that does not depend on basalt-ui at all', () => {
|
|
934
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ name: 'standalone' }))
|
|
935
|
+
const { code, rules } = run(`export function EmptyState() { return null }\n`)
|
|
936
|
+
expect(code).toBe(0)
|
|
937
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
it('does NOT flag a file outside a declared basalt.roots', () => {
|
|
941
|
+
writeFileSync(
|
|
942
|
+
resolve(dir, 'package.json'),
|
|
943
|
+
JSON.stringify({
|
|
944
|
+
basalt: { roots: ['packages/ui/src'] },
|
|
945
|
+
devDependencies: { 'basalt-ui': '^1.21.0' },
|
|
946
|
+
}),
|
|
947
|
+
)
|
|
948
|
+
expect(
|
|
949
|
+
run(`export function EmptyState() { return null }\n`, 'packages/core/src/a.tsx').rules,
|
|
950
|
+
).not.toContain('shadow-basalt-export')
|
|
951
|
+
// …and the declared root is still policed, so the narrowing costs no real coverage.
|
|
952
|
+
expect(
|
|
953
|
+
run(`export function EmptyState() { return null }\n`, 'packages/ui/src/a.tsx').rules,
|
|
954
|
+
).toContain('shadow-basalt-export')
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
// The second half: a PascalCase name is not a component. The nine-barrel widening pulled every
|
|
958
|
+
// PascalCase value export into range, and a bare-PascalCase test then matched a plain data class
|
|
959
|
+
// (obsidian's `SlugTracker` — the same NAME basalt's `./content` exports, and nothing else).
|
|
960
|
+
it('does NOT flag a plain class that merely shares a name', () => {
|
|
961
|
+
const { code, rules } = run(`export class SlugTracker {}\n`, 'slug.ts')
|
|
962
|
+
expect(code).toBe(0)
|
|
963
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it('DOES still flag a legacy class component (one that extends)', () => {
|
|
967
|
+
const { rules } = run(`export class EmptyState extends Component {}\n`)
|
|
968
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('does NOT flag a non-function binding that shares a name', () => {
|
|
972
|
+
const { code, rules } = run(`export const EmptyState = { label: 'none' }\n`, 'consts.ts')
|
|
973
|
+
expect(code).toBe(0)
|
|
974
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
it('DOES still flag a memo/forwardRef-wrapped component', () => {
|
|
978
|
+
expect(run(`export const EmptyState = memo(() => null)\n`).rules).toContain(
|
|
979
|
+
'shadow-basalt-export',
|
|
980
|
+
)
|
|
981
|
+
expect(run(`export const EmptyState = React.forwardRef(() => null)\n`).rules).toContain(
|
|
982
|
+
'shadow-basalt-export',
|
|
983
|
+
)
|
|
984
|
+
})
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
// ── theme-allow comment shapes (the plugin half of the annotation contract) ───
|
|
988
|
+
|
|
989
|
+
// The prefix rule has had three holes found in three rounds, so every comment shape a consumer
|
|
990
|
+
// actually writes is pinned here — and the guard's copy is pinned identically in
|
|
991
|
+
// `src/guard/check-source.test.ts`. The two must agree on what an annotation IS.
|
|
992
|
+
describe('theme-allow comment shapes', () => {
|
|
993
|
+
const flags = (source: string): boolean => run(source).rules.has('no-raw-font-size')
|
|
994
|
+
|
|
995
|
+
it.each([
|
|
996
|
+
[
|
|
997
|
+
'own line //',
|
|
998
|
+
`// theme-allow no-raw-font-size — deliberate\nexport const C = <Text fz={10} />\n`,
|
|
999
|
+
],
|
|
1000
|
+
[
|
|
1001
|
+
'trailing //',
|
|
1002
|
+
`export const C = <Text fz={10} /> // theme-allow no-raw-font-size — deliberate\n`,
|
|
1003
|
+
],
|
|
1004
|
+
[
|
|
1005
|
+
'trailing block',
|
|
1006
|
+
`export const C = <Text fz={10} /* theme-allow no-raw-font-size — deliberate */ />\n`,
|
|
1007
|
+
],
|
|
1008
|
+
[
|
|
1009
|
+
'JSX expression comment',
|
|
1010
|
+
`export const C = () => (\n <div>\n {/* theme-allow no-raw-font-size — deliberate */}\n <Text fz={10} />\n </div>\n)\n`,
|
|
1011
|
+
],
|
|
1012
|
+
[
|
|
1013
|
+
'JSX expression comment, token on its own wrapped line',
|
|
1014
|
+
`export const C = () => (\n <div>\n {/*\n theme-allow no-raw-font-size — deliberate */}\n <Text fz={10} />\n </div>\n)\n`,
|
|
1015
|
+
],
|
|
1016
|
+
[
|
|
1017
|
+
'block gutter',
|
|
1018
|
+
`/**\n * theme-allow no-raw-font-size — deliberate\n */\nexport const C = <Text fz={10} />\n`,
|
|
1019
|
+
],
|
|
1020
|
+
[
|
|
1021
|
+
'docblock opener',
|
|
1022
|
+
`/** theme-allow no-raw-font-size — deliberate */\nexport const C = <Text fz={10} />\n`,
|
|
1023
|
+
],
|
|
1024
|
+
])('%s waives', (_name, source) => {
|
|
1025
|
+
expect(flags(source as string)).toBe(false)
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
// The false NEGATIVE the prefix rule exists to close: prose that MENTIONS the token is not an
|
|
1029
|
+
// annotation. linewatch documented its own waivers in a docblock and disarmed the file.
|
|
1030
|
+
it('prose that merely mentions the token does not waive', () => {
|
|
1031
|
+
expect(
|
|
1032
|
+
flags(
|
|
1033
|
+
`// each chart here needs a theme-allow no-raw-font-size eventually\nexport const C = <Text fz={10} />\n`,
|
|
1034
|
+
),
|
|
1035
|
+
).toBe(true)
|
|
1036
|
+
})
|
|
921
1037
|
})
|
|
922
1038
|
|
|
923
1039
|
// ── hand-rolled-shell ────────────────────────────────────────────────────────
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -358,8 +358,14 @@ type SyncOptions = {
|
|
|
358
358
|
* - `--check` makes NO writes; exits non-zero if any managed file is out-of-date or locally drifted
|
|
359
359
|
* (a CI freshness gate), exit 0 when all current.
|
|
360
360
|
* - `--force` overwrites locally-drifted files instead of skipping them.
|
|
361
|
+
*
|
|
362
|
+
* Runs against `resolveProjectDir(cwd)`, exactly as `check-theme` and `doctor` do — and then
|
|
363
|
+
* REFUSES rather than scaffolding when the resolved project has no manifest. See
|
|
364
|
+
* {@link findManifestAbove} for the failure that bought this: `sync` is the drift refresh and
|
|
365
|
+
* `init` is the scaffold, so placing twenty files on a cwd that was never a consumer is a decision
|
|
366
|
+
* `sync` does not get to make silently.
|
|
361
367
|
*/
|
|
362
|
-
export declare function sync(opts?: SyncOptions,
|
|
368
|
+
export declare function sync(opts?: SyncOptions, invocationCwd?: string): number;
|
|
363
369
|
/**
|
|
364
370
|
* Assert the 8 SURFACES invariants against the live SURFACES + GUARD_RULES + plugin.json.
|
|
365
371
|
* Returns 0 when all pass; 1 when any fail (console.error each failure).
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
existsSync,
|
|
@@ -14,9 +15,11 @@ import {
|
|
|
14
15
|
checkSource,
|
|
15
16
|
DEFAULT_GUARD_CONFIG,
|
|
16
17
|
GENERATED_HEADER_LINE,
|
|
18
|
+
findAllowAnnotations,
|
|
17
19
|
guardKindRemedy,
|
|
18
20
|
GUARD_RULES,
|
|
19
21
|
guardWaiverHint,
|
|
22
|
+
neutralizeAllowAnnotation,
|
|
20
23
|
TOKENS_ONLY_DISABLED_KINDS,
|
|
21
24
|
unmatchedExemptPatterns
|
|
22
25
|
} from "../guard/index.js";
|
|
@@ -219,6 +222,13 @@ function appShellFiles(rootAbs) {
|
|
|
219
222
|
out.push(...walkSourceFiles(resolve(appDir, "public")));
|
|
220
223
|
return out;
|
|
221
224
|
}
|
|
225
|
+
const PLAIN_JSON_HINT_PATH = "manifest.json";
|
|
226
|
+
function waiverHintFor(relPath, profile) {
|
|
227
|
+
if (profile !== "tokens-only" || !relPath.endsWith(".webmanifest")) {
|
|
228
|
+
return guardWaiverHint(relPath);
|
|
229
|
+
}
|
|
230
|
+
return `${guardWaiverHint(PLAIN_JSON_HINT_PATH)} (basaltAppPlugin would emit the file instead, but it needs basalt's Vite/React layer, which a tokens-only consumer has opted out of.)`;
|
|
231
|
+
}
|
|
222
232
|
function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
223
233
|
if (conflictingProfileFlags(flags)) {
|
|
224
234
|
console.error(
|
|
@@ -329,7 +339,9 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
329
339
|
`);
|
|
330
340
|
}
|
|
331
341
|
const presentKinds = [...new Set(findings.map((f) => f.kind))].toSorted();
|
|
332
|
-
const waiverHints = [
|
|
342
|
+
const waiverHints = [
|
|
343
|
+
...new Set(findings.map((f) => waiverHintFor(f.relPath, profile)))
|
|
344
|
+
].toSorted();
|
|
333
345
|
console.error(["Fix:", ...presentKinds.map(guardKindRemedy), ...waiverHints].join(" "));
|
|
334
346
|
if (findings.some((f) => SERIES_MODULE_HINT_RE.test(f.relPath))) {
|
|
335
347
|
console.error(
|
|
@@ -338,20 +350,89 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
|
|
|
338
350
|
}
|
|
339
351
|
return errors.length > 0 ? 1 : 0;
|
|
340
352
|
}
|
|
341
|
-
const AUDIT_NEUTRALIZED_TOKEN = "basalt-audit-neutralized";
|
|
342
353
|
function findingKey(f) {
|
|
343
354
|
return `${f.relPath}:${f.line}:${f.kind}`;
|
|
344
355
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
356
|
+
function resolveToolBin(dir, name) {
|
|
357
|
+
let current = dir;
|
|
358
|
+
for (; ; ) {
|
|
359
|
+
const candidate = resolve(current, "node_modules/.bin", name);
|
|
360
|
+
if (existsSync(candidate)) return candidate;
|
|
361
|
+
const parent = resolve(current, "..");
|
|
362
|
+
if (parent === current) break;
|
|
363
|
+
current = parent;
|
|
364
|
+
}
|
|
365
|
+
const probe = spawnSync(name, ["--version"], { stdio: "ignore", timeout: 1e4 });
|
|
366
|
+
return probe.error === void 0 && probe.status === 0 ? name : null;
|
|
367
|
+
}
|
|
368
|
+
function diagnosticCounts(diagnostics, filename) {
|
|
369
|
+
const counts = /* @__PURE__ */ new Map();
|
|
370
|
+
for (const d of diagnostics) {
|
|
371
|
+
if (d.filename !== filename) continue;
|
|
372
|
+
const code = typeof d.code === "string" ? d.code : "";
|
|
373
|
+
const rule = /^basalt\((.+)\)$/.exec(code)?.[1];
|
|
374
|
+
if (rule === void 0) continue;
|
|
375
|
+
const line = d.labels?.[0]?.span?.line;
|
|
376
|
+
const key = `${typeof line === "number" ? line : 0}:${rule}`;
|
|
377
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
378
|
+
}
|
|
379
|
+
return counts;
|
|
380
|
+
}
|
|
381
|
+
function probePluginRules(cwd, guardCfg, probes, sources) {
|
|
382
|
+
if (probes.length === 0) return /* @__PURE__ */ new Map();
|
|
383
|
+
const bin = resolveToolBin(cwd, "oxlint");
|
|
384
|
+
if (bin === null) return null;
|
|
385
|
+
const written = [];
|
|
386
|
+
const probePaths = /* @__PURE__ */ new Map();
|
|
387
|
+
try {
|
|
388
|
+
for (const probe of probes) {
|
|
389
|
+
const probed = neutralizeAllowAnnotation(sources.get(probe.rel) ?? "", probe.line, guardCfg);
|
|
390
|
+
const dot = probe.rel.lastIndexOf(".");
|
|
391
|
+
const rel = dot === -1 ? `${probe.rel}.basalt-audit-${probe.slot}` : `${probe.rel.slice(0, dot)}.basalt-audit-${probe.slot}${probe.rel.slice(dot)}`;
|
|
392
|
+
const abs = resolve(cwd, rel);
|
|
393
|
+
if (existsSync(abs)) continue;
|
|
394
|
+
writeFileSync(abs, probed, "utf8");
|
|
395
|
+
written.push(abs);
|
|
396
|
+
probePaths.set(probe.slot, rel);
|
|
397
|
+
}
|
|
398
|
+
if (probePaths.size === 0) return null;
|
|
399
|
+
const targets = [...new Set(probes.map((p) => p.rel)), ...probePaths.values()];
|
|
400
|
+
const run2 = spawnSync(bin, ["--format=json", "--no-error-on-unmatched-pattern", ...targets], {
|
|
401
|
+
cwd,
|
|
402
|
+
encoding: "utf8",
|
|
403
|
+
timeout: 12e4,
|
|
404
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
405
|
+
});
|
|
406
|
+
if (run2.error !== void 0 || typeof run2.stdout !== "string") return null;
|
|
407
|
+
let diagnostics;
|
|
408
|
+
try {
|
|
409
|
+
diagnostics = JSON.parse(run2.stdout).diagnostics ?? [];
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
414
|
+
for (const probe of probes) {
|
|
415
|
+
const probeRel = probePaths.get(probe.slot);
|
|
416
|
+
if (probeRel === void 0) continue;
|
|
417
|
+
const before = diagnosticCounts(diagnostics, probe.rel);
|
|
418
|
+
const after = diagnosticCounts(diagnostics, probeRel);
|
|
419
|
+
const revealed = [];
|
|
420
|
+
for (const [key, count] of after) {
|
|
421
|
+
if (count <= (before.get(key) ?? 0)) continue;
|
|
422
|
+
const [line = "", rule = ""] = key.split(":");
|
|
423
|
+
revealed.push(`${rule}@${line}`);
|
|
424
|
+
}
|
|
425
|
+
verdicts.set(probe.slot, revealed.toSorted());
|
|
426
|
+
}
|
|
427
|
+
return verdicts;
|
|
428
|
+
} finally {
|
|
429
|
+
for (const abs of written) {
|
|
430
|
+
try {
|
|
431
|
+
unlinkSync(abs);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
355
436
|
}
|
|
356
437
|
function auditAllows(cwd, cfg, guardCfg, scanned) {
|
|
357
438
|
const token = guardCfg.allowComment;
|
|
@@ -363,6 +444,7 @@ basalt-ui check-theme --audit-allows \u2014 ${cwd}
|
|
|
363
444
|
let outOfReach = 0;
|
|
364
445
|
let unaccountable = 0;
|
|
365
446
|
const waiverLines = [];
|
|
447
|
+
const pluginProbes = [];
|
|
366
448
|
const sources = /* @__PURE__ */ new Map();
|
|
367
449
|
for (const rel of scanned) sources.set(rel, readFileSync(resolve(cwd, rel), "utf8"));
|
|
368
450
|
for (const rel of scanned) {
|
|
@@ -375,30 +457,59 @@ basalt-ui check-theme --audit-allows \u2014 ${cwd}
|
|
|
375
457
|
const unscopedAt = new Map(
|
|
376
458
|
baseline.filter((f) => f.kind === "theme-allow-unscoped").map((f) => [f.line, f.token])
|
|
377
459
|
);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const note = unscopedAt.get(
|
|
460
|
+
for (const site of findAllowAnnotations(text, rel, guardCfg)) {
|
|
461
|
+
const revealed = checkSource(
|
|
462
|
+
neutralizeAllowAnnotation(text, site.line, guardCfg),
|
|
463
|
+
rel,
|
|
464
|
+
guardCfg
|
|
465
|
+
).filter((f) => f.kind !== "theme-allow-unscoped" && !baselineKeys.has(findingKey(f))).map((f) => `${f.kind}@${f.line}`);
|
|
466
|
+
const note = unscopedAt.get(site.line);
|
|
385
467
|
if (note !== void 0) unaccountable++;
|
|
386
|
-
const
|
|
468
|
+
const label = ` ${rel}:${site.line}`.padEnd(52);
|
|
387
469
|
const suffix = note === void 0 ? "" : ` [${note}]`;
|
|
388
|
-
const ids = annotationRuleIds((line ?? "").slice((line ?? "").indexOf(token) + token.length));
|
|
389
470
|
if (revealed.length > 0) {
|
|
390
471
|
live++;
|
|
391
|
-
waiverLines.push(`${
|
|
392
|
-
|
|
472
|
+
waiverLines.push(`${label} suppresses ${[...new Set(revealed)].join(", ")}${suffix}`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (site.pluginRules.length > 0 && site.guardKinds.length === 0) {
|
|
476
|
+
pluginProbes.push({
|
|
477
|
+
slot: waiverLines.length,
|
|
478
|
+
rel,
|
|
479
|
+
line: site.line,
|
|
480
|
+
ids: site.pluginRules,
|
|
481
|
+
site: label,
|
|
482
|
+
suffix
|
|
483
|
+
});
|
|
484
|
+
waiverLines.push("");
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (site.guardKinds.length === 0 && site.unknownRules.length > 0) {
|
|
393
488
|
outOfReach++;
|
|
394
489
|
waiverLines.push(
|
|
395
|
-
`${
|
|
490
|
+
`${label} scoped to ${site.unknownRules.join(", ")} \u2014 names no guard kind and no plugin rule, so this audit cannot judge it (most likely a typo, which theme-allow-unscoped reports)${suffix}`
|
|
396
491
|
);
|
|
397
|
-
|
|
398
|
-
dead++;
|
|
399
|
-
waiverLines.push(`${site} SUPPRESSES NOTHING \u2014 dead, delete it${suffix}`);
|
|
492
|
+
continue;
|
|
400
493
|
}
|
|
494
|
+
dead++;
|
|
495
|
+
waiverLines.push(`${label} SUPPRESSES NOTHING \u2014 dead, delete it${suffix}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const pluginVerdicts = probePluginRules(cwd, guardCfg, pluginProbes, sources);
|
|
499
|
+
for (const probe of pluginProbes) {
|
|
500
|
+
const revealed = pluginVerdicts?.get(probe.slot);
|
|
501
|
+
if (revealed === void 0) {
|
|
502
|
+
outOfReach++;
|
|
503
|
+
waiverLines[probe.slot] = `${probe.site} scoped to ${probe.ids.join(", ")} \u2014 an oxlint plugin rule, and oxlint could not be run here, so this audit cannot judge it${probe.suffix}`;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (revealed.length > 0) {
|
|
507
|
+
live++;
|
|
508
|
+
waiverLines[probe.slot] = `${probe.site} suppresses ${revealed.join(", ")} (oxlint)${probe.suffix}`;
|
|
509
|
+
continue;
|
|
401
510
|
}
|
|
511
|
+
dead++;
|
|
512
|
+
waiverLines[probe.slot] = `${probe.site} SUPPRESSES NOTHING \u2014 dead, delete it (proved by re-running oxlint over the file with this annotation neutralized)${probe.suffix}`;
|
|
402
513
|
}
|
|
403
514
|
lines.push(`${token} annotations (${waiverLines.length}):`);
|
|
404
515
|
lines.push(...waiverLines.length === 0 ? [" (none)"] : waiverLines);
|
|
@@ -452,7 +563,10 @@ basalt.exemptRules entries (${exemptCount}):`);
|
|
|
452
563
|
lines.push(...exemptLines.length === 0 ? [" (none)"] : exemptLines);
|
|
453
564
|
lines.push(
|
|
454
565
|
`
|
|
455
|
-
${live} live, ${dead} dead, ${outOfReach}
|
|
566
|
+
${live} live, ${dead} dead, ${outOfReach} unjudgeable, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
|
|
567
|
+
);
|
|
568
|
+
lines.push(
|
|
569
|
+
`Scope: the ${scanned.length} file(s) check-theme scans under basalt.roots (${resolveRoots(cfg).join(", ")}). A waiver outside them is not audited \u2014 widen roots to cover it, or accept that nothing polices that file.`
|
|
456
570
|
);
|
|
457
571
|
if (dead > 0) {
|
|
458
572
|
lines.push(
|
|
@@ -969,6 +1083,38 @@ function inspectLefthookSeam(repoRoot) {
|
|
|
969
1083
|
if (entry === void 0) return { kind: "unwired", file };
|
|
970
1084
|
return existsSync(resolve(repoRoot, entry)) ? { kind: "ok", file, entry } : { kind: "broken", file, entry };
|
|
971
1085
|
}
|
|
1086
|
+
const LEFTHOOK_GUARD_RUN = /^\s*run:\s*.*\bcheck-theme\b/m;
|
|
1087
|
+
function lefthookDump(repoRoot) {
|
|
1088
|
+
const localBin = resolve(repoRoot, "node_modules/.bin/lefthook");
|
|
1089
|
+
const bin = existsSync(localBin) ? localBin : "lefthook";
|
|
1090
|
+
try {
|
|
1091
|
+
const result = spawnSync(bin, ["dump"], {
|
|
1092
|
+
cwd: repoRoot,
|
|
1093
|
+
encoding: "utf8",
|
|
1094
|
+
timeout: 1e4,
|
|
1095
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1096
|
+
});
|
|
1097
|
+
if (result.error !== void 0 || result.status !== 0) return null;
|
|
1098
|
+
return result.stdout;
|
|
1099
|
+
} catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function inspectLefthookGate(repoRoot) {
|
|
1104
|
+
const seam = inspectLefthookSeam(repoRoot);
|
|
1105
|
+
if (seam.kind === "no-file") return { kind: "no-file" };
|
|
1106
|
+
if (seam.kind === "ok") return { kind: "extends-ok", file: seam.file, entry: seam.entry };
|
|
1107
|
+
if (seam.kind === "broken") {
|
|
1108
|
+
return { kind: "extends-broken", file: seam.file, entry: seam.entry };
|
|
1109
|
+
}
|
|
1110
|
+
const dump = lefthookDump(repoRoot);
|
|
1111
|
+
if (dump !== null) {
|
|
1112
|
+
return LEFTHOOK_GUARD_RUN.test(dump) ? { kind: "wired", file: seam.file, proof: "dump" } : { kind: "absent", file: seam.file };
|
|
1113
|
+
}
|
|
1114
|
+
const raw = readIfExists(resolve(repoRoot, seam.file)) ?? "";
|
|
1115
|
+
if (LEFTHOOK_GUARD_RUN.test(raw)) return { kind: "wired", file: seam.file, proof: "config" };
|
|
1116
|
+
return { kind: "advisory", file: seam.file };
|
|
1117
|
+
}
|
|
972
1118
|
function mergeOxlintExtends(cwd, presetPath) {
|
|
973
1119
|
const abs = resolve(cwd, ".oxlintrc.json");
|
|
974
1120
|
const raw = readIfExists(abs);
|
|
@@ -1131,7 +1277,37 @@ function readOxlintPresetPlugins(pkgRoot) {
|
|
|
1131
1277
|
return [];
|
|
1132
1278
|
}
|
|
1133
1279
|
}
|
|
1134
|
-
function
|
|
1280
|
+
function findManifestAbove(dir) {
|
|
1281
|
+
let current = resolve(dir, "..");
|
|
1282
|
+
for (; ; ) {
|
|
1283
|
+
if (existsSync(resolve(current, MANIFEST_PATH))) return current;
|
|
1284
|
+
const parent = resolve(current, "..");
|
|
1285
|
+
if (parent === current) return null;
|
|
1286
|
+
current = parent;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function sync(opts = {}, invocationCwd = process.cwd()) {
|
|
1290
|
+
const project = resolveProjectDir(invocationCwd);
|
|
1291
|
+
if (project.ambiguous !== null) {
|
|
1292
|
+
console.error(
|
|
1293
|
+
`\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
|
|
1294
|
+
);
|
|
1295
|
+
return 1;
|
|
1296
|
+
}
|
|
1297
|
+
const cwd = project.dir;
|
|
1298
|
+
if (project.relocatedFrom !== null) {
|
|
1299
|
+
console.log(
|
|
1300
|
+
`basalt-ui sync: no basalt config at ${project.relocatedFrom} \u2014 running in ${relativePosix(project.relocatedFrom, cwd)}, where it lives.`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (!existsSync(resolve(cwd, MANIFEST_PATH))) {
|
|
1304
|
+
const elsewhere = findManifestAbove(cwd);
|
|
1305
|
+
const wouldWrite = managedFiles(resolvePeerFlags(cwd, {}), resolvePlacement(cwd)).length;
|
|
1306
|
+
console.error(
|
|
1307
|
+
`\u2716 basalt-ui sync: no ${MANIFEST_PATH} at ${cwd} \u2014 refusing to scaffold. \`sync\` refreshes an EXISTING basalt install; creating one is \`basalt-ui init\`'s decision, and continuing here would write ${wouldWrite} managed file(s) into this directory.` + (elsewhere === null ? " No manifest exists at any ancestor either \u2014 run `basalt-ui init` here if this package really is meant to be a consumer." : ` This repo's install is at ${relativePosix(cwd, elsewhere)} (${elsewhere}) \u2014 run sync there, or set BASALT_CWD to it.`)
|
|
1308
|
+
);
|
|
1309
|
+
return 1;
|
|
1310
|
+
}
|
|
1135
1311
|
const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
|
|
1136
1312
|
const ctx = renderContext(cwd);
|
|
1137
1313
|
const manifest = readManifest(cwd);
|
|
@@ -1140,6 +1316,7 @@ function sync(opts = {}, cwd = process.cwd()) {
|
|
|
1140
1316
|
const placement = resolvePlacement(cwd);
|
|
1141
1317
|
const files = managedFiles(peers, placement);
|
|
1142
1318
|
let updated = 0;
|
|
1319
|
+
let created = 0;
|
|
1143
1320
|
let recreated = 0;
|
|
1144
1321
|
let skippedDrift = 0;
|
|
1145
1322
|
let staleForCheck = 0;
|
|
@@ -1151,10 +1328,12 @@ function sync(opts = {}, cwd = process.cwd()) {
|
|
|
1151
1328
|
missingSources.push(file.source);
|
|
1152
1329
|
continue;
|
|
1153
1330
|
}
|
|
1331
|
+
const tracked = manifest.files[file.dest] !== void 0;
|
|
1154
1332
|
if (file.mode === "seed") {
|
|
1155
1333
|
if (state.current === null && !opts.check) {
|
|
1156
1334
|
manifest.files[file.dest] = writeUnit(file, cwd, state.desired);
|
|
1157
|
-
recreated++;
|
|
1335
|
+
if (tracked) recreated++;
|
|
1336
|
+
else created++;
|
|
1158
1337
|
}
|
|
1159
1338
|
continue;
|
|
1160
1339
|
}
|
|
@@ -1172,8 +1351,9 @@ function sync(opts = {}, cwd = process.cwd()) {
|
|
|
1172
1351
|
staleForCheck++;
|
|
1173
1352
|
if (opts.check) continue;
|
|
1174
1353
|
manifest.files[file.dest] = writeUnit(file, cwd, state.desired);
|
|
1175
|
-
if (kind
|
|
1176
|
-
else
|
|
1354
|
+
if (kind !== "missing") updated++;
|
|
1355
|
+
else if (tracked) recreated++;
|
|
1356
|
+
else created++;
|
|
1177
1357
|
}
|
|
1178
1358
|
if (!placement.isPackageRepoRoot) {
|
|
1179
1359
|
console.log(
|
|
@@ -1222,7 +1402,7 @@ function sync(opts = {}, cwd = process.cwd()) {
|
|
|
1222
1402
|
writeFileEnsuringDir(resolve(cwd, MANIFEST_PATH), `${JSON.stringify(manifest, null, 2)}
|
|
1223
1403
|
`);
|
|
1224
1404
|
console.log(
|
|
1225
|
-
`basalt-ui sync: ${updated} updated, ${recreated} recreated, ${skippedDrift} skipped (drift).`
|
|
1405
|
+
`basalt-ui sync: ${updated} updated, ${created} created, ${recreated} recreated, ${skippedDrift} skipped (drift).`
|
|
1226
1406
|
);
|
|
1227
1407
|
if (driftLines.length > 0) {
|
|
1228
1408
|
console.log("Locally-edited files were skipped (run with --force to overwrite):");
|
|
@@ -1630,22 +1810,30 @@ basalt-ui doctor \u2014 ${cwd}
|
|
|
1630
1810
|
}
|
|
1631
1811
|
}
|
|
1632
1812
|
const repoRoot = findRepoRoot(cwd);
|
|
1633
|
-
const
|
|
1813
|
+
const lefthookGate = profile === "tokens-only" ? { kind: "n/a" } : inspectLefthookGate(repoRoot);
|
|
1634
1814
|
const lefthookCorrect = shippedAssetPath(install, repoRoot, "configs/lefthook.yml");
|
|
1635
|
-
if (
|
|
1815
|
+
if (lefthookGate.kind === "n/a") {
|
|
1636
1816
|
pass("lefthook-preset: n/a \u2014 a tokens-only consumer wires its own hooks");
|
|
1637
|
-
} else if (
|
|
1817
|
+
} else if (lefthookGate.kind === "no-file") {
|
|
1638
1818
|
pass(`lefthook-preset: n/a \u2014 no lefthook config at ${repoRoot}`);
|
|
1639
|
-
} else if (
|
|
1640
|
-
|
|
1641
|
-
|
|
1819
|
+
} else if (lefthookGate.kind === "extends-ok") {
|
|
1820
|
+
pass(`${lefthookGate.file} extends the shipped lefthook preset (${lefthookGate.entry})`);
|
|
1821
|
+
} else if (lefthookGate.kind === "wired") {
|
|
1822
|
+
pass(
|
|
1823
|
+
`lefthook-preset: ${lefthookGate.file} does not extend the shipped preset, but a pre-commit command runs check-theme directly \u2014 the gate exists (read from ` + (lefthookGate.proof === "dump" ? "`lefthook dump`, so extends/include/root: are all resolved)." : "the config text; `lefthook dump` was not runnable here, so an `include:` or a remote config was not consulted).")
|
|
1642
1824
|
);
|
|
1643
|
-
} else if (
|
|
1825
|
+
} else if (lefthookGate.kind === "extends-broken") {
|
|
1644
1826
|
fail(
|
|
1645
|
-
`${
|
|
1827
|
+
`${lefthookGate.file} extends "${lefthookGate.entry}", which does not exist (${resolve(repoRoot, lefthookGate.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}".`
|
|
1828
|
+
);
|
|
1829
|
+
} else if (lefthookGate.kind === "absent") {
|
|
1830
|
+
warn(
|
|
1831
|
+
`${lefthookGate.file}: \`lefthook dump\` resolves the merged config and NO pre-commit command runs check-theme \u2014 the theme guard is not gating commits. Either add "extends: [${lefthookCorrect}]", or add your own command running \`bunx basalt-ui check-theme\` (with \`root:\` if it must run in a subdirectory).`
|
|
1646
1832
|
);
|
|
1647
1833
|
} else {
|
|
1648
|
-
|
|
1834
|
+
warn(
|
|
1835
|
+
`${lefthookGate.file} names no basalt gate and \`lefthook dump\` could not be run here, so this check cannot tell a missing gate from one wired through an \`include:\` or a remote config it never read \u2014 treat it as advisory. If the guard really is unwired, add "extends: [${lefthookCorrect}]" or your own command running \`bunx basalt-ui check-theme\`.`
|
|
1836
|
+
);
|
|
1649
1837
|
}
|
|
1650
1838
|
const publicDir = resolve(cwd, "public");
|
|
1651
1839
|
if (profile === "framework" && existsSync(publicDir)) {
|
package/dist/guard/index.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export type UnmatchedExemptPattern = {
|
|
|
28
28
|
* typo'd kind name is an exemption for a rule that does not exist.
|
|
29
29
|
*/
|
|
30
30
|
export declare function unmatchedExemptPatterns(cfg: GuardConfig, scannedRelPaths: readonly string[]): UnmatchedExemptPattern[];
|
|
31
|
+
/**
|
|
32
|
+
* The oxlint plugin's rule ids. Not guard kinds — they can never suppress a `checkSource` finding —
|
|
33
|
+
* but they ARE valid ids in a `theme-allow`, so an annotation scoped to one of them must parse as
|
|
34
|
+
* accountable rather than be reported as unscoped. Kept as a literal list because the plugin is a
|
|
35
|
+
* standalone `.js` file this module must not import (see the plugin's own header).
|
|
36
|
+
*
|
|
37
|
+
* Exported for `check-theme --audit-allows`: an annotation naming one of these is outside the
|
|
38
|
+
* guard's reach, and judging it needs a `oxlint` run rather than a `checkSource` one. Keep in step
|
|
39
|
+
* with the plugin's own `KNOWN_RULE_IDS`; `configs/oxlint-plugin.test.ts` asserts the two agree.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PLUGIN_RULE_IDS: ReadonlySet<string>;
|
|
31
42
|
/**
|
|
32
43
|
* The exact first line of every stylesheet the CLI emits — the contract between `basalt-ui
|
|
33
44
|
* tokens:css` / `fonts:css` and this scanner, and the single source of truth for it (`src/cli`
|
|
@@ -262,4 +273,60 @@ export declare function guardWaiverHint(relPath: string): string;
|
|
|
262
273
|
* if (findings.some((f) => f.kind === 'raw-hex')) { ... }
|
|
263
274
|
*/
|
|
264
275
|
export declare function checkSource(text: string, relPath: string, cfg: GuardConfig): Finding[];
|
|
276
|
+
/**
|
|
277
|
+
* One `theme-allow` annotation as WRITTEN, classified by whose reach its ids fall in.
|
|
278
|
+
*
|
|
279
|
+
* `guardKinds` is what `checkSource` can judge by re-running itself with the annotation
|
|
280
|
+
* neutralized. `pluginRules` is what only an `oxlint` run can judge. `unknownRules` names no rule
|
|
281
|
+
* at all. All three empty with `bare: true` is the legacy blanket form.
|
|
282
|
+
*/
|
|
283
|
+
export type AllowAnnotationSite = {
|
|
284
|
+
/** 1-based line the annotation is written on. */
|
|
285
|
+
readonly line: number;
|
|
286
|
+
/** `'file'` for the `theme-allow-file` declaration form, `'line'` otherwise. */
|
|
287
|
+
readonly scope: 'line' | 'file';
|
|
288
|
+
/** Every id the annotation names that resolves to a real rule, `basalt/` prefix stripped. */
|
|
289
|
+
readonly rules: readonly string[];
|
|
290
|
+
/** The subset of `rules` `checkSource` owns. */
|
|
291
|
+
readonly guardKinds: readonly GuardKind[];
|
|
292
|
+
/** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
|
|
293
|
+
readonly pluginRules: readonly string[];
|
|
294
|
+
/** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
|
|
295
|
+
readonly unknownRules: readonly string[];
|
|
296
|
+
/** A written reason follows the ids. */
|
|
297
|
+
readonly hasReason: boolean;
|
|
298
|
+
/** The legacy blanket form — no ids at all, known or unknown. Only covers anything at line scope. */
|
|
299
|
+
readonly bare: boolean;
|
|
300
|
+
/** The source line, verbatim. */
|
|
301
|
+
readonly text: string;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Every allow annotation in one file, with the ids it names classified by reach.
|
|
305
|
+
*
|
|
306
|
+
* Pure, and it shares `collectAllowAnnotations` with {@link checkSource} — so a caller enumerating
|
|
307
|
+
* annotations sees exactly the set the scan honours, rather than a second regex that can drift from
|
|
308
|
+
* it. `--audit-allows` carried such a mirror and it was already one alternation behind.
|
|
309
|
+
*
|
|
310
|
+
* A file-scoped declaration appears once, at the line it is written on.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* for (const site of findAllowAnnotations(text, rel, cfg)) {
|
|
314
|
+
* if (site.pluginRules.length > 0) probeWithOxlint(neutralizeAllowAnnotation(text, site.line, cfg))
|
|
315
|
+
* }
|
|
316
|
+
*/
|
|
317
|
+
export declare function findAllowAnnotations(text: string, relPath: string, cfg: GuardConfig): AllowAnnotationSite[];
|
|
318
|
+
/** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
|
|
319
|
+
export declare const NEUTRALIZED_ALLOW_TOKEN = "basalt-audit-neutralized";
|
|
320
|
+
/**
|
|
321
|
+
* `text` with the allow token on ONE (1-based) line rewritten to {@link NEUTRALIZED_ALLOW_TOKEN},
|
|
322
|
+
* leaving every other annotation intact.
|
|
323
|
+
*
|
|
324
|
+
* The probe half of an audit: re-scan the result and the findings that appear are exactly what that
|
|
325
|
+
* one annotation suppresses. Shipped rather than left to each caller so the guard probe and the
|
|
326
|
+
* oxlint probe neutralize identically — a `--audit-allows` verdict that differs between the two
|
|
327
|
+
* halves by a substitution detail is worse than no verdict.
|
|
328
|
+
*
|
|
329
|
+
* A line with no token comes back unchanged.
|
|
330
|
+
*/
|
|
331
|
+
export declare function neutralizeAllowAnnotation(text: string, line: number, cfg: GuardConfig): string;
|
|
265
332
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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;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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
|