jig-ui 0.26.2 → 0.27.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/CHANGELOG.md +59 -0
- package/README.md +3 -3
- package/dist/index.js +834 -592
- package/examples/E-165.html +15 -0
- package/package.json +4 -2
- package/rules/00-anti-patterns.md +11 -4
- package/rules/01-modes.md +1 -1
- package/rules/02-tokens.md +5 -4
- package/rules/03-patterns.md +5 -1
- package/rules.index.json +8 -0
- package/tokens/mode.operator.css +8 -2
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { existsSync as existsSync29, readFileSync as
|
|
5
|
+
import { existsSync as existsSync29, readFileSync as readFileSync39 } from "fs";
|
|
6
6
|
import { homedir as homedir3 } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join47 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -1699,8 +1699,8 @@ function sectionsOf(entries) {
|
|
|
1699
1699
|
}
|
|
1700
1700
|
|
|
1701
1701
|
// src/commands/check.ts
|
|
1702
|
-
import { existsSync as existsSync18, readFileSync as
|
|
1703
|
-
import { join as
|
|
1702
|
+
import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync24 } from "fs";
|
|
1703
|
+
import { join as join29 } from "path";
|
|
1704
1704
|
|
|
1705
1705
|
// src/check/files.ts
|
|
1706
1706
|
import { execFileSync } from "child_process";
|
|
@@ -1952,6 +1952,7 @@ function formatReport(findings, meta) {
|
|
|
1952
1952
|
` ${meta.unscanned.count} file(s) were not scanned (${exts}) \u2014 no style extraction exists for them, so any CSS they carry is invisible to this check and a clean result above does not cover it.`
|
|
1953
1953
|
);
|
|
1954
1954
|
}
|
|
1955
|
+
if (meta.tailwindSkipped) lines.push(` Tailwind classes not checked: ${meta.tailwindSkipped}.`);
|
|
1955
1956
|
if (meta.modeUnwired && meta.modeUnwired.length > 0) {
|
|
1956
1957
|
lines.push("");
|
|
1957
1958
|
for (const p of meta.modeUnwired) lines.push(` \u26A0 ${p.message}.`);
|
|
@@ -2279,6 +2280,52 @@ function brandDecidedGaps(projectRoot) {
|
|
|
2279
2280
|
return gaps;
|
|
2280
2281
|
}
|
|
2281
2282
|
|
|
2283
|
+
// src/check/tailwind-dead.ts
|
|
2284
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
2285
|
+
import { dirname as dirname6, join as join21 } from "path";
|
|
2286
|
+
|
|
2287
|
+
// src/tailwind/ask.ts
|
|
2288
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2289
|
+
import { createRequire } from "module";
|
|
2290
|
+
import { join as join20 } from "path";
|
|
2291
|
+
import { pathToFileURL } from "url";
|
|
2292
|
+
function tailwindNode(projectRoot) {
|
|
2293
|
+
const from = createRequire(join20(projectRoot, "package.json"));
|
|
2294
|
+
try {
|
|
2295
|
+
return from.resolve("@tailwindcss/node");
|
|
2296
|
+
} catch {
|
|
2297
|
+
}
|
|
2298
|
+
for (const plugin of ["@tailwindcss/vite", "@tailwindcss/postcss", "@tailwindcss/cli"]) {
|
|
2299
|
+
try {
|
|
2300
|
+
return createRequire(from.resolve(`${plugin}/package.json`)).resolve("@tailwindcss/node");
|
|
2301
|
+
} catch {
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
function askTailwind(projectRoot, module, body, input) {
|
|
2307
|
+
const code = [
|
|
2308
|
+
"import { readFileSync } from 'node:fs';",
|
|
2309
|
+
`const { __unstable__loadDesignSystem: loadDesignSystem } = await import(${JSON.stringify(pathToFileURL(module).href)});`,
|
|
2310
|
+
"const input = JSON.parse(readFileSync(0, 'utf8'));",
|
|
2311
|
+
"const answer = (value) => process.stdout.write(JSON.stringify(value));",
|
|
2312
|
+
body
|
|
2313
|
+
].join("\n");
|
|
2314
|
+
const out = execFileSync2(process.execPath, ["--input-type=module", "-e", code], {
|
|
2315
|
+
cwd: projectRoot,
|
|
2316
|
+
input: JSON.stringify(input),
|
|
2317
|
+
encoding: "utf8",
|
|
2318
|
+
timeout: 6e4,
|
|
2319
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
2320
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2321
|
+
});
|
|
2322
|
+
return JSON.parse(out);
|
|
2323
|
+
}
|
|
2324
|
+
function whyNot(err) {
|
|
2325
|
+
const text = String(err.stderr || err.message).trim();
|
|
2326
|
+
return (text.split("\n").find((l) => /error/i.test(l)) ?? text.split("\n")[0] ?? "it did not answer").slice(0, 160);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2282
2329
|
// src/check/ext.ts
|
|
2283
2330
|
var CSS_EXTENSIONS = [".css", ".scss", ".less"];
|
|
2284
2331
|
function hasExtension(file, exts) {
|
|
@@ -2383,40 +2430,138 @@ function isReaderText(file) {
|
|
|
2383
2430
|
return hasExtension(file, STYLE_HOST_EXTENSIONS);
|
|
2384
2431
|
}
|
|
2385
2432
|
|
|
2386
|
-
// src/check/
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
"
|
|
2395
|
-
"
|
|
2396
|
-
"
|
|
2397
|
-
"
|
|
2433
|
+
// src/check/styles.ts
|
|
2434
|
+
function isStyleHost(file) {
|
|
2435
|
+
return hasExtension(file, STYLE_HOST_EXTENSIONS);
|
|
2436
|
+
}
|
|
2437
|
+
function isMarkupHost(file) {
|
|
2438
|
+
return hasExtension(file, MARKUP_HOSTS);
|
|
2439
|
+
}
|
|
2440
|
+
var MARKUP_HOSTS = [
|
|
2441
|
+
".html",
|
|
2442
|
+
".htm",
|
|
2443
|
+
".vue",
|
|
2444
|
+
".svelte",
|
|
2445
|
+
".astro",
|
|
2446
|
+
".php",
|
|
2447
|
+
".erb",
|
|
2448
|
+
".twig",
|
|
2449
|
+
".hbs",
|
|
2450
|
+
".mdx",
|
|
2451
|
+
".jsx",
|
|
2452
|
+
".tsx",
|
|
2453
|
+
".asp",
|
|
2454
|
+
".aspx",
|
|
2455
|
+
".ascx",
|
|
2456
|
+
".master",
|
|
2457
|
+
".cshtml",
|
|
2458
|
+
".vbhtml",
|
|
2459
|
+
".razor",
|
|
2460
|
+
".jsp",
|
|
2461
|
+
".jspx",
|
|
2462
|
+
".eex",
|
|
2463
|
+
".heex",
|
|
2464
|
+
".leex",
|
|
2465
|
+
".ejs",
|
|
2466
|
+
".njk",
|
|
2467
|
+
".liquid",
|
|
2468
|
+
".mustache",
|
|
2469
|
+
".vm",
|
|
2470
|
+
".ftl",
|
|
2471
|
+
".jinja",
|
|
2472
|
+
".jinja2",
|
|
2473
|
+
".j2"
|
|
2398
2474
|
];
|
|
2399
|
-
var
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2475
|
+
var INDENTED_SYNTAXES = {
|
|
2476
|
+
".pug": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
|
|
2477
|
+
".jade": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
|
|
2478
|
+
".haml": {
|
|
2479
|
+
blockMarker: /^(\s*)(?::css|%style)\s*$/,
|
|
2480
|
+
attr: /(?::style\s*=>|\bstyle\s*:)\s*(["'])([\s\S]*?)\1/g
|
|
2481
|
+
},
|
|
2482
|
+
".slim": {
|
|
2483
|
+
blockMarker: /^(\s*)(?:css:|style:)\s*$/,
|
|
2484
|
+
attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
function indentedSyntaxFor(file) {
|
|
2488
|
+
const lower = file.toLowerCase();
|
|
2489
|
+
const ext = Object.keys(INDENTED_SYNTAXES).find((e) => lower.endsWith(e));
|
|
2490
|
+
return ext ? INDENTED_SYNTAXES[ext] : void 0;
|
|
2405
2491
|
}
|
|
2406
|
-
function
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2492
|
+
function indentedSpans(source, syntax) {
|
|
2493
|
+
const spans = [];
|
|
2494
|
+
const lines = source.split("\n");
|
|
2495
|
+
const offsets = [];
|
|
2496
|
+
let at = 0;
|
|
2497
|
+
for (const line of lines) {
|
|
2498
|
+
offsets.push(at);
|
|
2499
|
+
at += line.length + 1;
|
|
2411
2500
|
}
|
|
2412
|
-
|
|
2501
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2502
|
+
const marker = syntax.blockMarker.exec(lines[i]);
|
|
2503
|
+
if (!marker) continue;
|
|
2504
|
+
const markerIndent = marker[1].length;
|
|
2505
|
+
let end = i;
|
|
2506
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2507
|
+
if (lines[j].trim() === "") {
|
|
2508
|
+
end = j;
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
const indent = lines[j].length - lines[j].trimStart().length;
|
|
2512
|
+
if (indent <= markerIndent) break;
|
|
2513
|
+
end = j;
|
|
2514
|
+
}
|
|
2515
|
+
if (end === i) continue;
|
|
2516
|
+
const start = offsets[i + 1];
|
|
2517
|
+
const last = offsets[end] + lines[end].length;
|
|
2518
|
+
spans.push({ start, end: last });
|
|
2519
|
+
i = end;
|
|
2520
|
+
}
|
|
2521
|
+
return spans;
|
|
2413
2522
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
var
|
|
2417
|
-
function
|
|
2523
|
+
var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
2524
|
+
var STYLE_ATTR = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
|
|
2525
|
+
var CSS_TAG = /\b(?:styled(?:\.\w+|\([^)]*\))(?:\s*\.\w+\([^)]*\))*|css|createGlobalStyle|keyframes|injectGlobal)\s*`/g;
|
|
2526
|
+
function readTemplate(source, openBacktick) {
|
|
2527
|
+
let i = openBacktick + 1;
|
|
2528
|
+
const start = i;
|
|
2529
|
+
while (i < source.length) {
|
|
2530
|
+
const ch = source[i];
|
|
2531
|
+
if (ch === "\\") {
|
|
2532
|
+
i += 2;
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
if (ch === "`") return { start, end: i };
|
|
2536
|
+
if (ch === "$" && source[i + 1] === "{") {
|
|
2537
|
+
let depth = 1;
|
|
2538
|
+
i += 2;
|
|
2539
|
+
while (i < source.length && depth > 0) {
|
|
2540
|
+
if (source[i] === "\\") {
|
|
2541
|
+
i += 2;
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
if (source[i] === "{") depth++;
|
|
2545
|
+
else if (source[i] === "}") depth--;
|
|
2546
|
+
else if (source[i] === "`") {
|
|
2547
|
+
const inner = readTemplate(source, i);
|
|
2548
|
+
if (!inner) return null;
|
|
2549
|
+
i = inner.end;
|
|
2550
|
+
}
|
|
2551
|
+
i++;
|
|
2552
|
+
}
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
i++;
|
|
2556
|
+
}
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
var STYLE_OBJECT = /\bstyle\s*=\s*\{\{/g;
|
|
2560
|
+
function readObject(source, open) {
|
|
2418
2561
|
let depth = 0;
|
|
2419
|
-
|
|
2562
|
+
let i = open + 1;
|
|
2563
|
+
const start = open + 2;
|
|
2564
|
+
while (i < source.length) {
|
|
2420
2565
|
const ch = source[i];
|
|
2421
2566
|
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2422
2567
|
const quote = ch;
|
|
@@ -2425,100 +2570,82 @@ function readBraced(source, open) {
|
|
|
2425
2570
|
if (source[i] === "\\") i++;
|
|
2426
2571
|
i++;
|
|
2427
2572
|
}
|
|
2573
|
+
i++;
|
|
2428
2574
|
continue;
|
|
2429
2575
|
}
|
|
2430
2576
|
if (ch === "{") depth++;
|
|
2431
|
-
else if (ch === "}"
|
|
2577
|
+
else if (ch === "}") {
|
|
2578
|
+
depth--;
|
|
2579
|
+
if (depth === 0) return { start, end: i };
|
|
2580
|
+
}
|
|
2581
|
+
i++;
|
|
2432
2582
|
}
|
|
2433
2583
|
return null;
|
|
2434
2584
|
}
|
|
2435
|
-
function
|
|
2436
|
-
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
if (opener === '"' || opener === "'") {
|
|
2442
|
-
const end = source.indexOf(opener, at + 1);
|
|
2443
|
-
if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
|
|
2444
|
-
continue;
|
|
2445
|
-
}
|
|
2446
|
-
if (opener !== "{") continue;
|
|
2447
|
-
const raw = readBraced(source, at);
|
|
2448
|
-
if (raw === null) continue;
|
|
2449
|
-
const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
|
|
2450
|
-
(q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
|
|
2451
|
-
);
|
|
2452
|
-
if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
|
|
2585
|
+
function styleObjectToCss(body) {
|
|
2586
|
+
let out = "";
|
|
2587
|
+
for (const ch of body) {
|
|
2588
|
+
if (ch === '"' || ch === "'" || ch === "`") out += " ";
|
|
2589
|
+
else if (ch === ",") out += ";";
|
|
2590
|
+
else out += ch;
|
|
2453
2591
|
}
|
|
2454
2592
|
return out;
|
|
2455
2593
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
const
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
out.push({ utility, value, kind: "colour" });
|
|
2467
|
-
continue;
|
|
2594
|
+
function maskNonStyleRegions(source, file) {
|
|
2595
|
+
if (hasExtension(file, CSS_EXTENSIONS)) return source;
|
|
2596
|
+
if (!isStyleHost(file)) return blank(source);
|
|
2597
|
+
const spans = [];
|
|
2598
|
+
const indented = indentedSyntaxFor(file);
|
|
2599
|
+
if (indented) {
|
|
2600
|
+
spans.push(...indentedSpans(source, indented));
|
|
2601
|
+
for (const m of source.matchAll(indented.attr)) {
|
|
2602
|
+
const valueStart = m.index + m[0].indexOf(m[1]) + 1;
|
|
2603
|
+
spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
|
|
2468
2604
|
}
|
|
2469
|
-
|
|
2470
|
-
const parts = LENGTH_PARTS.exec(value);
|
|
2471
|
-
if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
|
|
2472
|
-
out.push({ utility, value, kind: "length" });
|
|
2605
|
+
return applySpans(source, spans);
|
|
2473
2606
|
}
|
|
2474
|
-
|
|
2607
|
+
if (hasExtension(file, MARKUP_HOSTS)) {
|
|
2608
|
+
for (const m of source.matchAll(STYLE_BLOCK)) {
|
|
2609
|
+
const bodyStart = m.index + m[0].indexOf(">") + 1;
|
|
2610
|
+
spans.push({ start: bodyStart, end: bodyStart + m[1].length });
|
|
2611
|
+
}
|
|
2612
|
+
for (const m of source.matchAll(STYLE_ATTR)) {
|
|
2613
|
+
const valueStart = m.index + m[0].indexOf(m[1]) + 1;
|
|
2614
|
+
spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
for (const m of source.matchAll(CSS_TAG)) {
|
|
2618
|
+
const backtick = m.index + m[0].length - 1;
|
|
2619
|
+
const span = readTemplate(source, backtick);
|
|
2620
|
+
if (span) spans.push({ ...span, braced: true });
|
|
2621
|
+
}
|
|
2622
|
+
for (const m of source.matchAll(STYLE_OBJECT)) {
|
|
2623
|
+
const span = readObject(source, m.index + m[0].length - 2);
|
|
2624
|
+
if (span) spans.push({ ...span, transform: styleObjectToCss, braced: true });
|
|
2625
|
+
}
|
|
2626
|
+
return applySpans(source, spans);
|
|
2475
2627
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
"gray-950": "#030712",
|
|
2490
|
-
"slate-50": "#f8fafc",
|
|
2491
|
-
"slate-400": "#94a3b8",
|
|
2492
|
-
"slate-500": "#64748b",
|
|
2493
|
-
"slate-800": "#1e293b",
|
|
2494
|
-
"slate-900": "#0f172a",
|
|
2495
|
-
"slate-950": "#020617",
|
|
2496
|
-
"zinc-400": "#a1a1aa",
|
|
2497
|
-
"zinc-500": "#71717a",
|
|
2498
|
-
"zinc-900": "#18181b",
|
|
2499
|
-
"neutral-400": "#a3a3a3",
|
|
2500
|
-
"neutral-500": "#737373",
|
|
2501
|
-
"neutral-900": "#171717",
|
|
2502
|
-
"stone-400": "#a8a29e",
|
|
2503
|
-
"stone-500": "#78716c",
|
|
2504
|
-
"stone-900": "#1c1917"
|
|
2505
|
-
};
|
|
2506
|
-
function palettePairs(classes) {
|
|
2507
|
-
let background;
|
|
2508
|
-
let foreground;
|
|
2509
|
-
for (const token of classes.split(/\s+/)) {
|
|
2510
|
-
const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
|
|
2511
|
-
const bg = /^bg-(.+)$/.exec(bare2);
|
|
2512
|
-
const fg = /^text-(.+)$/.exec(bare2);
|
|
2513
|
-
if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
|
|
2514
|
-
if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
|
|
2628
|
+
function applySpans(source, spans) {
|
|
2629
|
+
if (spans.length === 0) return blank(source);
|
|
2630
|
+
const out = blank(source).split("");
|
|
2631
|
+
for (const span of spans) {
|
|
2632
|
+
const text = source.slice(span.start, span.end);
|
|
2633
|
+
const rendered = span.transform ? span.transform(text) : text;
|
|
2634
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
2635
|
+
out[span.start + i] = rendered[i];
|
|
2636
|
+
}
|
|
2637
|
+
if (span.braced && span.start > 0 && span.end < out.length) {
|
|
2638
|
+
out[span.start - 1] = "{";
|
|
2639
|
+
out[span.end] = "}";
|
|
2640
|
+
}
|
|
2515
2641
|
}
|
|
2516
|
-
return
|
|
2642
|
+
return out.join("");
|
|
2643
|
+
}
|
|
2644
|
+
function blank(source) {
|
|
2645
|
+
let out = "";
|
|
2646
|
+
for (const ch of source) out += ch === "\n" ? "\n" : " ";
|
|
2647
|
+
return out;
|
|
2517
2648
|
}
|
|
2518
|
-
|
|
2519
|
-
// src/check/run.ts
|
|
2520
|
-
import { readFileSync as readFileSync17 } from "fs";
|
|
2521
|
-
import { join as join21 } from "path";
|
|
2522
2649
|
|
|
2523
2650
|
// src/check/css.ts
|
|
2524
2651
|
function maskComments(source) {
|
|
@@ -2650,138 +2777,40 @@ function sourceLine(source, line) {
|
|
|
2650
2777
|
return (lines[line - 1] ?? "").trim();
|
|
2651
2778
|
}
|
|
2652
2779
|
|
|
2653
|
-
// src/check/
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
"
|
|
2662
|
-
"
|
|
2663
|
-
"
|
|
2664
|
-
"
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
".
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
".vbhtml",
|
|
2679
|
-
".razor",
|
|
2680
|
-
".jsp",
|
|
2681
|
-
".jspx",
|
|
2682
|
-
".eex",
|
|
2683
|
-
".heex",
|
|
2684
|
-
".leex",
|
|
2685
|
-
".ejs",
|
|
2686
|
-
".njk",
|
|
2687
|
-
".liquid",
|
|
2688
|
-
".mustache",
|
|
2689
|
-
".vm",
|
|
2690
|
-
".ftl",
|
|
2691
|
-
".jinja",
|
|
2692
|
-
".jinja2",
|
|
2693
|
-
".j2"
|
|
2694
|
-
];
|
|
2695
|
-
var INDENTED_SYNTAXES = {
|
|
2696
|
-
".pug": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
|
|
2697
|
-
".jade": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
|
|
2698
|
-
".haml": {
|
|
2699
|
-
blockMarker: /^(\s*)(?::css|%style)\s*$/,
|
|
2700
|
-
attr: /(?::style\s*=>|\bstyle\s*:)\s*(["'])([\s\S]*?)\1/g
|
|
2701
|
-
},
|
|
2702
|
-
".slim": {
|
|
2703
|
-
blockMarker: /^(\s*)(?:css:|style:)\s*$/,
|
|
2704
|
-
attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g
|
|
2705
|
-
}
|
|
2706
|
-
};
|
|
2707
|
-
function indentedSyntaxFor(file) {
|
|
2708
|
-
const lower = file.toLowerCase();
|
|
2709
|
-
const ext = Object.keys(INDENTED_SYNTAXES).find((e) => lower.endsWith(e));
|
|
2710
|
-
return ext ? INDENTED_SYNTAXES[ext] : void 0;
|
|
2711
|
-
}
|
|
2712
|
-
function indentedSpans(source, syntax) {
|
|
2713
|
-
const spans = [];
|
|
2714
|
-
const lines = source.split("\n");
|
|
2715
|
-
const offsets = [];
|
|
2716
|
-
let at = 0;
|
|
2717
|
-
for (const line of lines) {
|
|
2718
|
-
offsets.push(at);
|
|
2719
|
-
at += line.length + 1;
|
|
2720
|
-
}
|
|
2721
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2722
|
-
const marker = syntax.blockMarker.exec(lines[i]);
|
|
2723
|
-
if (!marker) continue;
|
|
2724
|
-
const markerIndent = marker[1].length;
|
|
2725
|
-
let end = i;
|
|
2726
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
2727
|
-
if (lines[j].trim() === "") {
|
|
2728
|
-
end = j;
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
|
-
const indent = lines[j].length - lines[j].trimStart().length;
|
|
2732
|
-
if (indent <= markerIndent) break;
|
|
2733
|
-
end = j;
|
|
2734
|
-
}
|
|
2735
|
-
if (end === i) continue;
|
|
2736
|
-
const start = offsets[i + 1];
|
|
2737
|
-
const last = offsets[end] + lines[end].length;
|
|
2738
|
-
spans.push({ start, end: last });
|
|
2739
|
-
i = end;
|
|
2740
|
-
}
|
|
2741
|
-
return spans;
|
|
2742
|
-
}
|
|
2743
|
-
var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
2744
|
-
var STYLE_ATTR = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
|
|
2745
|
-
var CSS_TAG = /\b(?:styled(?:\.\w+|\([^)]*\))(?:\s*\.\w+\([^)]*\))*|css|createGlobalStyle|keyframes|injectGlobal)\s*`/g;
|
|
2746
|
-
function readTemplate(source, openBacktick) {
|
|
2747
|
-
let i = openBacktick + 1;
|
|
2748
|
-
const start = i;
|
|
2749
|
-
while (i < source.length) {
|
|
2750
|
-
const ch = source[i];
|
|
2751
|
-
if (ch === "\\") {
|
|
2752
|
-
i += 2;
|
|
2753
|
-
continue;
|
|
2754
|
-
}
|
|
2755
|
-
if (ch === "`") return { start, end: i };
|
|
2756
|
-
if (ch === "$" && source[i + 1] === "{") {
|
|
2757
|
-
let depth = 1;
|
|
2758
|
-
i += 2;
|
|
2759
|
-
while (i < source.length && depth > 0) {
|
|
2760
|
-
if (source[i] === "\\") {
|
|
2761
|
-
i += 2;
|
|
2762
|
-
continue;
|
|
2763
|
-
}
|
|
2764
|
-
if (source[i] === "{") depth++;
|
|
2765
|
-
else if (source[i] === "}") depth--;
|
|
2766
|
-
else if (source[i] === "`") {
|
|
2767
|
-
const inner = readTemplate(source, i);
|
|
2768
|
-
if (!inner) return null;
|
|
2769
|
-
i = inner.end;
|
|
2770
|
-
}
|
|
2771
|
-
i++;
|
|
2772
|
-
}
|
|
2773
|
-
continue;
|
|
2774
|
-
}
|
|
2775
|
-
i++;
|
|
2780
|
+
// src/check/length.ts
|
|
2781
|
+
var LENGTH_UNITS = [
|
|
2782
|
+
"px",
|
|
2783
|
+
"rem",
|
|
2784
|
+
"em",
|
|
2785
|
+
"pt",
|
|
2786
|
+
"vh",
|
|
2787
|
+
"vw",
|
|
2788
|
+
"vmin",
|
|
2789
|
+
"vmax",
|
|
2790
|
+
"ch",
|
|
2791
|
+
"ex"
|
|
2792
|
+
];
|
|
2793
|
+
var UNITS = LENGTH_UNITS.join("|");
|
|
2794
|
+
var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
|
|
2795
|
+
var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
|
|
2796
|
+
function isExcludedLength(value, unit) {
|
|
2797
|
+
if (value === 0) return true;
|
|
2798
|
+
return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
|
|
2799
|
+
}
|
|
2800
|
+
function hasHardCodedLength(value) {
|
|
2801
|
+
LENGTH_SCAN_RE.lastIndex = 0;
|
|
2802
|
+
let m;
|
|
2803
|
+
while (m = LENGTH_SCAN_RE.exec(value)) {
|
|
2804
|
+
if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
|
|
2776
2805
|
}
|
|
2777
|
-
return
|
|
2806
|
+
return false;
|
|
2778
2807
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2808
|
+
|
|
2809
|
+
// src/check/tailwind.ts
|
|
2810
|
+
var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
|
|
2811
|
+
function readBraced(source, open) {
|
|
2781
2812
|
let depth = 0;
|
|
2782
|
-
let i = open
|
|
2783
|
-
const start = open + 2;
|
|
2784
|
-
while (i < source.length) {
|
|
2813
|
+
for (let i = open; i < source.length; i++) {
|
|
2785
2814
|
const ch = source[i];
|
|
2786
2815
|
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2787
2816
|
const quote = ch;
|
|
@@ -2790,83 +2819,151 @@ function readObject(source, open) {
|
|
|
2790
2819
|
if (source[i] === "\\") i++;
|
|
2791
2820
|
i++;
|
|
2792
2821
|
}
|
|
2793
|
-
i++;
|
|
2794
2822
|
continue;
|
|
2795
2823
|
}
|
|
2796
2824
|
if (ch === "{") depth++;
|
|
2797
|
-
else if (ch === "}")
|
|
2798
|
-
depth--;
|
|
2799
|
-
if (depth === 0) return { start, end: i };
|
|
2800
|
-
}
|
|
2801
|
-
i++;
|
|
2825
|
+
else if (ch === "}" && --depth === 0) return source.slice(open + 1, i);
|
|
2802
2826
|
}
|
|
2803
2827
|
return null;
|
|
2804
2828
|
}
|
|
2805
|
-
function
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2829
|
+
function classAttributeValues(source) {
|
|
2830
|
+
const out = [];
|
|
2831
|
+
const lineAt = (offset) => source.slice(0, offset).split("\n").length;
|
|
2832
|
+
for (const m of source.matchAll(CLASS_ATTR)) {
|
|
2833
|
+
const at = m.index + m[0].length;
|
|
2834
|
+
const opener = source[at];
|
|
2835
|
+
if (opener === '"' || opener === "'") {
|
|
2836
|
+
const end = source.indexOf(opener, at + 1);
|
|
2837
|
+
if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
if (opener !== "{") continue;
|
|
2841
|
+
const raw = readBraced(source, at);
|
|
2842
|
+
if (raw === null) continue;
|
|
2843
|
+
const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
|
|
2844
|
+
(q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
|
|
2845
|
+
);
|
|
2846
|
+
if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
|
|
2811
2847
|
}
|
|
2812
2848
|
return out;
|
|
2813
2849
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
}
|
|
2827
|
-
if (hasExtension(file, MARKUP_HOSTS)) {
|
|
2828
|
-
for (const m of source.matchAll(STYLE_BLOCK)) {
|
|
2829
|
-
const bodyStart = m.index + m[0].indexOf(">") + 1;
|
|
2830
|
-
spans.push({ start: bodyStart, end: bodyStart + m[1].length });
|
|
2831
|
-
}
|
|
2832
|
-
for (const m of source.matchAll(STYLE_ATTR)) {
|
|
2833
|
-
const valueStart = m.index + m[0].indexOf(m[1]) + 1;
|
|
2834
|
-
spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
|
|
2850
|
+
var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
|
|
2851
|
+
var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
|
|
2852
|
+
var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
|
|
2853
|
+
function arbitraryValues(classes) {
|
|
2854
|
+
const out = [];
|
|
2855
|
+
for (const m of classes.matchAll(ARBITRARY)) {
|
|
2856
|
+
const [, utility, rawValue] = m;
|
|
2857
|
+
const value = rawValue.replace(/_/g, " ");
|
|
2858
|
+
if (/var\(|--/.test(value)) continue;
|
|
2859
|
+
if (COLOUR.test(value)) {
|
|
2860
|
+
out.push({ utility, value, kind: "colour" });
|
|
2861
|
+
continue;
|
|
2835
2862
|
}
|
|
2863
|
+
if (!LENGTH_RE.test(value)) continue;
|
|
2864
|
+
const parts = LENGTH_PARTS.exec(value);
|
|
2865
|
+
if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
|
|
2866
|
+
out.push({ utility, value, kind: "length" });
|
|
2836
2867
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2868
|
+
return out;
|
|
2869
|
+
}
|
|
2870
|
+
var PALETTE = {
|
|
2871
|
+
white: "#ffffff",
|
|
2872
|
+
black: "#000000",
|
|
2873
|
+
"gray-50": "#f9fafb",
|
|
2874
|
+
"gray-100": "#f3f4f6",
|
|
2875
|
+
"gray-200": "#e5e7eb",
|
|
2876
|
+
"gray-300": "#d1d5db",
|
|
2877
|
+
"gray-400": "#9ca3af",
|
|
2878
|
+
"gray-500": "#6b7280",
|
|
2879
|
+
"gray-600": "#4b5563",
|
|
2880
|
+
"gray-700": "#374151",
|
|
2881
|
+
"gray-800": "#1f2937",
|
|
2882
|
+
"gray-900": "#111827",
|
|
2883
|
+
"gray-950": "#030712",
|
|
2884
|
+
"slate-50": "#f8fafc",
|
|
2885
|
+
"slate-400": "#94a3b8",
|
|
2886
|
+
"slate-500": "#64748b",
|
|
2887
|
+
"slate-800": "#1e293b",
|
|
2888
|
+
"slate-900": "#0f172a",
|
|
2889
|
+
"slate-950": "#020617",
|
|
2890
|
+
"zinc-400": "#a1a1aa",
|
|
2891
|
+
"zinc-500": "#71717a",
|
|
2892
|
+
"zinc-900": "#18181b",
|
|
2893
|
+
"neutral-400": "#a3a3a3",
|
|
2894
|
+
"neutral-500": "#737373",
|
|
2895
|
+
"neutral-900": "#171717",
|
|
2896
|
+
"stone-400": "#a8a29e",
|
|
2897
|
+
"stone-500": "#78716c",
|
|
2898
|
+
"stone-900": "#1c1917"
|
|
2899
|
+
};
|
|
2900
|
+
function palettePairs(classes) {
|
|
2901
|
+
let background;
|
|
2902
|
+
let foreground;
|
|
2903
|
+
for (const token of classes.split(/\s+/)) {
|
|
2904
|
+
const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
|
|
2905
|
+
const bg = /^bg-(.+)$/.exec(bare2);
|
|
2906
|
+
const fg = /^text-(.+)$/.exec(bare2);
|
|
2907
|
+
if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
|
|
2908
|
+
if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
|
|
2845
2909
|
}
|
|
2846
|
-
return
|
|
2910
|
+
return background && foreground ? [{ background, foreground }] : [];
|
|
2847
2911
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2912
|
+
|
|
2913
|
+
// src/check/tailwind-dead.ts
|
|
2914
|
+
var TAILWIND_IMPORT = /@import\s+(?:url\(\s*)?["']tailwindcss["']/;
|
|
2915
|
+
var QUERY = `
|
|
2916
|
+
const systems = [];
|
|
2917
|
+
for (const entry of input.entries) systems.push(await loadDesignSystem(readFileSync(entry.file, 'utf8'), { base: entry.base }));
|
|
2918
|
+
const css = systems.map((ds) => ds.candidatesToCss(input.classes));
|
|
2919
|
+
answer(input.classes.filter((c, i) => systems.some((ds) => ds.parseCandidate(c).length > 0) && css.every((list) => !list[i])));
|
|
2920
|
+
`;
|
|
2921
|
+
function definedClasses(css) {
|
|
2922
|
+
const out = /* @__PURE__ */ new Set();
|
|
2923
|
+
for (const m of css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) out.add(m[1]);
|
|
2924
|
+
return out;
|
|
2925
|
+
}
|
|
2926
|
+
function deadTailwindClasses(projectRoot, files, stylesheets) {
|
|
2927
|
+
const read = (f) => {
|
|
2928
|
+
try {
|
|
2929
|
+
return readFileSync16(join21(projectRoot, f), "utf8");
|
|
2930
|
+
} catch {
|
|
2931
|
+
return "";
|
|
2856
2932
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2933
|
+
};
|
|
2934
|
+
const entries = stylesheets.filter((f) => hasExtension(f, CSS_EXTENSIONS) && TAILWIND_IMPORT.test(maskComments(read(f))));
|
|
2935
|
+
if (entries.length === 0) return { dead: [] };
|
|
2936
|
+
const module = tailwindNode(projectRoot);
|
|
2937
|
+
if (!module) return { dead: [], skipped: "a stylesheet imports Tailwind v4, but @tailwindcss/node is not installed here, so no class was asked about" };
|
|
2938
|
+
const defined = /* @__PURE__ */ new Set();
|
|
2939
|
+
for (const f of stylesheets) for (const c of definedClasses(maskComments(maskNonStyleRegions(read(f), f)))) defined.add(c);
|
|
2940
|
+
const uses = [];
|
|
2941
|
+
for (const file of files) {
|
|
2942
|
+
if (!isMarkupHost(file)) continue;
|
|
2943
|
+
for (const { classes, line } of classAttributeValues(read(file))) {
|
|
2944
|
+
for (const className of classes.split(/\s+/)) {
|
|
2945
|
+
if (!className || /[{}$`'"]/.test(className)) continue;
|
|
2946
|
+
uses.push({ file, line, className });
|
|
2947
|
+
}
|
|
2860
2948
|
}
|
|
2861
2949
|
}
|
|
2862
|
-
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2950
|
+
const candidates = [...new Set(uses.map((u) => u.className))].filter((c) => !defined.has(c.split(":").pop()));
|
|
2951
|
+
if (candidates.length === 0) return { dead: [] };
|
|
2952
|
+
let dead;
|
|
2953
|
+
try {
|
|
2954
|
+
const input = { entries: entries.map((e) => ({ file: join21(projectRoot, e), base: dirname6(join21(projectRoot, e)) })), classes: candidates };
|
|
2955
|
+
dead = askTailwind(projectRoot, module, QUERY, input);
|
|
2956
|
+
} catch (err) {
|
|
2957
|
+
return { dead: [], skipped: `Tailwind could not be asked which classes generate CSS (${whyNot(err)})` };
|
|
2958
|
+
}
|
|
2959
|
+
const deadSet = new Set(dead);
|
|
2960
|
+
return { dead: uses.filter((u) => deadSet.has(u.className)) };
|
|
2868
2961
|
}
|
|
2869
2962
|
|
|
2963
|
+
// src/check/run.ts
|
|
2964
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
2965
|
+
import { join as join23 } from "path";
|
|
2966
|
+
|
|
2870
2967
|
// src/check/finding.ts
|
|
2871
2968
|
function mkFinding(ctx, detector, file, line, message, excerpt) {
|
|
2872
2969
|
return {
|
|
@@ -3329,6 +3426,7 @@ var REM_TO_PX = 16;
|
|
|
3329
3426
|
var FONT_SIZE_DECL_RE = /(?<![-\w])font-size\s*:\s*([^;]+?)(?:;|$)/i;
|
|
3330
3427
|
var FONT_SIZE_PX_RE = /^(-?\d*\.?\d+)px$/i;
|
|
3331
3428
|
var FONT_SIZE_REM_RE = /^(-?\d*\.?\d+)rem$/i;
|
|
3429
|
+
var NOT_LARGER_RE = /^(?:0*\.\d+em|1(?:\.0+)?em|(?:\d{1,2}(?:\.\d+)?|100)%|smaller|small|x-small|xx-small)$/i;
|
|
3332
3430
|
var BOLD_RE = /(?<![-\w])font-weight\s*:\s*(bold|[6-9]\d\d)/i;
|
|
3333
3431
|
function resolveFloor(body) {
|
|
3334
3432
|
const m = FONT_SIZE_DECL_RE.exec(body);
|
|
@@ -3339,6 +3437,7 @@ function resolveFloor(body) {
|
|
|
3339
3437
|
let px2;
|
|
3340
3438
|
if (pxMatch) px2 = parseFloat(pxMatch[1]);
|
|
3341
3439
|
else if (remMatch) px2 = parseFloat(remMatch[1]) * REM_TO_PX;
|
|
3440
|
+
else if (NOT_LARGER_RE.test(raw)) return FLOOR;
|
|
3342
3441
|
else return LARGE_FLOOR;
|
|
3343
3442
|
const large = BOLD_RE.test(body) ? px2 >= 18.66 : px2 >= 24;
|
|
3344
3443
|
return large ? LARGE_FLOOR : FLOOR;
|
|
@@ -3669,9 +3768,22 @@ var viewportHeight = {
|
|
|
3669
3768
|
|
|
3670
3769
|
// src/check/detectors/input-zoom.ts
|
|
3671
3770
|
var SELECTOR_RE = /(?:^|[\s,>+~(])(?:input|select|textarea)\b(?![-\w])|\[contenteditable/i;
|
|
3672
|
-
var SIZE_RE = /(?<![-\w])font
|
|
3673
|
-
var TOUCH_QUERY_RE =
|
|
3771
|
+
var SIZE_RE = /(?<![-\w])font(?:-size)?\s*:\s*([^;}]+)/gi;
|
|
3772
|
+
var TOUCH_QUERY_RE = /^@media[^{]*\b(?:pointer\s*:\s*coarse|hover\s*:\s*none)\b/i;
|
|
3773
|
+
var BODY_SELECTOR_RE = /(?:^|,)\s*(?:html\s*,\s*)?body\s*(?:,|$)/i;
|
|
3674
3774
|
var ROOT_PX = 16;
|
|
3775
|
+
function touchFormText(source) {
|
|
3776
|
+
const out = { body: false, control: false };
|
|
3777
|
+
for (const media of splitRuleBlocks(source)) {
|
|
3778
|
+
if (!TOUCH_QUERY_RE.test(media.selector.trim())) continue;
|
|
3779
|
+
for (const block of leafBlocks(media.body)) {
|
|
3780
|
+
const body = /--text-body\s*:\s*([^;}]+)/i.exec(block.body);
|
|
3781
|
+
if (body && (pixels(body[1], void 0) ?? 0) >= ROOT_PX) out.body = true;
|
|
3782
|
+
if (SELECTOR_RE.test(block.selector) && /(?<![-\w])font(?:-size)?\s*:/i.test(block.body)) out.control = true;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
return out;
|
|
3786
|
+
}
|
|
3675
3787
|
function pixels(value, mode) {
|
|
3676
3788
|
const v = value.trim().replace(/\s*!important$/i, "");
|
|
3677
3789
|
const unit = /^(\d*\.?\d+)(px|rem|em)$/i.exec(v);
|
|
@@ -3680,18 +3792,36 @@ function pixels(value, mode) {
|
|
|
3680
3792
|
if (/^var\(\s*--text-body\s*\)$/i.test(v) && mode === "operator") return 14;
|
|
3681
3793
|
return void 0;
|
|
3682
3794
|
}
|
|
3795
|
+
function sizeOf(property, value) {
|
|
3796
|
+
if (!/^font\s*:/i.test(property)) return value;
|
|
3797
|
+
const m = /(?:^|\s)((?:\d*\.?\d+(?:px|rem|em))|var\(\s*--text-[\w-]+\s*\))\s*(?:\/|\s|$)/i.exec(value.trim());
|
|
3798
|
+
return m ? m[1] : value.trim();
|
|
3799
|
+
}
|
|
3800
|
+
function bodySize(source) {
|
|
3801
|
+
for (const block of leafBlocks(source)) {
|
|
3802
|
+
if (block.atRuleDepth !== 0 || !BODY_SELECTOR_RE.test(block.selector.trim())) continue;
|
|
3803
|
+
const m = /(?<![-\w])font-size\s*:\s*([^;}]+)/i.exec(block.body);
|
|
3804
|
+
if (m) return m[1];
|
|
3805
|
+
}
|
|
3806
|
+
return void 0;
|
|
3807
|
+
}
|
|
3683
3808
|
var inputZoom = {
|
|
3684
3809
|
name: "input-zoom",
|
|
3685
3810
|
appliesTo: (file) => isStyleBearing(file),
|
|
3686
3811
|
run(source, file, ctx) {
|
|
3687
|
-
|
|
3812
|
+
const own = touchFormText(source);
|
|
3813
|
+
if (own.control || ctx.touchFormText?.control) return [];
|
|
3814
|
+
const mode = own.body || ctx.touchFormText?.body ? void 0 : ctx.mode;
|
|
3688
3815
|
const findings = [];
|
|
3689
3816
|
for (const block of leafBlocks(source)) {
|
|
3690
3817
|
if (!SELECTOR_RE.test(block.selector)) continue;
|
|
3691
3818
|
SIZE_RE.lastIndex = 0;
|
|
3692
3819
|
let m;
|
|
3693
3820
|
while (m = SIZE_RE.exec(block.body)) {
|
|
3694
|
-
|
|
3821
|
+
let value = sizeOf(m[0], m[1]);
|
|
3822
|
+
const inherited = /^inherit\b/i.test(value.trim());
|
|
3823
|
+
if (inherited) value = bodySize(source) ?? "";
|
|
3824
|
+
const px2 = pixels(value, mode);
|
|
3695
3825
|
if (px2 === void 0 || px2 >= ROOT_PX) continue;
|
|
3696
3826
|
const line = lineOfOffset(block, m.index);
|
|
3697
3827
|
findings.push(
|
|
@@ -3700,7 +3830,7 @@ var inputZoom = {
|
|
|
3700
3830
|
"input-zoom",
|
|
3701
3831
|
file,
|
|
3702
3832
|
line,
|
|
3703
|
-
`form control text resolves to ${px2}px; iOS zooms the page on focus below 16px`,
|
|
3833
|
+
inherited ? `form control inherits the body's ${px2}px; iOS zooms the page on focus below 16px` : `form control text resolves to ${px2}px; iOS zooms the page on focus below 16px`,
|
|
3704
3834
|
sourceLine(source, line)
|
|
3705
3835
|
)
|
|
3706
3836
|
);
|
|
@@ -3804,8 +3934,8 @@ var menuState = {
|
|
|
3804
3934
|
};
|
|
3805
3935
|
|
|
3806
3936
|
// src/check/declared-properties.ts
|
|
3807
|
-
import { existsSync as existsSync13, readFileSync as
|
|
3808
|
-
import { join as
|
|
3937
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
3938
|
+
import { join as join22 } from "path";
|
|
3809
3939
|
var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
|
|
3810
3940
|
var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
|
|
3811
3941
|
var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
|
|
@@ -3817,16 +3947,16 @@ function addAll(names, text) {
|
|
|
3817
3947
|
}
|
|
3818
3948
|
}
|
|
3819
3949
|
function readPackageImport(projectRoot, spec) {
|
|
3820
|
-
const base =
|
|
3950
|
+
const base = join22(projectRoot, "node_modules", spec);
|
|
3821
3951
|
for (const candidate of [base, `${base}.css`]) {
|
|
3822
3952
|
try {
|
|
3823
|
-
if (existsSync13(candidate) && candidate.endsWith(".css")) return
|
|
3953
|
+
if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync17(candidate, "utf8");
|
|
3824
3954
|
} catch {
|
|
3825
3955
|
}
|
|
3826
3956
|
}
|
|
3827
3957
|
try {
|
|
3828
|
-
const pkg = JSON.parse(
|
|
3829
|
-
if (pkg.style) return
|
|
3958
|
+
const pkg = JSON.parse(readFileSync17(join22(base, "package.json"), "utf8"));
|
|
3959
|
+
if (pkg.style) return readFileSync17(join22(base, pkg.style), "utf8");
|
|
3830
3960
|
} catch {
|
|
3831
3961
|
}
|
|
3832
3962
|
return void 0;
|
|
@@ -3838,7 +3968,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
|
|
|
3838
3968
|
for (const file of files) {
|
|
3839
3969
|
let raw;
|
|
3840
3970
|
try {
|
|
3841
|
-
raw =
|
|
3971
|
+
raw = readFileSync17(join22(projectRoot, file), "utf8");
|
|
3842
3972
|
} catch {
|
|
3843
3973
|
continue;
|
|
3844
3974
|
}
|
|
@@ -4331,6 +4461,40 @@ var interfaceSafety = {
|
|
|
4331
4461
|
}
|
|
4332
4462
|
};
|
|
4333
4463
|
|
|
4464
|
+
// src/check/detectors/new-tab.ts
|
|
4465
|
+
var BLANK_LINK2 = /<a\b([^>]*\btarget\s*=\s*\{?\s*["'`]?_blank["'`]?\s*\}?[^>]*)>([\s\S]*?)<\/a\s*>/gi;
|
|
4466
|
+
var SAYS_SO = /new\s+(?:tab|window)|opens?\s+in\s+a\s+new/i;
|
|
4467
|
+
var ATTR = /\b(?:aria-label|title)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
4468
|
+
var newTabLink = {
|
|
4469
|
+
name: "new-tab-link",
|
|
4470
|
+
appliesTo: (file) => isReaderText(file),
|
|
4471
|
+
run(_source, file, ctx) {
|
|
4472
|
+
const raw = ctx.raw;
|
|
4473
|
+
const starts = buildLineIndex(raw);
|
|
4474
|
+
const findings = [];
|
|
4475
|
+
for (const m of raw.matchAll(BLANK_LINK2)) {
|
|
4476
|
+
const attrs = m[1];
|
|
4477
|
+
const inner = m[2];
|
|
4478
|
+
const named = [...attrs.matchAll(ATTR)].map((a) => a[1] ?? a[2] ?? "").join(" ");
|
|
4479
|
+
if (SAYS_SO.test(named) || SAYS_SO.test(inner)) continue;
|
|
4480
|
+
if (/\{[^}]*\}/.test(inner.replace(/<[^>]*>/g, "")) || /\{\s*\.\.\./.test(attrs)) continue;
|
|
4481
|
+
const text = inner.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 40);
|
|
4482
|
+
const line = lineForOffset(starts, m.index);
|
|
4483
|
+
findings.push(
|
|
4484
|
+
mkFinding(
|
|
4485
|
+
ctx,
|
|
4486
|
+
"new-tab-link",
|
|
4487
|
+
file,
|
|
4488
|
+
line,
|
|
4489
|
+
`"${text || "a link"}" opens a new tab and does not say so: keep it in the same tab, or, where leaving would lose the reader's work, add "(opens in a new tab)" to its accessible name`,
|
|
4490
|
+
sourceLine(raw, line)
|
|
4491
|
+
)
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
return findings;
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
|
|
4334
4498
|
// src/check/detectors/generated-look.ts
|
|
4335
4499
|
function report(ctx, name, file, raw, hits) {
|
|
4336
4500
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4495,12 +4659,13 @@ var glowAccent = {
|
|
|
4495
4659
|
}
|
|
4496
4660
|
};
|
|
4497
4661
|
var PAGE_SELECTOR = /(?:^|[\s,>])(?:html|body|:root|main)\b|--[\w-]*(?:bg|background|surface|page|canvas)[\w-]*/i;
|
|
4662
|
+
var NOT_PAGE_TOKEN = /-(?:success|error|danger|warn|warning|info|notice|overdue|due|paid|draft|pending|status|badge|tag|chip|pill|alert|toast|hover|active|selected|highlight|mark|code|kbd|tooltip|input|button|btn)(?=-|$)/i;
|
|
4498
4663
|
var TW_CREAM = /^bg-(?:amber|orange|yellow)-(?:50|100)$|^bg-stone-(?:100|200)$/;
|
|
4499
4664
|
function cream(value, tokens) {
|
|
4500
4665
|
const c = extractColorComponents(value.trim(), tokens);
|
|
4501
4666
|
if (!c || c.alpha < 0.9) return false;
|
|
4502
4667
|
const { r, g, b } = c.rgb;
|
|
4503
|
-
return Math.min(r, g, b) >= 215 && r >= g && g >= b && r - b >= 6;
|
|
4668
|
+
return Math.min(r, g, b) >= 215 && r >= g && g >= b && r - b >= 6 && g - b >= (r - b) * 0.35;
|
|
4504
4669
|
}
|
|
4505
4670
|
var creamPalette = {
|
|
4506
4671
|
name: "cream-palette",
|
|
@@ -4510,7 +4675,7 @@ var creamPalette = {
|
|
|
4510
4675
|
for (const block of leafBlocks(source)) {
|
|
4511
4676
|
for (const m of block.body.matchAll(/(?:^|[;{\s])((?:--[\w-]+|background(?:-color)?))\s*:\s*([^;}]+)/g)) {
|
|
4512
4677
|
const prop = m[1];
|
|
4513
|
-
const onPage = prop.startsWith("--") ? PAGE_SELECTOR.test(prop) : PAGE_SELECTOR.test(block.selector);
|
|
4678
|
+
const onPage = prop.startsWith("--") ? PAGE_SELECTOR.test(prop) && !NOT_PAGE_TOKEN.test(prop) : PAGE_SELECTOR.test(block.selector);
|
|
4514
4679
|
if (!onPage || !cream(m[2], ctx.tokens)) continue;
|
|
4515
4680
|
hits.push({ line: lineOfOffset(block, m.index), message: `a cream page surface (${m[2].trim()}): the palette a model reaches for when asked to look tasteful` });
|
|
4516
4681
|
}
|
|
@@ -4685,6 +4850,7 @@ var DETECTORS = [
|
|
|
4685
4850
|
semanticElement,
|
|
4686
4851
|
metadata,
|
|
4687
4852
|
interfaceSafety,
|
|
4853
|
+
newTabLink,
|
|
4688
4854
|
...generatedLookDetectors
|
|
4689
4855
|
];
|
|
4690
4856
|
function getDetector(name) {
|
|
@@ -4692,7 +4858,7 @@ function getDetector(name) {
|
|
|
4692
4858
|
}
|
|
4693
4859
|
|
|
4694
4860
|
// src/check/run.ts
|
|
4695
|
-
function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties) {
|
|
4861
|
+
function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText2) {
|
|
4696
4862
|
const findings = [];
|
|
4697
4863
|
const active = index.filter(
|
|
4698
4864
|
(e) => !!e.detector && !!getDetector(e.detector)
|
|
@@ -4704,13 +4870,13 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4704
4870
|
if (applicable.length === 0) continue;
|
|
4705
4871
|
let raw;
|
|
4706
4872
|
try {
|
|
4707
|
-
raw =
|
|
4873
|
+
raw = readFileSync18(join23(projectRoot, file), "utf8");
|
|
4708
4874
|
} catch {
|
|
4709
4875
|
continue;
|
|
4710
4876
|
}
|
|
4711
4877
|
const source = maskComments(maskNonStyleRegions(raw, file));
|
|
4712
4878
|
for (const { entry, detector } of applicable) {
|
|
4713
|
-
const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties };
|
|
4879
|
+
const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText: touchFormText2 };
|
|
4714
4880
|
findings.push(...detector.run(source, file, ctx));
|
|
4715
4881
|
}
|
|
4716
4882
|
}
|
|
@@ -4718,8 +4884,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4718
4884
|
}
|
|
4719
4885
|
|
|
4720
4886
|
// src/check/tokens.ts
|
|
4721
|
-
import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as
|
|
4722
|
-
import { join as
|
|
4887
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "fs";
|
|
4888
|
+
import { join as join24 } from "path";
|
|
4723
4889
|
function rootDeclarations(source) {
|
|
4724
4890
|
const out = {};
|
|
4725
4891
|
for (const block of splitRuleBlocks(source)) {
|
|
@@ -4734,20 +4900,20 @@ function rootDeclarations(source) {
|
|
|
4734
4900
|
}
|
|
4735
4901
|
function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
4736
4902
|
const jig = {};
|
|
4737
|
-
const tokensDir =
|
|
4903
|
+
const tokensDir = join24(projectRoot, ".jig", "tokens");
|
|
4738
4904
|
if (existsSync14(tokensDir)) {
|
|
4739
4905
|
for (const file of readdirSync9(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
|
|
4740
|
-
Object.assign(jig, rootDeclarations(
|
|
4906
|
+
Object.assign(jig, rootDeclarations(readFileSync19(join24(tokensDir, file), "utf8")));
|
|
4741
4907
|
}
|
|
4742
4908
|
}
|
|
4743
4909
|
const consumer = {};
|
|
4744
4910
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
4745
|
-
const vendored =
|
|
4911
|
+
const vendored = join24(".jig", "tokens");
|
|
4746
4912
|
for (const file of consumerFiles) {
|
|
4747
4913
|
if (file.includes(vendored)) continue;
|
|
4748
4914
|
let source;
|
|
4749
4915
|
try {
|
|
4750
|
-
source =
|
|
4916
|
+
source = readFileSync19(join24(projectRoot, file), "utf8");
|
|
4751
4917
|
} catch {
|
|
4752
4918
|
continue;
|
|
4753
4919
|
}
|
|
@@ -4766,8 +4932,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
|
4766
4932
|
}
|
|
4767
4933
|
|
|
4768
4934
|
// src/check/exempt.ts
|
|
4769
|
-
import { existsSync as existsSync15, readFileSync as
|
|
4770
|
-
import { join as
|
|
4935
|
+
import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
|
|
4936
|
+
import { join as join25 } from "path";
|
|
4771
4937
|
function globToRegExp(glob) {
|
|
4772
4938
|
let out = "";
|
|
4773
4939
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -4787,11 +4953,11 @@ function globToRegExp(glob) {
|
|
|
4787
4953
|
}
|
|
4788
4954
|
function readExemptions(projectRoot) {
|
|
4789
4955
|
const none = { patterns: [], ignored: [] };
|
|
4790
|
-
const configPath =
|
|
4956
|
+
const configPath = join25(projectRoot, "jig.config.json");
|
|
4791
4957
|
if (!existsSync15(configPath)) return none;
|
|
4792
4958
|
let declared2;
|
|
4793
4959
|
try {
|
|
4794
|
-
declared2 = JSON.parse(
|
|
4960
|
+
declared2 = JSON.parse(readFileSync20(configPath, "utf8")).exempt;
|
|
4795
4961
|
} catch {
|
|
4796
4962
|
return none;
|
|
4797
4963
|
}
|
|
@@ -4841,8 +5007,8 @@ function applyExemptions(files, patterns) {
|
|
|
4841
5007
|
}
|
|
4842
5008
|
|
|
4843
5009
|
// src/check/waiver.ts
|
|
4844
|
-
import { readFileSync as
|
|
4845
|
-
import { join as
|
|
5010
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5011
|
+
import { join as join26 } from "path";
|
|
4846
5012
|
var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
|
|
4847
5013
|
function readWaiver(line) {
|
|
4848
5014
|
const m = WAIVER.exec(line);
|
|
@@ -4857,7 +5023,7 @@ function applyWaivers(root, findings) {
|
|
|
4857
5023
|
let cached = lines.get(file);
|
|
4858
5024
|
if (!cached) {
|
|
4859
5025
|
try {
|
|
4860
|
-
cached =
|
|
5026
|
+
cached = readFileSync21(join26(root, file), "utf8").split("\n");
|
|
4861
5027
|
} catch {
|
|
4862
5028
|
cached = [];
|
|
4863
5029
|
}
|
|
@@ -4922,8 +5088,8 @@ function hasMenuToggle(raw) {
|
|
|
4922
5088
|
}
|
|
4923
5089
|
|
|
4924
5090
|
// src/check/mode-wiring.ts
|
|
4925
|
-
import { existsSync as existsSync16, readFileSync as
|
|
4926
|
-
import { dirname as
|
|
5091
|
+
import { existsSync as existsSync16, readFileSync as readFileSync22 } from "fs";
|
|
5092
|
+
import { dirname as dirname7, join as join27 } from "path";
|
|
4927
5093
|
var MODES = ["editorial", "product", "operator"];
|
|
4928
5094
|
function configProblems(config) {
|
|
4929
5095
|
const problems = [];
|
|
@@ -4945,10 +5111,10 @@ function configProblems(config) {
|
|
|
4945
5111
|
}
|
|
4946
5112
|
function modeWiringProblems(projectRoot) {
|
|
4947
5113
|
let config;
|
|
4948
|
-
const path =
|
|
5114
|
+
const path = join27(projectRoot, "jig.config.json");
|
|
4949
5115
|
if (!existsSync16(path)) return [];
|
|
4950
5116
|
try {
|
|
4951
|
-
config = JSON.parse(
|
|
5117
|
+
config = JSON.parse(readFileSync22(path, "utf8"));
|
|
4952
5118
|
} catch {
|
|
4953
5119
|
return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
|
|
4954
5120
|
}
|
|
@@ -4957,14 +5123,14 @@ function modeWiringProblems(projectRoot) {
|
|
|
4957
5123
|
if (typeof config.brand !== "string" || !Array.isArray(config.surfaces)) return [];
|
|
4958
5124
|
const modes = [...new Set(config.surfaces.map((s) => s?.mode).filter((m) => typeof m === "string"))];
|
|
4959
5125
|
if (modes.length === 0) return [];
|
|
4960
|
-
const dir =
|
|
5126
|
+
const dir = dirname7(config.brand);
|
|
4961
5127
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
4962
5128
|
const multi = modes.length > 1;
|
|
4963
5129
|
const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
|
|
4964
|
-
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync16(
|
|
5130
|
+
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync16(join27(projectRoot, rel(f))));
|
|
4965
5131
|
if (!anyBarrel) return [];
|
|
4966
5132
|
const problems = [];
|
|
4967
|
-
if (multi && existsSync16(
|
|
5133
|
+
if (multi && existsSync16(join27(projectRoot, rel("theme.css")))) {
|
|
4968
5134
|
problems.push({
|
|
4969
5135
|
mode: modes[0],
|
|
4970
5136
|
barrel: rel("theme.css"),
|
|
@@ -4976,7 +5142,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4976
5142
|
const modeFile = `mode.${mode}.css`;
|
|
4977
5143
|
let body;
|
|
4978
5144
|
try {
|
|
4979
|
-
body =
|
|
5145
|
+
body = readFileSync22(join27(projectRoot, barrel), "utf8");
|
|
4980
5146
|
} catch {
|
|
4981
5147
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
|
|
4982
5148
|
return;
|
|
@@ -4988,7 +5154,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4988
5154
|
barrel,
|
|
4989
5155
|
message: `jig.config.json declares ${mode}, but ${barrel} imports ${imported ? `mode.${imported}.css` : "no mode file"} \u2014 the page is styled at ${imported ?? "no"} density`
|
|
4990
5156
|
});
|
|
4991
|
-
} else if (!existsSync16(
|
|
5157
|
+
} else if (!existsSync16(join27(projectRoot, rel(modeFile)))) {
|
|
4992
5158
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
4993
5159
|
}
|
|
4994
5160
|
});
|
|
@@ -4996,8 +5162,8 @@ function modeWiringProblems(projectRoot) {
|
|
|
4996
5162
|
}
|
|
4997
5163
|
|
|
4998
5164
|
// src/check/token-audit.ts
|
|
4999
|
-
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as
|
|
5000
|
-
import { join as
|
|
5165
|
+
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
|
|
5166
|
+
import { join as join28 } from "path";
|
|
5001
5167
|
var TEXT_FLOOR = 4.5;
|
|
5002
5168
|
var UI_FLOOR = 3;
|
|
5003
5169
|
var FOREGROUNDS = [
|
|
@@ -5053,24 +5219,24 @@ function floorFor(token) {
|
|
|
5053
5219
|
function tokenFiles(projectRoot) {
|
|
5054
5220
|
const read = (rel) => {
|
|
5055
5221
|
try {
|
|
5056
|
-
return
|
|
5222
|
+
return readFileSync23(join28(projectRoot, ...rel.split("/")), "utf8");
|
|
5057
5223
|
} catch {
|
|
5058
5224
|
return null;
|
|
5059
5225
|
}
|
|
5060
5226
|
};
|
|
5061
|
-
const statePath =
|
|
5227
|
+
const statePath = join28(projectRoot, ".jig", "state.json");
|
|
5062
5228
|
if (existsSync17(statePath)) {
|
|
5063
5229
|
try {
|
|
5064
|
-
const state = JSON.parse(
|
|
5230
|
+
const state = JSON.parse(readFileSync23(statePath, "utf8"));
|
|
5065
5231
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
5066
5232
|
const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
|
|
5067
5233
|
if (out.length > 0) return out;
|
|
5068
5234
|
} catch {
|
|
5069
5235
|
}
|
|
5070
5236
|
}
|
|
5071
|
-
const legacy =
|
|
5237
|
+
const legacy = join28(projectRoot, ".jig", "tokens");
|
|
5072
5238
|
if (!existsSync17(legacy)) return [];
|
|
5073
|
-
return readdirSync10(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source:
|
|
5239
|
+
return readdirSync10(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync23(join28(legacy, f), "utf8") }));
|
|
5074
5240
|
}
|
|
5075
5241
|
function auditTokenLayer(projectRoot) {
|
|
5076
5242
|
const files = tokenFiles(projectRoot);
|
|
@@ -5141,15 +5307,15 @@ function auditTokenLayer(projectRoot) {
|
|
|
5141
5307
|
function checkExitCode(result) {
|
|
5142
5308
|
return result.hasError ? 1 : 0;
|
|
5143
5309
|
}
|
|
5144
|
-
var LEGACY_INDEX_REL =
|
|
5310
|
+
var LEGACY_INDEX_REL = join29(".jig", "rules.index.json");
|
|
5145
5311
|
function resolveIndexPath(projectRoot) {
|
|
5146
|
-
const legacy =
|
|
5312
|
+
const legacy = join29(projectRoot, LEGACY_INDEX_REL);
|
|
5147
5313
|
if (existsSync18(legacy)) return legacy;
|
|
5148
|
-
return
|
|
5314
|
+
return join29(assetRoot(), "rules.index.json");
|
|
5149
5315
|
}
|
|
5150
5316
|
function resolveMode(projectRoot) {
|
|
5151
5317
|
try {
|
|
5152
|
-
const config = JSON.parse(
|
|
5318
|
+
const config = JSON.parse(readFileSync24(join29(projectRoot, "jig.config.json"), "utf8"));
|
|
5153
5319
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
5154
5320
|
if (modes.length === 1) return modes[0];
|
|
5155
5321
|
return modes.length > 1 ? "mixed" : "unknown";
|
|
@@ -5166,14 +5332,14 @@ function summariseUnscanned(files) {
|
|
|
5166
5332
|
}
|
|
5167
5333
|
function countSpecs() {
|
|
5168
5334
|
try {
|
|
5169
|
-
return loadSpecs(
|
|
5335
|
+
return loadSpecs(join29(assetRoot(), "rules")).length;
|
|
5170
5336
|
} catch {
|
|
5171
5337
|
return 0;
|
|
5172
5338
|
}
|
|
5173
5339
|
}
|
|
5174
5340
|
function check(opts) {
|
|
5175
5341
|
const indexPath = resolveIndexPath(opts.projectRoot);
|
|
5176
|
-
const index = validateIndex(JSON.parse(
|
|
5342
|
+
const index = validateIndex(JSON.parse(readFileSync24(indexPath, "utf8")));
|
|
5177
5343
|
const selection = selectFiles(opts.projectRoot, opts.all);
|
|
5178
5344
|
const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
|
|
5179
5345
|
const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
|
|
@@ -5184,7 +5350,7 @@ function check(opts) {
|
|
|
5184
5350
|
const projectParticipates = stylesheets.some((f) => {
|
|
5185
5351
|
if (!hasExtension(f, CSS_EXTENSIONS)) return false;
|
|
5186
5352
|
try {
|
|
5187
|
-
return participatesInTokenLayer(
|
|
5353
|
+
return participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
|
|
5188
5354
|
} catch {
|
|
5189
5355
|
return false;
|
|
5190
5356
|
}
|
|
@@ -5192,7 +5358,7 @@ function check(opts) {
|
|
|
5192
5358
|
const projectResponsive = stylesheets.some((f) => {
|
|
5193
5359
|
if (!isStyleBearing(f)) return false;
|
|
5194
5360
|
try {
|
|
5195
|
-
const raw =
|
|
5361
|
+
const raw = readFileSync24(join29(opts.projectRoot, f), "utf8");
|
|
5196
5362
|
return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
|
|
5197
5363
|
} catch {
|
|
5198
5364
|
return false;
|
|
@@ -5201,22 +5367,33 @@ function check(opts) {
|
|
|
5201
5367
|
const viewportFitCover = stylesheets.some((f) => {
|
|
5202
5368
|
if (!isStyleBearing(f)) return false;
|
|
5203
5369
|
try {
|
|
5204
|
-
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(
|
|
5370
|
+
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync24(join29(opts.projectRoot, f), "utf8"));
|
|
5205
5371
|
} catch {
|
|
5206
5372
|
return false;
|
|
5207
5373
|
}
|
|
5208
5374
|
});
|
|
5209
5375
|
const projectMenuToggle = stylesheets.some((f) => {
|
|
5210
5376
|
try {
|
|
5211
|
-
return hasMenuToggle(
|
|
5377
|
+
return hasMenuToggle(readFileSync24(join29(opts.projectRoot, f), "utf8"));
|
|
5212
5378
|
} catch {
|
|
5213
5379
|
return false;
|
|
5214
5380
|
}
|
|
5215
5381
|
});
|
|
5216
5382
|
const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
|
|
5383
|
+
const touchFormText2 = { body: false, control: false };
|
|
5384
|
+
const vendoredTokens = existsSync18(join29(opts.projectRoot, ".jig", "tokens")) ? readdirSync11(join29(opts.projectRoot, ".jig", "tokens")).filter((f) => f.endsWith(".css")).map((f) => join29(".jig", "tokens", f)) : [];
|
|
5385
|
+
for (const f of [...stylesheets, ...vendoredTokens]) {
|
|
5386
|
+
if (!isStyleBearing(f)) continue;
|
|
5387
|
+
try {
|
|
5388
|
+
const found = touchFormText(maskComments(maskNonStyleRegions(readFileSync24(join29(opts.projectRoot, f), "utf8"), f)));
|
|
5389
|
+
touchFormText2.body ||= found.body;
|
|
5390
|
+
touchFormText2.control ||= found.control;
|
|
5391
|
+
} catch {
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5217
5394
|
const withStyles = files.filter((f) => {
|
|
5218
5395
|
try {
|
|
5219
|
-
const src =
|
|
5396
|
+
const src = readFileSync24(join29(opts.projectRoot, f), "utf8");
|
|
5220
5397
|
if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
|
|
5221
5398
|
if (maskNonStyleRegions(src, f).trim().length > 0) return true;
|
|
5222
5399
|
return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
|
|
@@ -5225,7 +5402,7 @@ function check(opts) {
|
|
|
5225
5402
|
}
|
|
5226
5403
|
}).length;
|
|
5227
5404
|
const resolvedMode = resolveMode(opts.projectRoot);
|
|
5228
|
-
const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties);
|
|
5405
|
+
const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText2);
|
|
5229
5406
|
const TOKEN_RULE = {
|
|
5230
5407
|
"--text-prose": "B-75",
|
|
5231
5408
|
"--size-touch-target": "E-51"
|
|
@@ -5246,6 +5423,19 @@ function check(opts) {
|
|
|
5246
5423
|
for (const gap of brandDecidedGaps(opts.projectRoot)) {
|
|
5247
5424
|
detected.push({ ruleId: "A-01", detector: "brand-decided", bucket: "mechanical", severity: "warning", file: gap.file, line: gap.line, message: gap.message });
|
|
5248
5425
|
}
|
|
5426
|
+
const tailwind = deadTailwindClasses(opts.projectRoot, files, stylesheets);
|
|
5427
|
+
const h117 = index.find((e) => e.id === "H-117");
|
|
5428
|
+
for (const d of tailwind.dead) {
|
|
5429
|
+
detected.push({
|
|
5430
|
+
ruleId: "H-117",
|
|
5431
|
+
detector: "tailwind-dead-class",
|
|
5432
|
+
bucket: h117?.bucket ?? "mechanical",
|
|
5433
|
+
severity: h117?.severity ?? "error",
|
|
5434
|
+
file: d.file,
|
|
5435
|
+
line: d.line,
|
|
5436
|
+
message: `\`${d.className}\` generates no CSS: Tailwind reads it as a utility, and nothing in this project's theme gives it a value, so the element gets nothing. Declare the token it needs in @theme, or use a class that exists`
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5249
5439
|
const { findings, waived } = applyWaivers(opts.projectRoot, detected);
|
|
5250
5440
|
const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
|
|
5251
5441
|
const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
|
|
@@ -5254,7 +5444,7 @@ function check(opts) {
|
|
|
5254
5444
|
// told to add a token @import it has nowhere to put.
|
|
5255
5445
|
cssFiles.length > 0 && cssFiles.every((f) => {
|
|
5256
5446
|
try {
|
|
5257
|
-
return !participatesInTokenLayer(
|
|
5447
|
+
return !participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
|
|
5258
5448
|
} catch {
|
|
5259
5449
|
return true;
|
|
5260
5450
|
}
|
|
@@ -5277,6 +5467,7 @@ function check(opts) {
|
|
|
5277
5467
|
exemptIgnored,
|
|
5278
5468
|
scope: selection.mode,
|
|
5279
5469
|
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
5470
|
+
tailwindSkipped: tailwind.skipped,
|
|
5280
5471
|
agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
|
|
5281
5472
|
waived
|
|
5282
5473
|
});
|
|
@@ -5284,12 +5475,12 @@ function check(opts) {
|
|
|
5284
5475
|
}
|
|
5285
5476
|
|
|
5286
5477
|
// src/commands/init.ts
|
|
5287
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as
|
|
5288
|
-
import { dirname as
|
|
5478
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync12, readFileSync as readFileSync28, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5479
|
+
import { dirname as dirname8, isAbsolute, join as join33, resolve as resolve2, sep as sep3 } from "path";
|
|
5289
5480
|
|
|
5290
5481
|
// src/init/detect.ts
|
|
5291
|
-
import { readFileSync as
|
|
5292
|
-
import { join as
|
|
5482
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
5483
|
+
import { join as join30 } from "path";
|
|
5293
5484
|
var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
|
|
5294
5485
|
var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
|
|
5295
5486
|
var TAILWIND_V4_THEME_RE = /@theme\b/;
|
|
@@ -5306,7 +5497,7 @@ var FRAMEWORK_DEPS = [
|
|
|
5306
5497
|
function detectFramework(projectRoot) {
|
|
5307
5498
|
let pkg;
|
|
5308
5499
|
try {
|
|
5309
|
-
pkg = JSON.parse(
|
|
5500
|
+
pkg = JSON.parse(readFileSync25(join30(projectRoot, "package.json"), "utf8"));
|
|
5310
5501
|
} catch {
|
|
5311
5502
|
return void 0;
|
|
5312
5503
|
}
|
|
@@ -5324,7 +5515,7 @@ function detect(projectRoot) {
|
|
|
5324
5515
|
for (const f of cssFiles) {
|
|
5325
5516
|
let source;
|
|
5326
5517
|
try {
|
|
5327
|
-
source =
|
|
5518
|
+
source = readFileSync25(join30(projectRoot, f), "utf8");
|
|
5328
5519
|
} catch {
|
|
5329
5520
|
continue;
|
|
5330
5521
|
}
|
|
@@ -5342,12 +5533,12 @@ function detect(projectRoot) {
|
|
|
5342
5533
|
}
|
|
5343
5534
|
|
|
5344
5535
|
// src/init/gitignore.ts
|
|
5345
|
-
import { execFileSync as
|
|
5536
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
5346
5537
|
function isIgnored(projectRoot, relPath) {
|
|
5347
5538
|
const candidates = relPath.endsWith("/") ? [relPath] : [relPath, `${relPath}/`];
|
|
5348
5539
|
return candidates.some((path) => {
|
|
5349
5540
|
try {
|
|
5350
|
-
|
|
5541
|
+
execFileSync3("git", ["check-ignore", "-q", "--", path], {
|
|
5351
5542
|
cwd: projectRoot,
|
|
5352
5543
|
stdio: ["ignore", "ignore", "ignore"]
|
|
5353
5544
|
});
|
|
@@ -5359,8 +5550,8 @@ function isIgnored(projectRoot, relPath) {
|
|
|
5359
5550
|
}
|
|
5360
5551
|
|
|
5361
5552
|
// src/init/derive.ts
|
|
5362
|
-
import { readFileSync as
|
|
5363
|
-
import { join as
|
|
5553
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
5554
|
+
import { join as join31 } from "path";
|
|
5364
5555
|
var DEFAULT_PROPOSAL = {
|
|
5365
5556
|
h: 264,
|
|
5366
5557
|
s: 0,
|
|
@@ -5393,7 +5584,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
|
|
|
5393
5584
|
for (const file of cssFiles) {
|
|
5394
5585
|
let source;
|
|
5395
5586
|
try {
|
|
5396
|
-
source =
|
|
5587
|
+
source = readFileSync26(join31(projectRoot, file), "utf8");
|
|
5397
5588
|
} catch {
|
|
5398
5589
|
continue;
|
|
5399
5590
|
}
|
|
@@ -5442,7 +5633,7 @@ function balancedBraceBody(source, openIdx) {
|
|
|
5442
5633
|
function fromTailwindConfig(projectRoot, tailwindConfigFile) {
|
|
5443
5634
|
let source;
|
|
5444
5635
|
try {
|
|
5445
|
-
source =
|
|
5636
|
+
source = readFileSync26(join31(projectRoot, tailwindConfigFile), "utf8");
|
|
5446
5637
|
} catch {
|
|
5447
5638
|
return null;
|
|
5448
5639
|
}
|
|
@@ -5498,7 +5689,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
|
|
|
5498
5689
|
for (const file of cssFiles) {
|
|
5499
5690
|
let source;
|
|
5500
5691
|
try {
|
|
5501
|
-
source =
|
|
5692
|
+
source = readFileSync26(join31(projectRoot, file), "utf8");
|
|
5502
5693
|
} catch {
|
|
5503
5694
|
continue;
|
|
5504
5695
|
}
|
|
@@ -5658,8 +5849,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
|
|
|
5658
5849
|
}
|
|
5659
5850
|
|
|
5660
5851
|
// src/init/project-name.ts
|
|
5661
|
-
import { readFileSync as
|
|
5662
|
-
import { basename as basename2, join as
|
|
5852
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
5853
|
+
import { basename as basename2, join as join32 } from "path";
|
|
5663
5854
|
function slugify(name) {
|
|
5664
5855
|
const withoutScope = name.replace(/^@[^/]+\//, "");
|
|
5665
5856
|
const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -5667,7 +5858,7 @@ function slugify(name) {
|
|
|
5667
5858
|
}
|
|
5668
5859
|
function deriveProjectSlug(projectRoot) {
|
|
5669
5860
|
try {
|
|
5670
|
-
const pkg = JSON.parse(
|
|
5861
|
+
const pkg = JSON.parse(readFileSync27(join32(projectRoot, "package.json"), "utf8"));
|
|
5671
5862
|
if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
|
|
5672
5863
|
} catch {
|
|
5673
5864
|
}
|
|
@@ -5676,6 +5867,8 @@ function deriveProjectSlug(projectRoot) {
|
|
|
5676
5867
|
|
|
5677
5868
|
// src/init/utilities.ts
|
|
5678
5869
|
var TAILWIND_NAMESPACES = [
|
|
5870
|
+
"--opacity-",
|
|
5871
|
+
"--transition-duration-",
|
|
5679
5872
|
"--color-",
|
|
5680
5873
|
"--font-",
|
|
5681
5874
|
"--text-",
|
|
@@ -5697,21 +5890,60 @@ var TAILWIND_NAMESPACES = [
|
|
|
5697
5890
|
"--size-",
|
|
5698
5891
|
"--border-width-"
|
|
5699
5892
|
];
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5893
|
+
var NOT_UTILITIES = /* @__PURE__ */ new Set(["--shadow-none"]);
|
|
5894
|
+
var RENAMES = [["--duration-", "--transition-duration-"]];
|
|
5895
|
+
function aliasCandidates(names) {
|
|
5896
|
+
const out = /* @__PURE__ */ new Map();
|
|
5897
|
+
for (const token of new Set(names)) {
|
|
5898
|
+
out.set(token, { alias: token, token });
|
|
5899
|
+
for (const [from, to] of RENAMES) if (token.startsWith(from)) {
|
|
5900
|
+
const alias = to + token.slice(from.length);
|
|
5901
|
+
out.set(alias, { alias, token });
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
return [...out.values()].sort((a, b) => a.alias.localeCompare(b.alias));
|
|
5905
|
+
}
|
|
5906
|
+
function listedAliases(names) {
|
|
5907
|
+
return aliasCandidates(names).filter((a) => TAILWIND_NAMESPACES.some((ns) => a.alias.startsWith(ns)) && !NOT_UTILITIES.has(a.alias));
|
|
5908
|
+
}
|
|
5909
|
+
var ALIASES_QUERY = `
|
|
5910
|
+
const ds = await loadDesignSystem('@import "tailwindcss";\\n@theme inline {\\n' + input.aliases.map((a, i) => ' ' + a + ': var(--jig-alias-' + i + ');').join('\\n') + '\\n}', { base: input.base });
|
|
5911
|
+
const used = new Set();
|
|
5912
|
+
const note = (list) => { for (const css of list) if (css) for (const m of css.matchAll(/var\\(--jig-alias-(\\d+)/g)) used.add(Number(m[1])); };
|
|
5913
|
+
note(ds.candidatesToCss(ds.getClassList().map(([c]) => c)));
|
|
5914
|
+
const roots = ds.utilities.keys('functional');
|
|
5915
|
+
const tries = [];
|
|
5916
|
+
input.aliases.forEach((a, i) => {
|
|
5917
|
+
if (used.has(i)) return;
|
|
5918
|
+
const parts = a.slice(2).split('-');
|
|
5919
|
+
for (let k = 1; k < parts.length; k++) for (const r of roots) tries.push(r + '-' + parts.slice(k).join('-'));
|
|
5920
|
+
});
|
|
5921
|
+
note(ds.candidatesToCss(tries));
|
|
5922
|
+
answer(input.aliases.filter((_, i) => used.has(i)));
|
|
5923
|
+
`;
|
|
5924
|
+
function compiledAliases(projectRoot, names) {
|
|
5925
|
+
const module = tailwindNode(projectRoot);
|
|
5926
|
+
if (!module) return void 0;
|
|
5927
|
+
const candidates = aliasCandidates(names);
|
|
5928
|
+
try {
|
|
5929
|
+
const kept = new Set(askTailwind(projectRoot, module, ALIASES_QUERY, { base: projectRoot, aliases: candidates.map((a) => a.alias) }));
|
|
5930
|
+
return candidates.filter((a) => kept.has(a.alias));
|
|
5931
|
+
} catch {
|
|
5932
|
+
return void 0;
|
|
5933
|
+
}
|
|
5703
5934
|
}
|
|
5704
5935
|
function declaredTokenNames(css) {
|
|
5705
5936
|
return [...css.matchAll(/(^|[;{\s])(--[\w-]+)\s*:/g)].map((m) => m[2]);
|
|
5706
5937
|
}
|
|
5707
|
-
function utilitiesBody(
|
|
5708
|
-
|
|
5938
|
+
function utilitiesBody(entries, version2) {
|
|
5939
|
+
const aliases = entries.map((e) => typeof e === "string" ? { alias: e, token: e } : e);
|
|
5940
|
+
if (aliases.length === 0) {
|
|
5709
5941
|
throw new Error(
|
|
5710
5942
|
"Refusing to write an empty @theme block: it generates no utilities while looking like it should. No token in this layer matches a Tailwind namespace."
|
|
5711
5943
|
);
|
|
5712
5944
|
}
|
|
5713
|
-
const width = Math.max(...
|
|
5714
|
-
const lines =
|
|
5945
|
+
const width = Math.max(...aliases.map((a) => a.alias.length));
|
|
5946
|
+
const lines = aliases.map((a) => ` ${a.alias}:${" ".repeat(width - a.alias.length)} var(${a.token});`);
|
|
5715
5947
|
return `/* utilities.css \u2014 vendored from Jig v${version2}.
|
|
5716
5948
|
Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill.
|
|
5717
5949
|
|
|
@@ -5735,10 +5967,11 @@ function utilitiesBody(names, version2) {
|
|
|
5735
5967
|
order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
|
|
5736
5968
|
removes the utility, deleting Jig's removes the value.
|
|
5737
5969
|
|
|
5738
|
-
Only tokens
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
\`
|
|
5970
|
+
Only tokens a Tailwind utility reads are listed. Durations are listed under the
|
|
5971
|
+
name Tailwind reads them by, \`--transition-duration-fast: var(--duration-fast)\`,
|
|
5972
|
+
so \`duration-fast\` works. \`--measure-*\` and \`--focus-ring-*\` have no utility,
|
|
5973
|
+
so they stay \`var()\`-only \u2014 read them from a class with Tailwind's
|
|
5974
|
+
custom-property form instead: \`max-w-(--measure-prose)\`.
|
|
5742
5975
|
|
|
5743
5976
|
Regenerate with \`jig init\` after the token layer changes; a token missing here
|
|
5744
5977
|
is a class that renders and matches nothing. */
|
|
@@ -5821,9 +6054,9 @@ function isValidSurfaceArray(v) {
|
|
|
5821
6054
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
5822
6055
|
if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
5823
6056
|
try {
|
|
5824
|
-
const parsed = JSON.parse(
|
|
6057
|
+
const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
5825
6058
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
5826
|
-
const brandResolves = brandCandidate ? existsSync19(
|
|
6059
|
+
const brandResolves = brandCandidate ? existsSync19(join33(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
5827
6060
|
return {
|
|
5828
6061
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
5829
6062
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -5836,7 +6069,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
|
|
|
5836
6069
|
if (!existsSync19(configAbsPath)) return { path: null };
|
|
5837
6070
|
let declared2;
|
|
5838
6071
|
try {
|
|
5839
|
-
declared2 = JSON.parse(
|
|
6072
|
+
declared2 = JSON.parse(readFileSync28(configAbsPath, "utf8")).brand;
|
|
5840
6073
|
} catch {
|
|
5841
6074
|
return { path: null };
|
|
5842
6075
|
}
|
|
@@ -5866,7 +6099,7 @@ function defaultTokenDir(detection) {
|
|
|
5866
6099
|
function readConfiguredSurfaces(configAbsPath) {
|
|
5867
6100
|
if (!existsSync19(configAbsPath)) return null;
|
|
5868
6101
|
try {
|
|
5869
|
-
const parsed = JSON.parse(
|
|
6102
|
+
const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
5870
6103
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
5871
6104
|
} catch {
|
|
5872
6105
|
return null;
|
|
@@ -5876,9 +6109,9 @@ function isCssModule(path) {
|
|
|
5876
6109
|
return /\.module\.(css|scss)$/i.test(path);
|
|
5877
6110
|
}
|
|
5878
6111
|
function legacyBrandFile(projectRoot) {
|
|
5879
|
-
const dir =
|
|
6112
|
+
const dir = join33(projectRoot, ".jig", "tokens");
|
|
5880
6113
|
if (!existsSync19(dir)) return void 0;
|
|
5881
|
-
const found =
|
|
6114
|
+
const found = readdirSync12(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
5882
6115
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
5883
6116
|
}
|
|
5884
6117
|
function isTokenLayerFile(f) {
|
|
@@ -5908,7 +6141,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
5908
6141
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
5909
6142
|
);
|
|
5910
6143
|
function wireBarrel(absPath, barrelImport) {
|
|
5911
|
-
const content = existsSync19(absPath) ?
|
|
6144
|
+
const content = existsSync19(absPath) ? readFileSync28(absPath, "utf8") : "";
|
|
5912
6145
|
const line = `@import "${barrelImport}";`;
|
|
5913
6146
|
if (content.includes(line)) return { status: "already-present" };
|
|
5914
6147
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -5924,7 +6157,7 @@ function wireBarrel(absPath, barrelImport) {
|
|
|
5924
6157
|
|
|
5925
6158
|
` : `${line}
|
|
5926
6159
|
`);
|
|
5927
|
-
mkdirSync6(
|
|
6160
|
+
mkdirSync6(dirname8(absPath), { recursive: true });
|
|
5928
6161
|
writeFileSync4(next === content ? absPath : absPath, next, "utf8");
|
|
5929
6162
|
return { status: "wired" };
|
|
5930
6163
|
}
|
|
@@ -6003,7 +6236,7 @@ async function init(opts) {
|
|
|
6003
6236
|
else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
|
|
6004
6237
|
}
|
|
6005
6238
|
if (opts.yes) {
|
|
6006
|
-
const declared2 = readConfiguredSurfaces(
|
|
6239
|
+
const declared2 = readConfiguredSurfaces(join33(opts.projectRoot, "jig.config.json"));
|
|
6007
6240
|
const inEffect = declared2 ?? surfaces;
|
|
6008
6241
|
const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
|
|
6009
6242
|
log(
|
|
@@ -6022,7 +6255,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
|
|
|
6022
6255
|
const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
|
|
6023
6256
|
const projectSlug = deriveProjectSlug(opts.projectRoot);
|
|
6024
6257
|
const configRelPath = "jig.config.json";
|
|
6025
|
-
const configAbsPath =
|
|
6258
|
+
const configAbsPath = join33(opts.projectRoot, configRelPath);
|
|
6026
6259
|
const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
|
|
6027
6260
|
if (configuredBrand.refused) {
|
|
6028
6261
|
log(
|
|
@@ -6056,7 +6289,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
|
|
|
6056
6289
|
}
|
|
6057
6290
|
}
|
|
6058
6291
|
const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
|
|
6059
|
-
const brandAbsPath =
|
|
6292
|
+
const brandAbsPath = join33(opts.projectRoot, ...brandRelPath.split("/"));
|
|
6060
6293
|
const tokensRelDir = brandRelPath.split("/").slice(0, -1);
|
|
6061
6294
|
log(
|
|
6062
6295
|
`Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
|
|
@@ -6089,15 +6322,15 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6089
6322
|
if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
|
|
6090
6323
|
}
|
|
6091
6324
|
if (brandAction === "written") {
|
|
6092
|
-
const defaultCssRaw =
|
|
6325
|
+
const defaultCssRaw = readFileSync28(join33(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
6093
6326
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
6094
|
-
mkdirSync6(
|
|
6327
|
+
mkdirSync6(dirname8(brandAbsPath), { recursive: true });
|
|
6095
6328
|
writeFileSync4(brandAbsPath, content, "utf8");
|
|
6096
6329
|
files[brandRelPath] = checksum(content);
|
|
6097
6330
|
}
|
|
6098
6331
|
if (relocateTo && configAction !== "written") {
|
|
6099
6332
|
try {
|
|
6100
|
-
const existing = JSON.parse(
|
|
6333
|
+
const existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
6101
6334
|
const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
|
|
6102
6335
|
`;
|
|
6103
6336
|
writeFileSync4(configAbsPath, content, "utf8");
|
|
@@ -6116,12 +6349,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6116
6349
|
} else if (configAction === "merged") {
|
|
6117
6350
|
let existing = {};
|
|
6118
6351
|
try {
|
|
6119
|
-
existing = JSON.parse(
|
|
6352
|
+
existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
6120
6353
|
} catch {
|
|
6121
6354
|
existing = {};
|
|
6122
6355
|
}
|
|
6123
6356
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
6124
|
-
const brandStillResolves = existingBrand ? existsSync19(
|
|
6357
|
+
const brandStillResolves = existingBrand ? existsSync19(join33(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
6125
6358
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
6126
6359
|
const merged = {
|
|
6127
6360
|
...existing,
|
|
@@ -6138,12 +6371,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6138
6371
|
}
|
|
6139
6372
|
const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
|
|
6140
6373
|
const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
|
|
6141
|
-
const wiringBrandAbsPath =
|
|
6374
|
+
const wiringBrandAbsPath = join33(opts.projectRoot, ...effectiveConfig.brand.split("/"));
|
|
6142
6375
|
const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
|
|
6143
6376
|
const modeAbsPaths = {};
|
|
6144
6377
|
for (const mode of declaredModes) {
|
|
6145
6378
|
const modeFileName = `mode.${mode}.css`;
|
|
6146
|
-
const modeAbsPath2 =
|
|
6379
|
+
const modeAbsPath2 = join33(opts.projectRoot, ...tokensRelDir, modeFileName);
|
|
6147
6380
|
modeAbsPaths[mode] = modeAbsPath2;
|
|
6148
6381
|
const modeRelPath = relKey(...tokensRelDir, modeFileName);
|
|
6149
6382
|
const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
|
|
@@ -6151,9 +6384,9 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6151
6384
|
if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
|
|
6152
6385
|
else if (modeState.modified) modeAction = "skipped-edited";
|
|
6153
6386
|
if (modeAction === "written") {
|
|
6154
|
-
const modeSourceRaw =
|
|
6387
|
+
const modeSourceRaw = readFileSync28(join33(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
6155
6388
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
6156
|
-
mkdirSync6(
|
|
6389
|
+
mkdirSync6(dirname8(modeAbsPath2), { recursive: true });
|
|
6157
6390
|
writeFileSync4(modeAbsPath2, content, "utf8");
|
|
6158
6391
|
files[modeRelPath] = checksum(content);
|
|
6159
6392
|
} else {
|
|
@@ -6166,20 +6399,20 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6166
6399
|
const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
|
|
6167
6400
|
for (const mode of declaredModes) {
|
|
6168
6401
|
const rel = relKey(...tokensRelDir, barrelFor(mode));
|
|
6169
|
-
const abs =
|
|
6402
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
|
|
6170
6403
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6171
6404
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
6172
6405
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
6173
6406
|
continue;
|
|
6174
6407
|
}
|
|
6175
6408
|
const content = barrelBody(barrelFor(mode), brandFileOnly, mode, opts.version);
|
|
6176
|
-
mkdirSync6(
|
|
6409
|
+
mkdirSync6(dirname8(abs), { recursive: true });
|
|
6177
6410
|
writeFileSync4(abs, content, "utf8");
|
|
6178
6411
|
files[rel] = checksum(content);
|
|
6179
6412
|
}
|
|
6180
6413
|
if (multiMode) {
|
|
6181
6414
|
const rel = relKey(...tokensRelDir, "theme.css");
|
|
6182
|
-
const abs =
|
|
6415
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
6183
6416
|
if (existsSync19(abs)) {
|
|
6184
6417
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6185
6418
|
if (state.tracked && !state.modified) {
|
|
@@ -6195,13 +6428,13 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6195
6428
|
const declared2 = declaredTokenNames(
|
|
6196
6429
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
6197
6430
|
try {
|
|
6198
|
-
return
|
|
6431
|
+
return readFileSync28(f, "utf8");
|
|
6199
6432
|
} catch {
|
|
6200
6433
|
return "";
|
|
6201
6434
|
}
|
|
6202
6435
|
}).join("\n")
|
|
6203
6436
|
);
|
|
6204
|
-
const aliasable =
|
|
6437
|
+
const aliasable = compiledAliases(opts.projectRoot, declared2) ?? listedAliases(declared2);
|
|
6205
6438
|
const rel = relKey(...tokensRelDir, "utilities.css");
|
|
6206
6439
|
if (aliasable.length === 0) {
|
|
6207
6440
|
log(`
|
|
@@ -6220,19 +6453,19 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6220
6453
|
log(` This changes how components are written. Both styles are correct.`);
|
|
6221
6454
|
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
6222
6455
|
if (answer === "y" || answer === "yes") {
|
|
6223
|
-
const abs =
|
|
6456
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
6224
6457
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6225
6458
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
6226
6459
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
6227
6460
|
} else {
|
|
6228
6461
|
const content = utilitiesBody(aliasable, opts.version);
|
|
6229
|
-
mkdirSync6(
|
|
6462
|
+
mkdirSync6(dirname8(abs), { recursive: true });
|
|
6230
6463
|
writeFileSync4(abs, content, "utf8");
|
|
6231
6464
|
files[rel] = checksum(content);
|
|
6232
6465
|
const target = findWireTarget(detection);
|
|
6233
6466
|
const importPath = target ? relativeImportPath(
|
|
6234
|
-
|
|
6235
|
-
|
|
6467
|
+
dirname8(join33(opts.projectRoot, target)),
|
|
6468
|
+
join33(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
6236
6469
|
) : `./${rel}`;
|
|
6237
6470
|
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
6238
6471
|
log(` @import "tailwindcss";`);
|
|
@@ -6251,11 +6484,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6251
6484
|
writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
|
|
6252
6485
|
if (relocateTo && currentBrand) {
|
|
6253
6486
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
6254
|
-
const oldAbsDir =
|
|
6487
|
+
const oldAbsDir = join33(opts.projectRoot, ...oldDir);
|
|
6255
6488
|
if (existsSync19(oldAbsDir)) {
|
|
6256
|
-
for (const name of
|
|
6489
|
+
for (const name of readdirSync12(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
6257
6490
|
const rel = relKey(...oldDir, name);
|
|
6258
|
-
const abs =
|
|
6491
|
+
const abs = join33(oldAbsDir, name);
|
|
6259
6492
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6260
6493
|
if (!state.tracked || state.modified) {
|
|
6261
6494
|
log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
|
|
@@ -6269,14 +6502,14 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6269
6502
|
const wireTarget = findWireTarget(detection);
|
|
6270
6503
|
let wiring;
|
|
6271
6504
|
if (multiMode) {
|
|
6272
|
-
const bare2 =
|
|
6505
|
+
const bare2 = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
6273
6506
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
6274
|
-
const abs =
|
|
6507
|
+
const abs = join33(opts.projectRoot, file);
|
|
6275
6508
|
try {
|
|
6276
|
-
const before =
|
|
6509
|
+
const before = readFileSync28(abs, "utf8");
|
|
6277
6510
|
const after = before.split("\n").filter((l) => {
|
|
6278
6511
|
const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
|
|
6279
|
-
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(
|
|
6512
|
+
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname8(abs), m[1]) === bare2);
|
|
6280
6513
|
}).join("\n");
|
|
6281
6514
|
if (after !== before) {
|
|
6282
6515
|
writeFileSync4(abs, after, "utf8");
|
|
@@ -6295,21 +6528,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
|
|
|
6295
6528
|
snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
|
|
6296
6529
|
};
|
|
6297
6530
|
} else if (wireTarget) {
|
|
6298
|
-
const targetAbsDir =
|
|
6531
|
+
const targetAbsDir = dirname8(join33(opts.projectRoot, wireTarget));
|
|
6299
6532
|
const barrelImport = relativeImportPath(
|
|
6300
6533
|
targetAbsDir,
|
|
6301
|
-
|
|
6534
|
+
join33(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
6302
6535
|
);
|
|
6303
6536
|
const snippet = `@import "${barrelImport}";`;
|
|
6304
6537
|
try {
|
|
6305
6538
|
if (relocateTo && currentBrand) {
|
|
6306
|
-
const abs =
|
|
6539
|
+
const abs = join33(opts.projectRoot, wireTarget);
|
|
6307
6540
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
6308
|
-
const before = existsSync19(abs) ?
|
|
6541
|
+
const before = existsSync19(abs) ? readFileSync28(abs, "utf8") : "";
|
|
6309
6542
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
6310
6543
|
if (after !== before) writeFileSync4(abs, after, "utf8");
|
|
6311
6544
|
}
|
|
6312
|
-
const outcome = wireBarrel(
|
|
6545
|
+
const outcome = wireBarrel(join33(opts.projectRoot, wireTarget), barrelImport);
|
|
6313
6546
|
wiring = { target: wireTarget, status: outcome.status, snippet };
|
|
6314
6547
|
const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
|
|
6315
6548
|
log(`
|
|
@@ -6326,7 +6559,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6326
6559
|
} else {
|
|
6327
6560
|
const barrelImport = relativeImportPath(
|
|
6328
6561
|
opts.projectRoot,
|
|
6329
|
-
|
|
6562
|
+
join33(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
6330
6563
|
);
|
|
6331
6564
|
const snippet = `@import "${barrelImport}";`;
|
|
6332
6565
|
wiring = { target: null, status: "print-only", snippet };
|
|
@@ -6335,7 +6568,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6335
6568
|
if (pasteTargets.length > 0) {
|
|
6336
6569
|
log("Paste the block for whichever of these is your global stylesheet:");
|
|
6337
6570
|
for (const target of pasteTargets) {
|
|
6338
|
-
const dir =
|
|
6571
|
+
const dir = dirname8(join33(opts.projectRoot, target));
|
|
6339
6572
|
const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
|
|
6340
6573
|
@import "${relativeImportPath(dir, modeAbsPath)}";`;
|
|
6341
6574
|
log(`
|
|
@@ -6385,29 +6618,29 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6385
6618
|
}
|
|
6386
6619
|
|
|
6387
6620
|
// src/commands/verdicts.ts
|
|
6388
|
-
import { execFileSync as
|
|
6389
|
-
import { existsSync as existsSync25, readFileSync as
|
|
6390
|
-
import { join as
|
|
6621
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
6622
|
+
import { existsSync as existsSync25, readFileSync as readFileSync34 } from "fs";
|
|
6623
|
+
import { join as join41, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
|
|
6391
6624
|
|
|
6392
6625
|
// src/rules/citations.ts
|
|
6393
|
-
import { join as
|
|
6626
|
+
import { join as join34 } from "path";
|
|
6394
6627
|
function citableIds(packageRoot2) {
|
|
6395
|
-
const rulesDir =
|
|
6628
|
+
const rulesDir = join34(packageRoot2, "rules");
|
|
6396
6629
|
const ids = [
|
|
6397
|
-
...loadRules(rulesDir,
|
|
6630
|
+
...loadRules(rulesDir, join34(packageRoot2, "rules.index.json")).map((r) => r.id),
|
|
6398
6631
|
...loadSpecs(rulesDir).map((s) => s.id)
|
|
6399
6632
|
];
|
|
6400
6633
|
return [...new Set(ids)].sort();
|
|
6401
6634
|
}
|
|
6402
6635
|
|
|
6403
6636
|
// src/probe/check.ts
|
|
6404
|
-
import { existsSync as existsSync23, readdirSync as
|
|
6405
|
-
import { join as
|
|
6637
|
+
import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync32 } from "fs";
|
|
6638
|
+
import { join as join39 } from "path";
|
|
6406
6639
|
|
|
6407
6640
|
// src/probe/save.ts
|
|
6408
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as
|
|
6641
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync14, readFileSync as readFileSync31, writeFileSync as writeFileSync5 } from "fs";
|
|
6409
6642
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6410
|
-
import { join as
|
|
6643
|
+
import { join as join38, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
6411
6644
|
|
|
6412
6645
|
// src/probe/script.ts
|
|
6413
6646
|
var PROBE_VERSION = 9;
|
|
@@ -6692,9 +6925,9 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6692
6925
|
|
|
6693
6926
|
// src/probe/browser.ts
|
|
6694
6927
|
import { spawn } from "child_process";
|
|
6695
|
-
import { existsSync as existsSync20, mkdtempSync, readdirSync as
|
|
6928
|
+
import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync13, rmSync as rmSync3 } from "fs";
|
|
6696
6929
|
import { homedir as homedir2, tmpdir } from "os";
|
|
6697
|
-
import { join as
|
|
6930
|
+
import { join as join35 } from "path";
|
|
6698
6931
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
6699
6932
|
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
|
|
6700
6933
|
var CHROME_APPS = [
|
|
@@ -6706,20 +6939,20 @@ var CHROME_APPS = [
|
|
|
6706
6939
|
// Every Windows machine has Edge; Chrome is often installed per user.
|
|
6707
6940
|
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6708
6941
|
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6709
|
-
...process.env.LOCALAPPDATA ? [
|
|
6942
|
+
...process.env.LOCALAPPDATA ? [join35(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
|
|
6710
6943
|
];
|
|
6711
6944
|
function cachedBrowsers() {
|
|
6712
6945
|
const roots = [
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6946
|
+
join35(homedir2(), ".cache", "ms-playwright"),
|
|
6947
|
+
join35(homedir2(), "Library", "Caches", "ms-playwright"),
|
|
6948
|
+
join35(homedir2(), "AppData", "Local", "ms-playwright"),
|
|
6949
|
+
join35(homedir2(), ".cache", "puppeteer", "chrome")
|
|
6717
6950
|
];
|
|
6718
6951
|
const found = [];
|
|
6719
6952
|
for (const root of roots) {
|
|
6720
6953
|
let entries;
|
|
6721
6954
|
try {
|
|
6722
|
-
entries =
|
|
6955
|
+
entries = readdirSync13(root);
|
|
6723
6956
|
} catch {
|
|
6724
6957
|
continue;
|
|
6725
6958
|
}
|
|
@@ -6733,7 +6966,7 @@ function cachedBrowsers() {
|
|
|
6733
6966
|
["chrome-win", "chrome.exe"],
|
|
6734
6967
|
["chrome-win64", "chrome.exe"]
|
|
6735
6968
|
]) {
|
|
6736
|
-
const candidate =
|
|
6969
|
+
const candidate = join35(root, entry, ...rel);
|
|
6737
6970
|
if (existsSync20(candidate)) found.push(candidate);
|
|
6738
6971
|
}
|
|
6739
6972
|
}
|
|
@@ -6759,7 +6992,7 @@ function which(command) {
|
|
|
6759
6992
|
for (const dir of dirs) {
|
|
6760
6993
|
if (!dir) continue;
|
|
6761
6994
|
for (const ext of exts) {
|
|
6762
|
-
const candidate =
|
|
6995
|
+
const candidate = join35(dir, command + ext);
|
|
6763
6996
|
if (existsSync20(candidate)) return candidate;
|
|
6764
6997
|
}
|
|
6765
6998
|
}
|
|
@@ -6772,7 +7005,7 @@ async function runProbe(opts) {
|
|
|
6772
7005
|
"No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
|
|
6773
7006
|
);
|
|
6774
7007
|
}
|
|
6775
|
-
const profile = mkdtempSync(
|
|
7008
|
+
const profile = mkdtempSync(join35(tmpdir(), "jig-probe-"));
|
|
6776
7009
|
const child = spawn(chrome, [
|
|
6777
7010
|
"--headless=new",
|
|
6778
7011
|
"--remote-debugging-port=0",
|
|
@@ -6876,8 +7109,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
|
6876
7109
|
|
|
6877
7110
|
// src/probe/serve.ts
|
|
6878
7111
|
import { createServer } from "http";
|
|
6879
|
-
import { existsSync as existsSync21, readFileSync as
|
|
6880
|
-
import { extname, join as
|
|
7112
|
+
import { existsSync as existsSync21, readFileSync as readFileSync29, statSync as statSync3 } from "fs";
|
|
7113
|
+
import { extname, join as join36, resolve as resolve3, sep as sep4 } from "path";
|
|
6881
7114
|
var TYPES = {
|
|
6882
7115
|
".html": "text/html; charset=utf-8",
|
|
6883
7116
|
".htm": "text/html; charset=utf-8",
|
|
@@ -6904,19 +7137,19 @@ async function serveDirectory(dir) {
|
|
|
6904
7137
|
const root = resolve3(dir);
|
|
6905
7138
|
const server = createServer((req, res) => {
|
|
6906
7139
|
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6907
|
-
let file = resolve3(
|
|
7140
|
+
let file = resolve3(join36(root, path));
|
|
6908
7141
|
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6909
7142
|
res.writeHead(403).end();
|
|
6910
7143
|
return;
|
|
6911
7144
|
}
|
|
6912
|
-
if (existsSync21(file) && statSync3(file).isDirectory()) file =
|
|
7145
|
+
if (existsSync21(file) && statSync3(file).isDirectory()) file = join36(file, "index.html");
|
|
6913
7146
|
else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
|
|
6914
7147
|
if (!existsSync21(file)) {
|
|
6915
7148
|
res.writeHead(404).end();
|
|
6916
7149
|
return;
|
|
6917
7150
|
}
|
|
6918
7151
|
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6919
|
-
res.end(
|
|
7152
|
+
res.end(readFileSync29(file));
|
|
6920
7153
|
});
|
|
6921
7154
|
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6922
7155
|
const address = server.address();
|
|
@@ -6928,8 +7161,8 @@ async function serveDirectory(dir) {
|
|
|
6928
7161
|
}
|
|
6929
7162
|
|
|
6930
7163
|
// src/probe/switches.ts
|
|
6931
|
-
import { readFileSync as
|
|
6932
|
-
import { join as
|
|
7164
|
+
import { readFileSync as readFileSync30 } from "fs";
|
|
7165
|
+
import { join as join37 } from "path";
|
|
6933
7166
|
var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
|
|
6934
7167
|
function declaredSwitches(projectRoot) {
|
|
6935
7168
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6937,7 +7170,7 @@ function declaredSwitches(projectRoot) {
|
|
|
6937
7170
|
if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
|
|
6938
7171
|
let text;
|
|
6939
7172
|
try {
|
|
6940
|
-
text =
|
|
7173
|
+
text = readFileSync30(join37(projectRoot, file), "utf8");
|
|
6941
7174
|
} catch {
|
|
6942
7175
|
continue;
|
|
6943
7176
|
}
|
|
@@ -6972,11 +7205,11 @@ function saveProbe(opts) {
|
|
|
6972
7205
|
}
|
|
6973
7206
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
6974
7207
|
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
6975
|
-
probe.pageChecksum = checksum(
|
|
7208
|
+
probe.pageChecksum = checksum(readFileSync31(page, "utf8"));
|
|
6976
7209
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6977
|
-
const dir =
|
|
7210
|
+
const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6978
7211
|
mkdirSync7(dir, { recursive: true });
|
|
6979
|
-
const path =
|
|
7212
|
+
const path = join38(dir, `probe-${width}.json`);
|
|
6980
7213
|
writeFileSync5(path, JSON.stringify(probe), "utf8");
|
|
6981
7214
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
6982
7215
|
}
|
|
@@ -7025,11 +7258,11 @@ async function withPageUrl(projectRoot, abs, serve, use) {
|
|
|
7025
7258
|
}
|
|
7026
7259
|
}
|
|
7027
7260
|
function recordedPage(projectRoot, surface) {
|
|
7028
|
-
const dir =
|
|
7261
|
+
const dir = join38(projectRoot, ".jig", "critique", surface);
|
|
7029
7262
|
if (!existsSync22(dir)) return void 0;
|
|
7030
|
-
for (const f of
|
|
7263
|
+
for (const f of readdirSync14(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
|
|
7031
7264
|
try {
|
|
7032
|
-
const probe = JSON.parse(
|
|
7265
|
+
const probe = JSON.parse(readFileSync31(join38(dir, f), "utf8"));
|
|
7033
7266
|
if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
|
|
7034
7267
|
} catch {
|
|
7035
7268
|
}
|
|
@@ -7037,19 +7270,19 @@ function recordedPage(projectRoot, surface) {
|
|
|
7037
7270
|
return void 0;
|
|
7038
7271
|
}
|
|
7039
7272
|
async function ensureProbes(opts) {
|
|
7040
|
-
const dir =
|
|
7273
|
+
const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7041
7274
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
7042
7275
|
let current;
|
|
7043
7276
|
try {
|
|
7044
|
-
current = checksum(
|
|
7277
|
+
current = checksum(readFileSync31(abs, "utf8"));
|
|
7045
7278
|
} catch {
|
|
7046
7279
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
7047
7280
|
}
|
|
7048
|
-
const recorded = existsSync22(dir) ?
|
|
7281
|
+
const recorded = existsSync22(dir) ? readdirSync14(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
|
|
7049
7282
|
const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
|
|
7050
7283
|
const missing = widths.filter((width) => {
|
|
7051
7284
|
try {
|
|
7052
|
-
const probe = JSON.parse(
|
|
7285
|
+
const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
|
|
7053
7286
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
7054
7287
|
} catch {
|
|
7055
7288
|
return true;
|
|
@@ -7060,7 +7293,7 @@ async function ensureProbes(opts) {
|
|
|
7060
7293
|
let serve;
|
|
7061
7294
|
for (const width of widths) {
|
|
7062
7295
|
try {
|
|
7063
|
-
const probe = JSON.parse(
|
|
7296
|
+
const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
|
|
7064
7297
|
if (probe.serveRoot) {
|
|
7065
7298
|
serve = probe.serveRoot;
|
|
7066
7299
|
break;
|
|
@@ -7076,9 +7309,9 @@ async function ensureProbes(opts) {
|
|
|
7076
7309
|
return { recorded: missing };
|
|
7077
7310
|
}
|
|
7078
7311
|
function critiquedSurfaces(projectRoot) {
|
|
7079
|
-
const root =
|
|
7312
|
+
const root = join38(projectRoot, ".jig", "critique");
|
|
7080
7313
|
if (!existsSync22(root)) return [];
|
|
7081
|
-
return
|
|
7314
|
+
return readdirSync14(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join38(root, surface, "screen.json")) || existsSync22(join38(root, surface, "code.json"))));
|
|
7082
7315
|
}
|
|
7083
7316
|
|
|
7084
7317
|
// src/probe/check.ts
|
|
@@ -7090,7 +7323,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
7090
7323
|
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
7091
7324
|
let current;
|
|
7092
7325
|
try {
|
|
7093
|
-
current =
|
|
7326
|
+
current = readFileSync32(page, "utf8");
|
|
7094
7327
|
} catch {
|
|
7095
7328
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
7096
7329
|
}
|
|
@@ -7102,9 +7335,9 @@ function stampProblem(projectRoot, file, p) {
|
|
|
7102
7335
|
function readProbes(projectRoot, dir, errors) {
|
|
7103
7336
|
if (!existsSync23(dir)) return [];
|
|
7104
7337
|
const probes = [];
|
|
7105
|
-
for (const f of
|
|
7338
|
+
for (const f of readdirSync15(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
7106
7339
|
try {
|
|
7107
|
-
const p = JSON.parse(
|
|
7340
|
+
const p = JSON.parse(readFileSync32(join39(dir, f), "utf8"));
|
|
7108
7341
|
if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
|
|
7109
7342
|
errors.push(`${f} was taken by an older \`jig probe\` (version ${p.jigProbe}; this Jig reads version ${PROBE_VERSION}). Record it again with \`jig probe --run\`.`);
|
|
7110
7343
|
continue;
|
|
@@ -7228,19 +7461,19 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
|
|
|
7228
7461
|
}
|
|
7229
7462
|
|
|
7230
7463
|
// src/check/spec-shape.ts
|
|
7231
|
-
import { existsSync as existsSync24, readdirSync as
|
|
7232
|
-
import { join as
|
|
7464
|
+
import { existsSync as existsSync24, readdirSync as readdirSync16, statSync as statSync4 } from "fs";
|
|
7465
|
+
import { join as join40 } from "path";
|
|
7233
7466
|
function newestSpec(projectRoot) {
|
|
7234
|
-
const dir =
|
|
7467
|
+
const dir = join40(projectRoot, ".jig", "specs");
|
|
7235
7468
|
if (!existsSync24(dir)) return void 0;
|
|
7236
|
-
const files =
|
|
7469
|
+
const files = readdirSync16(dir).filter((f) => f.endsWith(".md"));
|
|
7237
7470
|
if (files.length === 0) return void 0;
|
|
7238
|
-
const newest = files.map((f) => ({ f, at: statSync4(
|
|
7239
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(
|
|
7471
|
+
const newest = files.map((f) => ({ f, at: statSync4(join40(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
7472
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join40(dir, newest)) };
|
|
7240
7473
|
}
|
|
7241
7474
|
function specFor(projectRoot, surface) {
|
|
7242
7475
|
if (surface) {
|
|
7243
|
-
const path =
|
|
7476
|
+
const path = join40(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7244
7477
|
if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
|
|
7245
7478
|
}
|
|
7246
7479
|
return newestSpec(projectRoot);
|
|
@@ -7398,14 +7631,14 @@ function reasonProblems(file, judged, errors) {
|
|
|
7398
7631
|
function readJson(path, errors) {
|
|
7399
7632
|
if (!existsSync25(path)) return null;
|
|
7400
7633
|
try {
|
|
7401
|
-
return JSON.parse(
|
|
7634
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
7402
7635
|
} catch (e) {
|
|
7403
7636
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
7404
7637
|
return {};
|
|
7405
7638
|
}
|
|
7406
7639
|
}
|
|
7407
7640
|
function specIndexable(projectRoot, surface) {
|
|
7408
|
-
const path =
|
|
7641
|
+
const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7409
7642
|
let front = "";
|
|
7410
7643
|
try {
|
|
7411
7644
|
front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
@@ -7413,11 +7646,20 @@ function specIndexable(projectRoot, surface) {
|
|
|
7413
7646
|
}
|
|
7414
7647
|
const declared2 = specIndexableField(front);
|
|
7415
7648
|
if (declared2 === true || declared2 === false) return declared2;
|
|
7416
|
-
const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
|
|
7649
|
+
const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase() ?? projectMode(projectRoot);
|
|
7417
7650
|
return mode !== "product" && mode !== "operator";
|
|
7418
7651
|
}
|
|
7652
|
+
function projectMode(projectRoot) {
|
|
7653
|
+
try {
|
|
7654
|
+
const config = JSON.parse(readText(join41(projectRoot, "jig.config.json")));
|
|
7655
|
+
const modes = [...new Set((config.surfaces ?? []).map((s) => s?.mode).filter((m) => typeof m === "string"))];
|
|
7656
|
+
return modes.length === 1 ? modes[0].toLowerCase() : void 0;
|
|
7657
|
+
} catch {
|
|
7658
|
+
return void 0;
|
|
7659
|
+
}
|
|
7660
|
+
}
|
|
7419
7661
|
function specNeedsNav(projectRoot, surface) {
|
|
7420
|
-
const path =
|
|
7662
|
+
const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7421
7663
|
if (!existsSync25(path)) return false;
|
|
7422
7664
|
const front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
7423
7665
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
@@ -7496,7 +7738,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
7496
7738
|
function checkDecisions(projectRoot, dir, errors) {
|
|
7497
7739
|
const required = decisionNames(projectRoot);
|
|
7498
7740
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
7499
|
-
const file = readJson(
|
|
7741
|
+
const file = readJson(join41(dir, "decisions.json"), errors);
|
|
7500
7742
|
if (!file) {
|
|
7501
7743
|
errors.push(
|
|
7502
7744
|
`decisions.json is missing. Every decision in DECISIONS.md is judged against the built page, one verdict each: ${required.slice(0, 4).join(", ")}${required.length > 4 ? `, and ${required.length - 4} more` : ""}. A page can satisfy every rule and still break what this project decided.`
|
|
@@ -7549,9 +7791,9 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7549
7791
|
if (unjudged.length === 0) return [];
|
|
7550
7792
|
const path = decisionsFile(projectRoot);
|
|
7551
7793
|
if (!path) return [];
|
|
7552
|
-
const git = (args) =>
|
|
7794
|
+
const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7553
7795
|
try {
|
|
7554
|
-
const verdictFile = relative4(projectRoot,
|
|
7796
|
+
const verdictFile = relative4(projectRoot, join41(dir, "decisions.json")).split(sep6).join("/");
|
|
7555
7797
|
if (git(["status", "--porcelain", "--", verdictFile])) return [];
|
|
7556
7798
|
const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
|
|
7557
7799
|
if (!judgedIn) return [];
|
|
@@ -7575,7 +7817,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7575
7817
|
function verifyVerdicts(opts) {
|
|
7576
7818
|
const root = opts.packageRoot ?? assetRoot();
|
|
7577
7819
|
const errors = [];
|
|
7578
|
-
const index = JSON.parse(
|
|
7820
|
+
const index = JSON.parse(readFileSync34(join41(root, "rules.index.json"), "utf8"));
|
|
7579
7821
|
const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
|
|
7580
7822
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
7581
7823
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -7584,9 +7826,9 @@ function verifyVerdicts(opts) {
|
|
|
7584
7826
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
7585
7827
|
]);
|
|
7586
7828
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
7587
|
-
const dir =
|
|
7588
|
-
const screenFile = readJson(
|
|
7589
|
-
const codeFile = readJson(
|
|
7829
|
+
const dir = join41(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7830
|
+
const screenFile = readJson(join41(dir, "screen.json"), errors);
|
|
7831
|
+
const codeFile = readJson(join41(dir, "code.json"), errors);
|
|
7590
7832
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
7591
7833
|
const decisionList = decisionNames(opts.projectRoot);
|
|
7592
7834
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
|
|
@@ -7616,13 +7858,13 @@ function verifyVerdicts(opts) {
|
|
|
7616
7858
|
const measured = [];
|
|
7617
7859
|
let specFront = "";
|
|
7618
7860
|
try {
|
|
7619
|
-
specFront = readText(
|
|
7861
|
+
specFront = readText(join41(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
7620
7862
|
} catch {
|
|
7621
7863
|
}
|
|
7622
7864
|
if (specIndexableField(specFront) === "unreadable") {
|
|
7623
7865
|
errors.push(`.jig/specs/${opts.surface}.spec.md: \`indexable:\` is neither true nor false, so this review cannot tell whether the page is meant to be found and does not guess. Write \`indexable: true\` or \`indexable: false\`, and put the reason in the spec's body.`);
|
|
7624
7866
|
} else {
|
|
7625
|
-
const mode = /^\s*mode\s*:\s*["']?(\w+)/im.exec(specFront)?.[1]?.toLowerCase();
|
|
7867
|
+
const mode = /^\s*mode\s*:\s*["']?(\w+)/im.exec(specFront)?.[1]?.toLowerCase() ?? projectMode(opts.projectRoot);
|
|
7626
7868
|
const probed = probeCheck(probes, verdictOf, specIndexable(opts.projectRoot, opts.surface), mode);
|
|
7627
7869
|
errors.push(...probed.contradictions);
|
|
7628
7870
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7642,7 +7884,7 @@ function verifyVerdicts(opts) {
|
|
|
7642
7884
|
return { ok: errors.length === 0, errors, screen, code, decisions, rendered, measured, line, ...previous ? { previous } : {} };
|
|
7643
7885
|
}
|
|
7644
7886
|
function previousFindings(projectRoot, dir) {
|
|
7645
|
-
const git = (args) =>
|
|
7887
|
+
const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7646
7888
|
const rel = relative4(projectRoot, dir).split("\\").join("/");
|
|
7647
7889
|
const files = ["screen.json", "code.json", "decisions.json"];
|
|
7648
7890
|
const paths = files.map((f) => `${rel}/${f}`);
|
|
@@ -7661,7 +7903,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
7661
7903
|
const out = /* @__PURE__ */ new Map();
|
|
7662
7904
|
let text;
|
|
7663
7905
|
try {
|
|
7664
|
-
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(
|
|
7906
|
+
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join41(dir, f));
|
|
7665
7907
|
} catch {
|
|
7666
7908
|
return out;
|
|
7667
7909
|
}
|
|
@@ -7743,17 +7985,17 @@ function sourceChanges(git, from, to) {
|
|
|
7743
7985
|
}
|
|
7744
7986
|
|
|
7745
7987
|
// src/commands/gate.ts
|
|
7746
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as
|
|
7988
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync18, readFileSync as readFileSync38, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7747
7989
|
import { createHash as createHash3 } from "crypto";
|
|
7748
|
-
import { join as
|
|
7990
|
+
import { join as join46 } from "path";
|
|
7749
7991
|
|
|
7750
7992
|
// src/check/mockup-drawing.ts
|
|
7751
|
-
import { join as
|
|
7993
|
+
import { join as join42 } from "path";
|
|
7752
7994
|
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
7753
7995
|
function mockupDrawingProblems(root, specBody, at) {
|
|
7754
7996
|
let html;
|
|
7755
7997
|
try {
|
|
7756
|
-
html = readText(
|
|
7998
|
+
html = readText(join42(root, at));
|
|
7757
7999
|
} catch {
|
|
7758
8000
|
return [];
|
|
7759
8001
|
}
|
|
@@ -7814,7 +8056,7 @@ function frameRegionProblems(frames, specBody, at) {
|
|
|
7814
8056
|
function approvedDrawingProblems(root, specBody, at) {
|
|
7815
8057
|
let html;
|
|
7816
8058
|
try {
|
|
7817
|
-
html = readText(
|
|
8059
|
+
html = readText(join42(root, at));
|
|
7818
8060
|
} catch {
|
|
7819
8061
|
return [];
|
|
7820
8062
|
}
|
|
@@ -7917,15 +8159,15 @@ function sizeBlock2(sizes, size) {
|
|
|
7917
8159
|
}
|
|
7918
8160
|
|
|
7919
8161
|
// src/commands/gate.ts
|
|
7920
|
-
import { execFileSync as
|
|
8162
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
7921
8163
|
|
|
7922
8164
|
// src/commands/ship.ts
|
|
7923
|
-
import { existsSync as existsSync26, readdirSync as
|
|
7924
|
-
import { join as
|
|
8165
|
+
import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync36 } from "fs";
|
|
8166
|
+
import { join as join44 } from "path";
|
|
7925
8167
|
|
|
7926
8168
|
// src/commands/seo.ts
|
|
7927
|
-
import { readFileSync as
|
|
7928
|
-
import { join as
|
|
8169
|
+
import { readFileSync as readFileSync35 } from "fs";
|
|
8170
|
+
import { join as join43 } from "path";
|
|
7929
8171
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
7930
8172
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
7931
8173
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -7966,7 +8208,7 @@ function seo(opts) {
|
|
|
7966
8208
|
for (const file of files) {
|
|
7967
8209
|
let raw;
|
|
7968
8210
|
try {
|
|
7969
|
-
raw =
|
|
8211
|
+
raw = readFileSync35(join43(root, file), "utf8");
|
|
7970
8212
|
} catch {
|
|
7971
8213
|
continue;
|
|
7972
8214
|
}
|
|
@@ -8031,7 +8273,7 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
8031
8273
|
const own = specCritique(specBody);
|
|
8032
8274
|
if (own) return own === "at-ship";
|
|
8033
8275
|
try {
|
|
8034
|
-
return JSON.parse(
|
|
8276
|
+
return JSON.parse(readFileSync36(join44(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
|
|
8035
8277
|
} catch {
|
|
8036
8278
|
return false;
|
|
8037
8279
|
}
|
|
@@ -8039,14 +8281,14 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
8039
8281
|
function tweakDeferred(dir) {
|
|
8040
8282
|
let t;
|
|
8041
8283
|
try {
|
|
8042
|
-
t = JSON.parse(
|
|
8284
|
+
t = JSON.parse(readFileSync36(join44(dir, "tweak.json"), "utf8"));
|
|
8043
8285
|
} catch {
|
|
8044
8286
|
return false;
|
|
8045
8287
|
}
|
|
8046
8288
|
if (t.deferred === void 0 || t.deferred === false) return false;
|
|
8047
8289
|
let judged;
|
|
8048
8290
|
try {
|
|
8049
|
-
judged = JSON.parse(
|
|
8291
|
+
judged = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).tweak;
|
|
8050
8292
|
} catch {
|
|
8051
8293
|
}
|
|
8052
8294
|
return judged !== t.at;
|
|
@@ -8057,14 +8299,14 @@ function pageChecksum(html) {
|
|
|
8057
8299
|
function changedSinceLock(projectRoot, dir) {
|
|
8058
8300
|
let lock;
|
|
8059
8301
|
try {
|
|
8060
|
-
lock = JSON.parse(
|
|
8302
|
+
lock = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8"));
|
|
8061
8303
|
} catch {
|
|
8062
8304
|
return void 0;
|
|
8063
8305
|
}
|
|
8064
8306
|
const file = lock.page?.file;
|
|
8065
8307
|
if (!file || !lock.page?.checksum) return void 0;
|
|
8066
8308
|
try {
|
|
8067
|
-
const html =
|
|
8309
|
+
const html = readFileSync36(join44(projectRoot, file), "utf8");
|
|
8068
8310
|
const seen = [pageChecksum(html), checksum(html)];
|
|
8069
8311
|
return seen.includes(lock.page.checksum) ? void 0 : file;
|
|
8070
8312
|
} catch {
|
|
@@ -8076,7 +8318,7 @@ function supersededBy(front) {
|
|
|
8076
8318
|
return named ? named : void 0;
|
|
8077
8319
|
}
|
|
8078
8320
|
function pageStatus(projectRoot, surface) {
|
|
8079
|
-
const spec = readText(
|
|
8321
|
+
const spec = readText(join44(projectRoot, ".jig", "specs", `${surface}.spec.md`));
|
|
8080
8322
|
const front = spec.split(/^---\s*$/m)[1] ?? "";
|
|
8081
8323
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
|
|
8082
8324
|
return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
|
|
@@ -8084,10 +8326,10 @@ function pageStatus(projectRoot, surface) {
|
|
|
8084
8326
|
const successor = supersededBy(front);
|
|
8085
8327
|
if (successor !== void 0) {
|
|
8086
8328
|
const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
|
|
8087
|
-
return existsSync26(
|
|
8329
|
+
return existsSync26(join44(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
|
|
8088
8330
|
}
|
|
8089
|
-
const dir =
|
|
8090
|
-
if (!existsSync26(
|
|
8331
|
+
const dir = join44(projectRoot, ".jig", "critique", surface);
|
|
8332
|
+
if (!existsSync26(join44(dir, "screen.json")) && !existsSync26(join44(dir, "code.json"))) {
|
|
8091
8333
|
return { surface, state: "never", detail: "never critiqued" };
|
|
8092
8334
|
}
|
|
8093
8335
|
if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
|
|
@@ -8097,7 +8339,7 @@ function pageStatus(projectRoot, surface) {
|
|
|
8097
8339
|
const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
|
|
8098
8340
|
let judgedPage = false;
|
|
8099
8341
|
try {
|
|
8100
|
-
judgedPage = !!JSON.parse(
|
|
8342
|
+
judgedPage = !!JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).page?.checksum;
|
|
8101
8343
|
} catch {
|
|
8102
8344
|
}
|
|
8103
8345
|
if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
|
|
@@ -8117,8 +8359,8 @@ function ship(opts) {
|
|
|
8117
8359
|
const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
8118
8360
|
const seoResult = seo({ projectRoot: root });
|
|
8119
8361
|
const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
|
|
8120
|
-
const specsDir =
|
|
8121
|
-
const surfaces = existsSync26(specsDir) ?
|
|
8362
|
+
const specsDir = join44(root, ".jig", "specs");
|
|
8363
|
+
const surfaces = existsSync26(specsDir) ? readdirSync17(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
|
|
8122
8364
|
const pages = surfaces.map((s) => pageStatus(root, s));
|
|
8123
8365
|
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
|
|
8124
8366
|
const agentFiles = agentFileProblems(root);
|
|
@@ -8139,9 +8381,9 @@ function ship(opts) {
|
|
|
8139
8381
|
}
|
|
8140
8382
|
|
|
8141
8383
|
// src/check/spec-checked.ts
|
|
8142
|
-
import { existsSync as existsSync27, readFileSync as
|
|
8384
|
+
import { existsSync as existsSync27, readFileSync as readFileSync37 } from "fs";
|
|
8143
8385
|
import { createHash as createHash2 } from "crypto";
|
|
8144
|
-
import { join as
|
|
8386
|
+
import { join as join45 } from "path";
|
|
8145
8387
|
|
|
8146
8388
|
// src/check/owner-word.ts
|
|
8147
8389
|
var SAYS = {
|
|
@@ -8226,7 +8468,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8226
8468
|
const recordPath = `.jig/specs/${spec.slug}.checked.json`;
|
|
8227
8469
|
let record;
|
|
8228
8470
|
try {
|
|
8229
|
-
record = JSON.parse(
|
|
8471
|
+
record = JSON.parse(readFileSync37(join45(root, recordPath), "utf8"));
|
|
8230
8472
|
} catch {
|
|
8231
8473
|
problems.push(
|
|
8232
8474
|
`${recordPath} is missing. Before the owner is asked to confirm, a reader who did not write the spec checks its quotations against the owner's words and its facts against their sources, and writes that record (step 3c). The owner checks the spec by the sheet it gives them.`
|
|
@@ -8255,10 +8497,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8255
8497
|
if (unsaid.length) problems.push(`${recordPath}: ${unsaid.length} unchecked ${unsaid.length === 1 ? "fact says" : "facts say"} nothing of why. Say in \`note\` why the source could not be opened; the owner opens it instead.`);
|
|
8256
8498
|
const missing = facts.map((f) => typeof f?.source === "string" ? f.source.trim() : "").filter((s) => s && !/^[a-z][a-z0-9+.-]*:\/\//i.test(s)).filter((s) => {
|
|
8257
8499
|
const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
|
|
8258
|
-
if (!file || !existsSync27(
|
|
8500
|
+
if (!file || !existsSync27(join45(root, file))) return true;
|
|
8259
8501
|
if (!line) return false;
|
|
8260
8502
|
try {
|
|
8261
|
-
return Number(line) >
|
|
8503
|
+
return Number(line) > readFileSync37(join45(root, file), "utf8").split("\n").length;
|
|
8262
8504
|
} catch {
|
|
8263
8505
|
return true;
|
|
8264
8506
|
}
|
|
@@ -8275,7 +8517,7 @@ function lastJigInvocation(transcriptPath) {
|
|
|
8275
8517
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8276
8518
|
let text;
|
|
8277
8519
|
try {
|
|
8278
|
-
text =
|
|
8520
|
+
text = readFileSync38(transcriptPath, "utf8");
|
|
8279
8521
|
} catch {
|
|
8280
8522
|
return void 0;
|
|
8281
8523
|
}
|
|
@@ -8303,7 +8545,7 @@ function ownerWords(transcriptPath) {
|
|
|
8303
8545
|
if (!transcriptPath || !existsSync28(transcriptPath)) return "";
|
|
8304
8546
|
const said = [];
|
|
8305
8547
|
try {
|
|
8306
|
-
for (const line of
|
|
8548
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8307
8549
|
if (!line.includes('"user"')) continue;
|
|
8308
8550
|
let entry;
|
|
8309
8551
|
try {
|
|
@@ -8325,7 +8567,7 @@ function ownerWords(transcriptPath) {
|
|
|
8325
8567
|
}
|
|
8326
8568
|
function namedSpec(root, surface) {
|
|
8327
8569
|
if (!surface) return specFor(root, void 0);
|
|
8328
|
-
return existsSync28(
|
|
8570
|
+
return existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
|
|
8329
8571
|
}
|
|
8330
8572
|
function isConfirmed(body) {
|
|
8331
8573
|
return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
|
|
@@ -8334,7 +8576,7 @@ function ownerReplyAfter(transcriptPath, asked) {
|
|
|
8334
8576
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8335
8577
|
const turns = [];
|
|
8336
8578
|
try {
|
|
8337
|
-
for (const line of
|
|
8579
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8338
8580
|
let entry;
|
|
8339
8581
|
try {
|
|
8340
8582
|
entry = JSON.parse(line);
|
|
@@ -8362,7 +8604,7 @@ function lastAssistantText(transcriptPath) {
|
|
|
8362
8604
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8363
8605
|
let found;
|
|
8364
8606
|
try {
|
|
8365
|
-
for (const line of
|
|
8607
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8366
8608
|
if (!line.includes('"assistant"')) continue;
|
|
8367
8609
|
let entry;
|
|
8368
8610
|
try {
|
|
@@ -8388,7 +8630,7 @@ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak
|
|
|
8388
8630
|
function sessionStart(transcriptPath) {
|
|
8389
8631
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8390
8632
|
try {
|
|
8391
|
-
for (const line of
|
|
8633
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8392
8634
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
8393
8635
|
if (m) {
|
|
8394
8636
|
const t = Date.parse(m[1]);
|
|
@@ -8403,29 +8645,29 @@ function verdictsMtime(dir) {
|
|
|
8403
8645
|
let newest = 0;
|
|
8404
8646
|
for (const name of VERDICT_FILES) {
|
|
8405
8647
|
try {
|
|
8406
|
-
newest = Math.max(newest, statSync5(
|
|
8648
|
+
newest = Math.max(newest, statSync5(join46(dir, name)).mtimeMs);
|
|
8407
8649
|
} catch {
|
|
8408
8650
|
}
|
|
8409
8651
|
}
|
|
8410
8652
|
return newest;
|
|
8411
8653
|
}
|
|
8412
8654
|
function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
8413
|
-
const critiqueDir =
|
|
8655
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8414
8656
|
if (!existsSync28(critiqueDir)) return [];
|
|
8415
|
-
const all =
|
|
8657
|
+
const all = readdirSync18(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
8416
8658
|
const start = sessionStart(transcriptPath);
|
|
8417
8659
|
if (start === void 0) return all;
|
|
8418
8660
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
8419
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
8661
|
+
return all.filter((s) => s === current || verdictsMtime(join46(critiqueDir, s)) >= start - 1e3);
|
|
8420
8662
|
}
|
|
8421
8663
|
function surfacesToProbe(root, input) {
|
|
8422
8664
|
const invocation = lastJigInvocation(input.transcript_path);
|
|
8423
8665
|
return surfacesInPlay(root, invocation?.command, input.transcript_path, invocation?.surface);
|
|
8424
8666
|
}
|
|
8425
8667
|
function fileAtSessionStart(root, path, start) {
|
|
8426
|
-
const git = (args) =>
|
|
8668
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8427
8669
|
try {
|
|
8428
|
-
if (start !== void 0 && statSync5(
|
|
8670
|
+
if (start !== void 0 && statSync5(join46(root, path)).mtimeMs < start - 1e3) return readText(join46(root, path));
|
|
8429
8671
|
} catch {
|
|
8430
8672
|
}
|
|
8431
8673
|
try {
|
|
@@ -8436,14 +8678,14 @@ function fileAtSessionStart(root, path, start) {
|
|
|
8436
8678
|
}
|
|
8437
8679
|
}
|
|
8438
8680
|
function changedOutsideRecords(root, start) {
|
|
8439
|
-
const git = (args) =>
|
|
8681
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8440
8682
|
try {
|
|
8441
8683
|
const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
|
|
8442
8684
|
if (!base) return [];
|
|
8443
8685
|
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
8444
8686
|
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
8445
8687
|
try {
|
|
8446
|
-
return start === void 0 || statSync5(
|
|
8688
|
+
return start === void 0 || statSync5(join46(root, f)).mtimeMs >= start - 1e3;
|
|
8447
8689
|
} catch {
|
|
8448
8690
|
return false;
|
|
8449
8691
|
}
|
|
@@ -8454,12 +8696,12 @@ function changedOutsideRecords(root, start) {
|
|
|
8454
8696
|
}
|
|
8455
8697
|
}
|
|
8456
8698
|
function drawingChangedAfterApproval(root, specPath, at) {
|
|
8457
|
-
const git = (args) =>
|
|
8699
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8458
8700
|
try {
|
|
8459
8701
|
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
8460
8702
|
if (!approvedIn) return void 0;
|
|
8461
8703
|
const then = lf(git(["show", `${approvedIn}:./${at}`]));
|
|
8462
|
-
return then !== readText(
|
|
8704
|
+
return then !== readText(join46(root, at)) ? approvedIn.slice(0, 7) : void 0;
|
|
8463
8705
|
} catch {
|
|
8464
8706
|
return void 0;
|
|
8465
8707
|
}
|
|
@@ -8471,7 +8713,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8471
8713
|
const found = decisionsFile(root);
|
|
8472
8714
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
8473
8715
|
else {
|
|
8474
|
-
const body = readText(
|
|
8716
|
+
const body = readText(join46(root, found));
|
|
8475
8717
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
8476
8718
|
problems.push("DECISIONS.md has no `## Unresolved` section. Round 3 asks by name what is still undecided; write what the owner named, or `None named by the owner.`");
|
|
8477
8719
|
}
|
|
@@ -8481,7 +8723,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8481
8723
|
if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
|
|
8482
8724
|
}
|
|
8483
8725
|
}
|
|
8484
|
-
if (command === "spec" && surface && !existsSync28(
|
|
8726
|
+
if (command === "spec" && surface && !existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`))) {
|
|
8485
8727
|
problems.push(`spec wrote no .jig/specs/${surface}.spec.md. The spec is that file: a draft anywhere else is read by no later step and checked by nothing.`);
|
|
8486
8728
|
return problems;
|
|
8487
8729
|
}
|
|
@@ -8504,10 +8746,10 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8504
8746
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8505
8747
|
if (/^approved/i.test(mockup)) {
|
|
8506
8748
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
8507
|
-
else if (!/^https?:/i.test(at) && !existsSync28(
|
|
8749
|
+
else if (!/^https?:/i.test(at) && !existsSync28(join46(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
8508
8750
|
else if (!/^https?:/i.test(at) && !at.startsWith(".jig/mockups/")) problems.push(`The drawing is at ${at}. A mockup lives in .jig/mockups/, outside what check scans and outside what ships.`);
|
|
8509
8751
|
}
|
|
8510
|
-
if (at && /\.html?$/i.test(at) && existsSync28(
|
|
8752
|
+
if (at && /\.html?$/i.test(at) && existsSync28(join46(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
|
|
8511
8753
|
}
|
|
8512
8754
|
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
8513
8755
|
if ((command === "spec" || command === "make") && spec) {
|
|
@@ -8535,7 +8777,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8535
8777
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8536
8778
|
const mockup = mockupField(spec.body);
|
|
8537
8779
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8538
|
-
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(
|
|
8780
|
+
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
|
|
8539
8781
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
8540
8782
|
if (changedAfter) {
|
|
8541
8783
|
problems.push(`${at} has changed since the owner approved it (${changedAfter}). An approval is of the drawing as the owner saw it: set \`mockup: pending\` in ${spec.path} and ask again.`);
|
|
@@ -8547,7 +8789,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8547
8789
|
if (found) {
|
|
8548
8790
|
let now = "";
|
|
8549
8791
|
try {
|
|
8550
|
-
now = readText(
|
|
8792
|
+
now = readText(join46(root, found));
|
|
8551
8793
|
} catch {
|
|
8552
8794
|
}
|
|
8553
8795
|
const then = fileAtSessionStart(root, found, start);
|
|
@@ -8561,13 +8803,13 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8561
8803
|
if (found) {
|
|
8562
8804
|
let now = "";
|
|
8563
8805
|
try {
|
|
8564
|
-
now = readText(
|
|
8806
|
+
now = readText(join46(root, found));
|
|
8565
8807
|
} catch {
|
|
8566
8808
|
}
|
|
8567
8809
|
const then = fileAtSessionStart(root, found, start);
|
|
8568
8810
|
if (now && now !== then) {
|
|
8569
8811
|
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
8570
|
-
const change = readTweak(
|
|
8812
|
+
const change = readTweak(join46(root, ".jig", "critique", spec.slug))?.change;
|
|
8571
8813
|
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
8572
8814
|
}
|
|
8573
8815
|
}
|
|
@@ -8576,7 +8818,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8576
8818
|
for (const file of modeFiles(root)) {
|
|
8577
8819
|
let now = "";
|
|
8578
8820
|
try {
|
|
8579
|
-
now = readText(
|
|
8821
|
+
now = readText(join46(root, file));
|
|
8580
8822
|
} catch {
|
|
8581
8823
|
continue;
|
|
8582
8824
|
}
|
|
@@ -8587,8 +8829,8 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8587
8829
|
}
|
|
8588
8830
|
}
|
|
8589
8831
|
if (command === "critique") {
|
|
8590
|
-
const dir =
|
|
8591
|
-
const surfaces = existsSync28(dir) ?
|
|
8832
|
+
const dir = join46(root, ".jig", "critique");
|
|
8833
|
+
const surfaces = existsSync28(dir) ? readdirSync18(dir).filter((s) => existsSync28(join46(dir, s, "screen.json")) || existsSync28(join46(dir, s, "code.json"))) : [];
|
|
8592
8834
|
for (const surface2 of surfaces) {
|
|
8593
8835
|
const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
|
|
8594
8836
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -8599,7 +8841,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8599
8841
|
}
|
|
8600
8842
|
}
|
|
8601
8843
|
const own = specFor(root, surface)?.slug;
|
|
8602
|
-
if (own && surfaces.includes(own)) problems.push(...reportOmissions(
|
|
8844
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join46(dir, own), own));
|
|
8603
8845
|
if (surfaces.length === 0) {
|
|
8604
8846
|
problems.push("critique wrote no verdict files. Each reader arm writes .jig/critique/<surface>/screen.json or code.json, and `jig verdicts <surface>` \u2014 not your own count \u2014 decides whether the review is complete. A report without them is not a review.");
|
|
8605
8847
|
}
|
|
@@ -8612,7 +8854,7 @@ function drawingBeforeAsking(root, surface) {
|
|
|
8612
8854
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8613
8855
|
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8614
8856
|
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
8615
|
-
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(
|
|
8857
|
+
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(join46(root, at))) return [];
|
|
8616
8858
|
const found = mockupDrawingProblems(root, spec.body, at);
|
|
8617
8859
|
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
8618
8860
|
}
|
|
@@ -8620,14 +8862,14 @@ function specChecked(root, spec, start, owner) {
|
|
|
8620
8862
|
const found = decisionsFile(root);
|
|
8621
8863
|
let decisions = "";
|
|
8622
8864
|
try {
|
|
8623
|
-
if (found) decisions = readText(
|
|
8865
|
+
if (found) decisions = readText(join46(root, found));
|
|
8624
8866
|
} catch {
|
|
8625
8867
|
}
|
|
8626
8868
|
return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
|
|
8627
8869
|
}
|
|
8628
8870
|
function modeFiles(root) {
|
|
8629
8871
|
try {
|
|
8630
|
-
const state = JSON.parse(
|
|
8872
|
+
const state = JSON.parse(readFileSync38(join46(root, ".jig", "state.json"), "utf8"));
|
|
8631
8873
|
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
8632
8874
|
} catch {
|
|
8633
8875
|
return [];
|
|
@@ -8636,7 +8878,7 @@ function modeFiles(root) {
|
|
|
8636
8878
|
function reportOmissions(dir, surface) {
|
|
8637
8879
|
let report2;
|
|
8638
8880
|
try {
|
|
8639
|
-
report2 = readText(
|
|
8881
|
+
report2 = readText(join46(dir, "REPORT.md")).toLowerCase();
|
|
8640
8882
|
} catch {
|
|
8641
8883
|
return [];
|
|
8642
8884
|
}
|
|
@@ -8644,7 +8886,7 @@ function reportOmissions(dir, surface) {
|
|
|
8644
8886
|
for (const f of VERDICT_FILES) {
|
|
8645
8887
|
let file;
|
|
8646
8888
|
try {
|
|
8647
|
-
file = JSON.parse(
|
|
8889
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
8648
8890
|
} catch {
|
|
8649
8891
|
continue;
|
|
8650
8892
|
}
|
|
@@ -8665,16 +8907,16 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8665
8907
|
if (!/^(approved|skipped)/i.test(mockup)) problems.push(`${spec.path}: \`mockup:\` is ${mockup.split(/\s/)[0] || "empty"}. A tweak changes a page whose drawing the owner has approved (or skipped); take a new page through \`mockup\` and \`make\`.`);
|
|
8666
8908
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
8667
8909
|
const surface = spec.slug;
|
|
8668
|
-
const record = readTweak(
|
|
8910
|
+
const record = readTweak(join46(root, ".jig", "critique", surface));
|
|
8669
8911
|
if (!record) {
|
|
8670
8912
|
problems.push(`.jig/critique/${surface}/tweak.json is missing. It names the change in the owner's words (\`change\`), when it was made (\`at\`) and the rule ids and decisions re-judged for it (\`ids\`).`);
|
|
8671
8913
|
} else {
|
|
8672
8914
|
if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
|
|
8673
|
-
else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(
|
|
8915
|
+
else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join46(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
|
|
8674
8916
|
problems.push(`.jig/critique/${surface}/tweak.json: \`change\` is "${record.change.length > 90 ? `${record.change.slice(0, 90)}\u2026` : record.change}", which the owner did not say in this session. It is their words for the change, as they gave them; what you made of them goes in the spec's Tweak entry.`);
|
|
8675
8917
|
}
|
|
8676
8918
|
try {
|
|
8677
|
-
const raw = JSON.parse(
|
|
8919
|
+
const raw = JSON.parse(readFileSync38(join46(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
|
|
8678
8920
|
const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
|
|
8679
8921
|
if (extra.length) problems.push(`.jig/critique/${surface}/tweak.json: ${extra.map((k) => `\`${k}\``).join(", ")} ${extra.length === 1 ? "is not a field" : "are not fields"} Jig reads. tweak.json holds \`at\`, \`change\`, \`ids\` and \`deferred\`; what else you have to say goes in the spec's Tweak entry.`);
|
|
8680
8922
|
} catch {
|
|
@@ -8689,7 +8931,7 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8689
8931
|
}
|
|
8690
8932
|
return problems;
|
|
8691
8933
|
}
|
|
8692
|
-
const unjudged = unjudgedTweakIds(
|
|
8934
|
+
const unjudged = unjudgedTweakIds(join46(root, ".jig", "critique", surface), record);
|
|
8693
8935
|
if (unjudged.length) {
|
|
8694
8936
|
problems.push(
|
|
8695
8937
|
`.jig/critique/${surface}: tweak.json names ${unjudged.slice(0, 6).join(", ")}${unjudged.length > 6 ? " and more" : ""} but no verdict for ${unjudged.length === 1 ? "it" : "them"} carries \`"tweak": "${record.at}"\`. Each named verdict is re-judged on the changed page by a reader that did not make the change.`
|
|
@@ -8699,7 +8941,7 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8699
8941
|
return problems;
|
|
8700
8942
|
}
|
|
8701
8943
|
function structureSinceApproval(root, spec, front) {
|
|
8702
|
-
const git = (args) =>
|
|
8944
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8703
8945
|
let baseline;
|
|
8704
8946
|
let judged = true;
|
|
8705
8947
|
try {
|
|
@@ -8725,9 +8967,9 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8725
8967
|
problems.push(`${spec.path}: the regions under \`sizes:\` differ from the ones the page had ${when}. A change to what the page holds is not a tweak: take it through \`spec\` and \`mockup\`.`);
|
|
8726
8968
|
}
|
|
8727
8969
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8728
|
-
if (at && !/^https?:/i.test(at) && existsSync28(
|
|
8970
|
+
if (at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
|
|
8729
8971
|
try {
|
|
8730
|
-
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(
|
|
8972
|
+
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join46(root, at))) {
|
|
8731
8973
|
problems.push(`${at} has changed since ${when.replace(/^when /, "")}. A tweak leaves the drawing as approved; a change the drawing must show goes through \`mockup\`.`);
|
|
8732
8974
|
}
|
|
8733
8975
|
} catch {
|
|
@@ -8737,7 +8979,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8737
8979
|
}
|
|
8738
8980
|
function readTweak(dir) {
|
|
8739
8981
|
try {
|
|
8740
|
-
const raw = JSON.parse(
|
|
8982
|
+
const raw = JSON.parse(readFileSync38(join46(dir, "tweak.json"), "utf8"));
|
|
8741
8983
|
const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
|
|
8742
8984
|
return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
|
|
8743
8985
|
} catch {
|
|
@@ -8750,13 +8992,13 @@ function surfacePage(projectRoot, surface) {
|
|
|
8750
8992
|
const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8751
8993
|
const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
|
|
8752
8994
|
for (const candidate of candidates) {
|
|
8753
|
-
if (candidate && existsSync28(
|
|
8995
|
+
if (candidate && existsSync28(join46(projectRoot, candidate))) return candidate;
|
|
8754
8996
|
}
|
|
8755
8997
|
return void 0;
|
|
8756
8998
|
}
|
|
8757
8999
|
function gate(opts) {
|
|
8758
9000
|
const root = opts.projectRoot;
|
|
8759
|
-
if (!existsSync28(
|
|
9001
|
+
if (!existsSync28(join46(root, "jig.config.json")) && !existsSync28(join46(root, ".jig"))) {
|
|
8760
9002
|
return { block: false, reason: "" };
|
|
8761
9003
|
}
|
|
8762
9004
|
const invocation = lastJigInvocation(opts.input.transcript_path);
|
|
@@ -8792,7 +9034,7 @@ ${result.report}`);
|
|
|
8792
9034
|
problems.push(`/jig spec: you are asking the owner to confirm a spec that is not at .jig/specs/${invocation.surface}.spec.md. Write it there, with \`confirmed: false\`, have it checked (step 3c), and ask again: the checks the owner relies on read that file, and a draft kept anywhere else is checked by nothing.`);
|
|
8793
9035
|
}
|
|
8794
9036
|
if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
|
|
8795
|
-
if (spec && existsSync28(
|
|
9037
|
+
if (spec && existsSync28(join46(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
|
|
8796
9038
|
problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
|
|
8797
9039
|
}
|
|
8798
9040
|
}
|
|
@@ -8819,15 +9061,15 @@ ${result.report}`);
|
|
|
8819
9061
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8820
9062
|
problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8821
9063
|
const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
|
|
8822
|
-
const now = existsSync28(
|
|
9064
|
+
const now = existsSync28(join46(root, f)) ? readText(join46(root, f)) : "";
|
|
8823
9065
|
return fileAtSessionStart(root, f, start) !== now;
|
|
8824
9066
|
});
|
|
8825
9067
|
if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
|
|
8826
|
-
const critiqueDir =
|
|
9068
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8827
9069
|
if (existsSync28(critiqueDir)) {
|
|
8828
9070
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
|
|
8829
|
-
const dir =
|
|
8830
|
-
if (!existsSync28(
|
|
9071
|
+
const dir = join46(critiqueDir, surface);
|
|
9072
|
+
if (!existsSync28(join46(dir, "screen.json")) && !existsSync28(join46(dir, "code.json"))) continue;
|
|
8831
9073
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
8832
9074
|
if (!v.ok) {
|
|
8833
9075
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -8857,10 +9099,10 @@ ${problems.join("\n\n")}`
|
|
|
8857
9099
|
}
|
|
8858
9100
|
const session = opts.input.session_id ?? "unknown";
|
|
8859
9101
|
const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
8860
|
-
const stateFile =
|
|
9102
|
+
const stateFile = join46(root, ".jig", "gate.json");
|
|
8861
9103
|
let state = {};
|
|
8862
9104
|
try {
|
|
8863
|
-
state = JSON.parse(
|
|
9105
|
+
state = JSON.parse(readFileSync38(stateFile, "utf8"));
|
|
8864
9106
|
} catch {
|
|
8865
9107
|
}
|
|
8866
9108
|
if (problems.length === 0) {
|
|
@@ -8895,7 +9137,7 @@ function verdictDigests(dir) {
|
|
|
8895
9137
|
for (const f of VERDICT_FILES) {
|
|
8896
9138
|
let file;
|
|
8897
9139
|
try {
|
|
8898
|
-
file = JSON.parse(
|
|
9140
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
8899
9141
|
} catch {
|
|
8900
9142
|
continue;
|
|
8901
9143
|
}
|
|
@@ -8910,7 +9152,7 @@ function writeLock(root, surface, lockPath, dir, now) {
|
|
|
8910
9152
|
const pageFile2 = recordedPage(root, surface);
|
|
8911
9153
|
let page;
|
|
8912
9154
|
try {
|
|
8913
|
-
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(
|
|
9155
|
+
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync38(join46(root, pageFile2), "utf8")) };
|
|
8914
9156
|
} catch {
|
|
8915
9157
|
}
|
|
8916
9158
|
const tweakAt = readTweak(dir)?.at;
|
|
@@ -8923,7 +9165,7 @@ function verdictChecksum(dir) {
|
|
|
8923
9165
|
const parts = VERDICT_FILES.map((f) => {
|
|
8924
9166
|
try {
|
|
8925
9167
|
return `${f}
|
|
8926
|
-
${
|
|
9168
|
+
${readFileSync38(join46(dir, f), "utf8")}`;
|
|
8927
9169
|
} catch {
|
|
8928
9170
|
return "";
|
|
8929
9171
|
}
|
|
@@ -8931,15 +9173,15 @@ ${readFileSync37(join44(dir, f), "utf8")}`;
|
|
|
8931
9173
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
8932
9174
|
}
|
|
8933
9175
|
function verdictGuard(root, command, inPlay) {
|
|
8934
|
-
const critiqueDir =
|
|
9176
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8935
9177
|
if (!existsSync28(critiqueDir)) return [];
|
|
8936
9178
|
const problems = [];
|
|
8937
|
-
for (const surface of
|
|
9179
|
+
for (const surface of readdirSync18(critiqueDir)) {
|
|
8938
9180
|
if (surface.startsWith("_")) continue;
|
|
8939
|
-
const dir =
|
|
9181
|
+
const dir = join46(critiqueDir, surface);
|
|
8940
9182
|
const now = verdictChecksum(dir);
|
|
8941
9183
|
if (!now) continue;
|
|
8942
|
-
const lockPath =
|
|
9184
|
+
const lockPath = join46(dir, LOCK);
|
|
8943
9185
|
if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
|
|
8944
9186
|
writeLock(root, surface, lockPath, dir, now);
|
|
8945
9187
|
problems.push(...lockLeftBehind(root, surface));
|
|
@@ -8947,7 +9189,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
8947
9189
|
}
|
|
8948
9190
|
let lock = {};
|
|
8949
9191
|
try {
|
|
8950
|
-
lock = JSON.parse(
|
|
9192
|
+
lock = JSON.parse(readFileSync38(lockPath, "utf8"));
|
|
8951
9193
|
} catch {
|
|
8952
9194
|
continue;
|
|
8953
9195
|
}
|
|
@@ -8972,7 +9214,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
8972
9214
|
function lockLeftBehind(root, surface) {
|
|
8973
9215
|
const rel = `.jig/critique/${surface}`;
|
|
8974
9216
|
try {
|
|
8975
|
-
const status = (paths) =>
|
|
9217
|
+
const status = (paths) => execFileSync5("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
8976
9218
|
if (!status([`${rel}/${LOCK}`])) return [];
|
|
8977
9219
|
if (status(VERDICT_FILES.map((f) => `${rel}/${f}`))) return [];
|
|
8978
9220
|
} catch {
|
|
@@ -8984,7 +9226,7 @@ function probesLeftBehind(root, surfaces) {
|
|
|
8984
9226
|
if (!surfaces.length) return [];
|
|
8985
9227
|
let lines;
|
|
8986
9228
|
try {
|
|
8987
|
-
lines =
|
|
9229
|
+
lines = execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
|
|
8988
9230
|
} catch {
|
|
8989
9231
|
return [];
|
|
8990
9232
|
}
|
|
@@ -9023,7 +9265,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
9023
9265
|
for (const f of VERDICT_FILES) {
|
|
9024
9266
|
let file;
|
|
9025
9267
|
try {
|
|
9026
|
-
file = JSON.parse(
|
|
9268
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
9027
9269
|
} catch {
|
|
9028
9270
|
continue;
|
|
9029
9271
|
}
|
|
@@ -9035,7 +9277,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
9035
9277
|
}
|
|
9036
9278
|
function save(file, state) {
|
|
9037
9279
|
try {
|
|
9038
|
-
mkdirSync8(
|
|
9280
|
+
mkdirSync8(join46(file, ".."), { recursive: true });
|
|
9039
9281
|
writeFileSync6(file, JSON.stringify(state), "utf8");
|
|
9040
9282
|
} catch {
|
|
9041
9283
|
}
|
|
@@ -9043,7 +9285,7 @@ function save(file, state) {
|
|
|
9043
9285
|
|
|
9044
9286
|
// src/index.ts
|
|
9045
9287
|
var packageRoot = getPackageRoot();
|
|
9046
|
-
var { version } = JSON.parse(
|
|
9288
|
+
var { version } = JSON.parse(readFileSync39(join47(packageRoot, "package.json"), "utf8"));
|
|
9047
9289
|
function warnIfUnpublishedPin() {
|
|
9048
9290
|
if (isDevVersion(version)) {
|
|
9049
9291
|
console.warn(
|
|
@@ -9155,12 +9397,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
9155
9397
|
program.command("checksum").description("Print a spec's checksum, the value its .checked.json records as `spec`.").argument("<spec>", "the spec by name (tokens-and-modes) or path (.jig/specs/tokens-and-modes.spec.md)").action((name) => {
|
|
9156
9398
|
const projectRoot = findProjectRoot(process.cwd());
|
|
9157
9399
|
const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
|
|
9158
|
-
const path =
|
|
9400
|
+
const path = join47(projectRoot, ".jig", "specs", `${slug}.spec.md`);
|
|
9159
9401
|
if (!existsSync29(path)) {
|
|
9160
9402
|
console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
|
|
9161
9403
|
process.exit(1);
|
|
9162
9404
|
}
|
|
9163
|
-
console.log(specChecksum(
|
|
9405
|
+
console.log(specChecksum(readFileSync39(path, "utf8")));
|
|
9164
9406
|
});
|
|
9165
9407
|
program.command("verdicts").description("Verify a critique's verdict files and compute its counts.").argument("<surface>", "the surface slug the critique wrote under .jig/critique/").option("--reprobe", "re-take every probe that is missing or was taken on an older version of the page, first (needs a browser)").action(async (surface, opts) => {
|
|
9166
9408
|
const projectRoot = findProjectRoot(process.cwd());
|
|
@@ -9294,7 +9536,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
9294
9536
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
9295
9537
|
let input = {};
|
|
9296
9538
|
try {
|
|
9297
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
9539
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync39(0, "utf8") || "{}");
|
|
9298
9540
|
} catch {
|
|
9299
9541
|
}
|
|
9300
9542
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -9335,7 +9577,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
9335
9577
|
});
|
|
9336
9578
|
function resolveSurfaceMode(projectRoot) {
|
|
9337
9579
|
try {
|
|
9338
|
-
const config = JSON.parse(
|
|
9580
|
+
const config = JSON.parse(readFileSync39(join47(projectRoot, "jig.config.json"), "utf8"));
|
|
9339
9581
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
9340
9582
|
return modes.length === 1 ? modes[0] : void 0;
|
|
9341
9583
|
} catch {
|