jig-ui 0.26.3 → 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 +29 -0
- package/README.md +3 -3
- package/dist/index.js +766 -578
- package/examples/E-165.html +15 -0
- package/package.json +4 -2
- package/rules/00-anti-patterns.md +8 -1
- package/rules/01-modes.md +1 -1
- package/rules/02-tokens.md +5 -4
- package/rules.index.json +8 -0
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, readdirSync as readdirSync11, 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) {
|
|
@@ -2643,145 +2770,47 @@ function lineForOffset(starts, offset) {
|
|
|
2643
2770
|
if (starts[mid] <= offset) lo = mid;
|
|
2644
2771
|
else hi = mid - 1;
|
|
2645
2772
|
}
|
|
2646
|
-
return lo + 1;
|
|
2647
|
-
}
|
|
2648
|
-
function sourceLine(source, line) {
|
|
2649
|
-
const lines = source.split("\n");
|
|
2650
|
-
return (lines[line - 1] ?? "").trim();
|
|
2651
|
-
}
|
|
2652
|
-
|
|
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++;
|
|
2773
|
+
return lo + 1;
|
|
2774
|
+
}
|
|
2775
|
+
function sourceLine(source, line) {
|
|
2776
|
+
const lines = source.split("\n");
|
|
2777
|
+
return (lines[line - 1] ?? "").trim();
|
|
2778
|
+
}
|
|
2779
|
+
|
|
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 {
|
|
@@ -3837,8 +3934,8 @@ var menuState = {
|
|
|
3837
3934
|
};
|
|
3838
3935
|
|
|
3839
3936
|
// src/check/declared-properties.ts
|
|
3840
|
-
import { existsSync as existsSync13, readFileSync as
|
|
3841
|
-
import { join as
|
|
3937
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
3938
|
+
import { join as join22 } from "path";
|
|
3842
3939
|
var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
|
|
3843
3940
|
var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
|
|
3844
3941
|
var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
|
|
@@ -3850,16 +3947,16 @@ function addAll(names, text) {
|
|
|
3850
3947
|
}
|
|
3851
3948
|
}
|
|
3852
3949
|
function readPackageImport(projectRoot, spec) {
|
|
3853
|
-
const base =
|
|
3950
|
+
const base = join22(projectRoot, "node_modules", spec);
|
|
3854
3951
|
for (const candidate of [base, `${base}.css`]) {
|
|
3855
3952
|
try {
|
|
3856
|
-
if (existsSync13(candidate) && candidate.endsWith(".css")) return
|
|
3953
|
+
if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync17(candidate, "utf8");
|
|
3857
3954
|
} catch {
|
|
3858
3955
|
}
|
|
3859
3956
|
}
|
|
3860
3957
|
try {
|
|
3861
|
-
const pkg = JSON.parse(
|
|
3862
|
-
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");
|
|
3863
3960
|
} catch {
|
|
3864
3961
|
}
|
|
3865
3962
|
return void 0;
|
|
@@ -3871,7 +3968,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
|
|
|
3871
3968
|
for (const file of files) {
|
|
3872
3969
|
let raw;
|
|
3873
3970
|
try {
|
|
3874
|
-
raw =
|
|
3971
|
+
raw = readFileSync17(join22(projectRoot, file), "utf8");
|
|
3875
3972
|
} catch {
|
|
3876
3973
|
continue;
|
|
3877
3974
|
}
|
|
@@ -4364,6 +4461,40 @@ var interfaceSafety = {
|
|
|
4364
4461
|
}
|
|
4365
4462
|
};
|
|
4366
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
|
+
|
|
4367
4498
|
// src/check/detectors/generated-look.ts
|
|
4368
4499
|
function report(ctx, name, file, raw, hits) {
|
|
4369
4500
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4719,6 +4850,7 @@ var DETECTORS = [
|
|
|
4719
4850
|
semanticElement,
|
|
4720
4851
|
metadata,
|
|
4721
4852
|
interfaceSafety,
|
|
4853
|
+
newTabLink,
|
|
4722
4854
|
...generatedLookDetectors
|
|
4723
4855
|
];
|
|
4724
4856
|
function getDetector(name) {
|
|
@@ -4738,7 +4870,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4738
4870
|
if (applicable.length === 0) continue;
|
|
4739
4871
|
let raw;
|
|
4740
4872
|
try {
|
|
4741
|
-
raw =
|
|
4873
|
+
raw = readFileSync18(join23(projectRoot, file), "utf8");
|
|
4742
4874
|
} catch {
|
|
4743
4875
|
continue;
|
|
4744
4876
|
}
|
|
@@ -4752,8 +4884,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4752
4884
|
}
|
|
4753
4885
|
|
|
4754
4886
|
// src/check/tokens.ts
|
|
4755
|
-
import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as
|
|
4756
|
-
import { join as
|
|
4887
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "fs";
|
|
4888
|
+
import { join as join24 } from "path";
|
|
4757
4889
|
function rootDeclarations(source) {
|
|
4758
4890
|
const out = {};
|
|
4759
4891
|
for (const block of splitRuleBlocks(source)) {
|
|
@@ -4768,20 +4900,20 @@ function rootDeclarations(source) {
|
|
|
4768
4900
|
}
|
|
4769
4901
|
function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
4770
4902
|
const jig = {};
|
|
4771
|
-
const tokensDir =
|
|
4903
|
+
const tokensDir = join24(projectRoot, ".jig", "tokens");
|
|
4772
4904
|
if (existsSync14(tokensDir)) {
|
|
4773
4905
|
for (const file of readdirSync9(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
|
|
4774
|
-
Object.assign(jig, rootDeclarations(
|
|
4906
|
+
Object.assign(jig, rootDeclarations(readFileSync19(join24(tokensDir, file), "utf8")));
|
|
4775
4907
|
}
|
|
4776
4908
|
}
|
|
4777
4909
|
const consumer = {};
|
|
4778
4910
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
4779
|
-
const vendored =
|
|
4911
|
+
const vendored = join24(".jig", "tokens");
|
|
4780
4912
|
for (const file of consumerFiles) {
|
|
4781
4913
|
if (file.includes(vendored)) continue;
|
|
4782
4914
|
let source;
|
|
4783
4915
|
try {
|
|
4784
|
-
source =
|
|
4916
|
+
source = readFileSync19(join24(projectRoot, file), "utf8");
|
|
4785
4917
|
} catch {
|
|
4786
4918
|
continue;
|
|
4787
4919
|
}
|
|
@@ -4800,8 +4932,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
|
4800
4932
|
}
|
|
4801
4933
|
|
|
4802
4934
|
// src/check/exempt.ts
|
|
4803
|
-
import { existsSync as existsSync15, readFileSync as
|
|
4804
|
-
import { join as
|
|
4935
|
+
import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
|
|
4936
|
+
import { join as join25 } from "path";
|
|
4805
4937
|
function globToRegExp(glob) {
|
|
4806
4938
|
let out = "";
|
|
4807
4939
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -4821,11 +4953,11 @@ function globToRegExp(glob) {
|
|
|
4821
4953
|
}
|
|
4822
4954
|
function readExemptions(projectRoot) {
|
|
4823
4955
|
const none = { patterns: [], ignored: [] };
|
|
4824
|
-
const configPath =
|
|
4956
|
+
const configPath = join25(projectRoot, "jig.config.json");
|
|
4825
4957
|
if (!existsSync15(configPath)) return none;
|
|
4826
4958
|
let declared2;
|
|
4827
4959
|
try {
|
|
4828
|
-
declared2 = JSON.parse(
|
|
4960
|
+
declared2 = JSON.parse(readFileSync20(configPath, "utf8")).exempt;
|
|
4829
4961
|
} catch {
|
|
4830
4962
|
return none;
|
|
4831
4963
|
}
|
|
@@ -4875,8 +5007,8 @@ function applyExemptions(files, patterns) {
|
|
|
4875
5007
|
}
|
|
4876
5008
|
|
|
4877
5009
|
// src/check/waiver.ts
|
|
4878
|
-
import { readFileSync as
|
|
4879
|
-
import { join as
|
|
5010
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5011
|
+
import { join as join26 } from "path";
|
|
4880
5012
|
var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
|
|
4881
5013
|
function readWaiver(line) {
|
|
4882
5014
|
const m = WAIVER.exec(line);
|
|
@@ -4891,7 +5023,7 @@ function applyWaivers(root, findings) {
|
|
|
4891
5023
|
let cached = lines.get(file);
|
|
4892
5024
|
if (!cached) {
|
|
4893
5025
|
try {
|
|
4894
|
-
cached =
|
|
5026
|
+
cached = readFileSync21(join26(root, file), "utf8").split("\n");
|
|
4895
5027
|
} catch {
|
|
4896
5028
|
cached = [];
|
|
4897
5029
|
}
|
|
@@ -4956,8 +5088,8 @@ function hasMenuToggle(raw) {
|
|
|
4956
5088
|
}
|
|
4957
5089
|
|
|
4958
5090
|
// src/check/mode-wiring.ts
|
|
4959
|
-
import { existsSync as existsSync16, readFileSync as
|
|
4960
|
-
import { dirname as
|
|
5091
|
+
import { existsSync as existsSync16, readFileSync as readFileSync22 } from "fs";
|
|
5092
|
+
import { dirname as dirname7, join as join27 } from "path";
|
|
4961
5093
|
var MODES = ["editorial", "product", "operator"];
|
|
4962
5094
|
function configProblems(config) {
|
|
4963
5095
|
const problems = [];
|
|
@@ -4979,10 +5111,10 @@ function configProblems(config) {
|
|
|
4979
5111
|
}
|
|
4980
5112
|
function modeWiringProblems(projectRoot) {
|
|
4981
5113
|
let config;
|
|
4982
|
-
const path =
|
|
5114
|
+
const path = join27(projectRoot, "jig.config.json");
|
|
4983
5115
|
if (!existsSync16(path)) return [];
|
|
4984
5116
|
try {
|
|
4985
|
-
config = JSON.parse(
|
|
5117
|
+
config = JSON.parse(readFileSync22(path, "utf8"));
|
|
4986
5118
|
} catch {
|
|
4987
5119
|
return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
|
|
4988
5120
|
}
|
|
@@ -4991,14 +5123,14 @@ function modeWiringProblems(projectRoot) {
|
|
|
4991
5123
|
if (typeof config.brand !== "string" || !Array.isArray(config.surfaces)) return [];
|
|
4992
5124
|
const modes = [...new Set(config.surfaces.map((s) => s?.mode).filter((m) => typeof m === "string"))];
|
|
4993
5125
|
if (modes.length === 0) return [];
|
|
4994
|
-
const dir =
|
|
5126
|
+
const dir = dirname7(config.brand);
|
|
4995
5127
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
4996
5128
|
const multi = modes.length > 1;
|
|
4997
5129
|
const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
|
|
4998
|
-
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))));
|
|
4999
5131
|
if (!anyBarrel) return [];
|
|
5000
5132
|
const problems = [];
|
|
5001
|
-
if (multi && existsSync16(
|
|
5133
|
+
if (multi && existsSync16(join27(projectRoot, rel("theme.css")))) {
|
|
5002
5134
|
problems.push({
|
|
5003
5135
|
mode: modes[0],
|
|
5004
5136
|
barrel: rel("theme.css"),
|
|
@@ -5010,7 +5142,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
5010
5142
|
const modeFile = `mode.${mode}.css`;
|
|
5011
5143
|
let body;
|
|
5012
5144
|
try {
|
|
5013
|
-
body =
|
|
5145
|
+
body = readFileSync22(join27(projectRoot, barrel), "utf8");
|
|
5014
5146
|
} catch {
|
|
5015
5147
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
|
|
5016
5148
|
return;
|
|
@@ -5022,7 +5154,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
5022
5154
|
barrel,
|
|
5023
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`
|
|
5024
5156
|
});
|
|
5025
|
-
} else if (!existsSync16(
|
|
5157
|
+
} else if (!existsSync16(join27(projectRoot, rel(modeFile)))) {
|
|
5026
5158
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
5027
5159
|
}
|
|
5028
5160
|
});
|
|
@@ -5030,8 +5162,8 @@ function modeWiringProblems(projectRoot) {
|
|
|
5030
5162
|
}
|
|
5031
5163
|
|
|
5032
5164
|
// src/check/token-audit.ts
|
|
5033
|
-
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as
|
|
5034
|
-
import { join as
|
|
5165
|
+
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
|
|
5166
|
+
import { join as join28 } from "path";
|
|
5035
5167
|
var TEXT_FLOOR = 4.5;
|
|
5036
5168
|
var UI_FLOOR = 3;
|
|
5037
5169
|
var FOREGROUNDS = [
|
|
@@ -5087,24 +5219,24 @@ function floorFor(token) {
|
|
|
5087
5219
|
function tokenFiles(projectRoot) {
|
|
5088
5220
|
const read = (rel) => {
|
|
5089
5221
|
try {
|
|
5090
|
-
return
|
|
5222
|
+
return readFileSync23(join28(projectRoot, ...rel.split("/")), "utf8");
|
|
5091
5223
|
} catch {
|
|
5092
5224
|
return null;
|
|
5093
5225
|
}
|
|
5094
5226
|
};
|
|
5095
|
-
const statePath =
|
|
5227
|
+
const statePath = join28(projectRoot, ".jig", "state.json");
|
|
5096
5228
|
if (existsSync17(statePath)) {
|
|
5097
5229
|
try {
|
|
5098
|
-
const state = JSON.parse(
|
|
5230
|
+
const state = JSON.parse(readFileSync23(statePath, "utf8"));
|
|
5099
5231
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
5100
5232
|
const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
|
|
5101
5233
|
if (out.length > 0) return out;
|
|
5102
5234
|
} catch {
|
|
5103
5235
|
}
|
|
5104
5236
|
}
|
|
5105
|
-
const legacy =
|
|
5237
|
+
const legacy = join28(projectRoot, ".jig", "tokens");
|
|
5106
5238
|
if (!existsSync17(legacy)) return [];
|
|
5107
|
-
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") }));
|
|
5108
5240
|
}
|
|
5109
5241
|
function auditTokenLayer(projectRoot) {
|
|
5110
5242
|
const files = tokenFiles(projectRoot);
|
|
@@ -5175,15 +5307,15 @@ function auditTokenLayer(projectRoot) {
|
|
|
5175
5307
|
function checkExitCode(result) {
|
|
5176
5308
|
return result.hasError ? 1 : 0;
|
|
5177
5309
|
}
|
|
5178
|
-
var LEGACY_INDEX_REL =
|
|
5310
|
+
var LEGACY_INDEX_REL = join29(".jig", "rules.index.json");
|
|
5179
5311
|
function resolveIndexPath(projectRoot) {
|
|
5180
|
-
const legacy =
|
|
5312
|
+
const legacy = join29(projectRoot, LEGACY_INDEX_REL);
|
|
5181
5313
|
if (existsSync18(legacy)) return legacy;
|
|
5182
|
-
return
|
|
5314
|
+
return join29(assetRoot(), "rules.index.json");
|
|
5183
5315
|
}
|
|
5184
5316
|
function resolveMode(projectRoot) {
|
|
5185
5317
|
try {
|
|
5186
|
-
const config = JSON.parse(
|
|
5318
|
+
const config = JSON.parse(readFileSync24(join29(projectRoot, "jig.config.json"), "utf8"));
|
|
5187
5319
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
5188
5320
|
if (modes.length === 1) return modes[0];
|
|
5189
5321
|
return modes.length > 1 ? "mixed" : "unknown";
|
|
@@ -5200,14 +5332,14 @@ function summariseUnscanned(files) {
|
|
|
5200
5332
|
}
|
|
5201
5333
|
function countSpecs() {
|
|
5202
5334
|
try {
|
|
5203
|
-
return loadSpecs(
|
|
5335
|
+
return loadSpecs(join29(assetRoot(), "rules")).length;
|
|
5204
5336
|
} catch {
|
|
5205
5337
|
return 0;
|
|
5206
5338
|
}
|
|
5207
5339
|
}
|
|
5208
5340
|
function check(opts) {
|
|
5209
5341
|
const indexPath = resolveIndexPath(opts.projectRoot);
|
|
5210
|
-
const index = validateIndex(JSON.parse(
|
|
5342
|
+
const index = validateIndex(JSON.parse(readFileSync24(indexPath, "utf8")));
|
|
5211
5343
|
const selection = selectFiles(opts.projectRoot, opts.all);
|
|
5212
5344
|
const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
|
|
5213
5345
|
const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
|
|
@@ -5218,7 +5350,7 @@ function check(opts) {
|
|
|
5218
5350
|
const projectParticipates = stylesheets.some((f) => {
|
|
5219
5351
|
if (!hasExtension(f, CSS_EXTENSIONS)) return false;
|
|
5220
5352
|
try {
|
|
5221
|
-
return participatesInTokenLayer(
|
|
5353
|
+
return participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
|
|
5222
5354
|
} catch {
|
|
5223
5355
|
return false;
|
|
5224
5356
|
}
|
|
@@ -5226,7 +5358,7 @@ function check(opts) {
|
|
|
5226
5358
|
const projectResponsive = stylesheets.some((f) => {
|
|
5227
5359
|
if (!isStyleBearing(f)) return false;
|
|
5228
5360
|
try {
|
|
5229
|
-
const raw =
|
|
5361
|
+
const raw = readFileSync24(join29(opts.projectRoot, f), "utf8");
|
|
5230
5362
|
return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
|
|
5231
5363
|
} catch {
|
|
5232
5364
|
return false;
|
|
@@ -5235,25 +5367,25 @@ function check(opts) {
|
|
|
5235
5367
|
const viewportFitCover = stylesheets.some((f) => {
|
|
5236
5368
|
if (!isStyleBearing(f)) return false;
|
|
5237
5369
|
try {
|
|
5238
|
-
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"));
|
|
5239
5371
|
} catch {
|
|
5240
5372
|
return false;
|
|
5241
5373
|
}
|
|
5242
5374
|
});
|
|
5243
5375
|
const projectMenuToggle = stylesheets.some((f) => {
|
|
5244
5376
|
try {
|
|
5245
|
-
return hasMenuToggle(
|
|
5377
|
+
return hasMenuToggle(readFileSync24(join29(opts.projectRoot, f), "utf8"));
|
|
5246
5378
|
} catch {
|
|
5247
5379
|
return false;
|
|
5248
5380
|
}
|
|
5249
5381
|
});
|
|
5250
5382
|
const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
|
|
5251
5383
|
const touchFormText2 = { body: false, control: false };
|
|
5252
|
-
const vendoredTokens = existsSync18(
|
|
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)) : [];
|
|
5253
5385
|
for (const f of [...stylesheets, ...vendoredTokens]) {
|
|
5254
5386
|
if (!isStyleBearing(f)) continue;
|
|
5255
5387
|
try {
|
|
5256
|
-
const found = touchFormText(maskComments(maskNonStyleRegions(
|
|
5388
|
+
const found = touchFormText(maskComments(maskNonStyleRegions(readFileSync24(join29(opts.projectRoot, f), "utf8"), f)));
|
|
5257
5389
|
touchFormText2.body ||= found.body;
|
|
5258
5390
|
touchFormText2.control ||= found.control;
|
|
5259
5391
|
} catch {
|
|
@@ -5261,7 +5393,7 @@ function check(opts) {
|
|
|
5261
5393
|
}
|
|
5262
5394
|
const withStyles = files.filter((f) => {
|
|
5263
5395
|
try {
|
|
5264
|
-
const src =
|
|
5396
|
+
const src = readFileSync24(join29(opts.projectRoot, f), "utf8");
|
|
5265
5397
|
if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
|
|
5266
5398
|
if (maskNonStyleRegions(src, f).trim().length > 0) return true;
|
|
5267
5399
|
return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
|
|
@@ -5291,6 +5423,19 @@ function check(opts) {
|
|
|
5291
5423
|
for (const gap of brandDecidedGaps(opts.projectRoot)) {
|
|
5292
5424
|
detected.push({ ruleId: "A-01", detector: "brand-decided", bucket: "mechanical", severity: "warning", file: gap.file, line: gap.line, message: gap.message });
|
|
5293
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
|
+
}
|
|
5294
5439
|
const { findings, waived } = applyWaivers(opts.projectRoot, detected);
|
|
5295
5440
|
const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
|
|
5296
5441
|
const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
|
|
@@ -5299,7 +5444,7 @@ function check(opts) {
|
|
|
5299
5444
|
// told to add a token @import it has nowhere to put.
|
|
5300
5445
|
cssFiles.length > 0 && cssFiles.every((f) => {
|
|
5301
5446
|
try {
|
|
5302
|
-
return !participatesInTokenLayer(
|
|
5447
|
+
return !participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
|
|
5303
5448
|
} catch {
|
|
5304
5449
|
return true;
|
|
5305
5450
|
}
|
|
@@ -5322,6 +5467,7 @@ function check(opts) {
|
|
|
5322
5467
|
exemptIgnored,
|
|
5323
5468
|
scope: selection.mode,
|
|
5324
5469
|
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
5470
|
+
tailwindSkipped: tailwind.skipped,
|
|
5325
5471
|
agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
|
|
5326
5472
|
waived
|
|
5327
5473
|
});
|
|
@@ -5329,12 +5475,12 @@ function check(opts) {
|
|
|
5329
5475
|
}
|
|
5330
5476
|
|
|
5331
5477
|
// src/commands/init.ts
|
|
5332
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync12, readFileSync as
|
|
5333
|
-
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";
|
|
5334
5480
|
|
|
5335
5481
|
// src/init/detect.ts
|
|
5336
|
-
import { readFileSync as
|
|
5337
|
-
import { join as
|
|
5482
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
5483
|
+
import { join as join30 } from "path";
|
|
5338
5484
|
var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
|
|
5339
5485
|
var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
|
|
5340
5486
|
var TAILWIND_V4_THEME_RE = /@theme\b/;
|
|
@@ -5351,7 +5497,7 @@ var FRAMEWORK_DEPS = [
|
|
|
5351
5497
|
function detectFramework(projectRoot) {
|
|
5352
5498
|
let pkg;
|
|
5353
5499
|
try {
|
|
5354
|
-
pkg = JSON.parse(
|
|
5500
|
+
pkg = JSON.parse(readFileSync25(join30(projectRoot, "package.json"), "utf8"));
|
|
5355
5501
|
} catch {
|
|
5356
5502
|
return void 0;
|
|
5357
5503
|
}
|
|
@@ -5369,7 +5515,7 @@ function detect(projectRoot) {
|
|
|
5369
5515
|
for (const f of cssFiles) {
|
|
5370
5516
|
let source;
|
|
5371
5517
|
try {
|
|
5372
|
-
source =
|
|
5518
|
+
source = readFileSync25(join30(projectRoot, f), "utf8");
|
|
5373
5519
|
} catch {
|
|
5374
5520
|
continue;
|
|
5375
5521
|
}
|
|
@@ -5387,12 +5533,12 @@ function detect(projectRoot) {
|
|
|
5387
5533
|
}
|
|
5388
5534
|
|
|
5389
5535
|
// src/init/gitignore.ts
|
|
5390
|
-
import { execFileSync as
|
|
5536
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
5391
5537
|
function isIgnored(projectRoot, relPath) {
|
|
5392
5538
|
const candidates = relPath.endsWith("/") ? [relPath] : [relPath, `${relPath}/`];
|
|
5393
5539
|
return candidates.some((path) => {
|
|
5394
5540
|
try {
|
|
5395
|
-
|
|
5541
|
+
execFileSync3("git", ["check-ignore", "-q", "--", path], {
|
|
5396
5542
|
cwd: projectRoot,
|
|
5397
5543
|
stdio: ["ignore", "ignore", "ignore"]
|
|
5398
5544
|
});
|
|
@@ -5404,8 +5550,8 @@ function isIgnored(projectRoot, relPath) {
|
|
|
5404
5550
|
}
|
|
5405
5551
|
|
|
5406
5552
|
// src/init/derive.ts
|
|
5407
|
-
import { readFileSync as
|
|
5408
|
-
import { join as
|
|
5553
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
5554
|
+
import { join as join31 } from "path";
|
|
5409
5555
|
var DEFAULT_PROPOSAL = {
|
|
5410
5556
|
h: 264,
|
|
5411
5557
|
s: 0,
|
|
@@ -5438,7 +5584,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
|
|
|
5438
5584
|
for (const file of cssFiles) {
|
|
5439
5585
|
let source;
|
|
5440
5586
|
try {
|
|
5441
|
-
source =
|
|
5587
|
+
source = readFileSync26(join31(projectRoot, file), "utf8");
|
|
5442
5588
|
} catch {
|
|
5443
5589
|
continue;
|
|
5444
5590
|
}
|
|
@@ -5487,7 +5633,7 @@ function balancedBraceBody(source, openIdx) {
|
|
|
5487
5633
|
function fromTailwindConfig(projectRoot, tailwindConfigFile) {
|
|
5488
5634
|
let source;
|
|
5489
5635
|
try {
|
|
5490
|
-
source =
|
|
5636
|
+
source = readFileSync26(join31(projectRoot, tailwindConfigFile), "utf8");
|
|
5491
5637
|
} catch {
|
|
5492
5638
|
return null;
|
|
5493
5639
|
}
|
|
@@ -5543,7 +5689,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
|
|
|
5543
5689
|
for (const file of cssFiles) {
|
|
5544
5690
|
let source;
|
|
5545
5691
|
try {
|
|
5546
|
-
source =
|
|
5692
|
+
source = readFileSync26(join31(projectRoot, file), "utf8");
|
|
5547
5693
|
} catch {
|
|
5548
5694
|
continue;
|
|
5549
5695
|
}
|
|
@@ -5703,8 +5849,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
|
|
|
5703
5849
|
}
|
|
5704
5850
|
|
|
5705
5851
|
// src/init/project-name.ts
|
|
5706
|
-
import { readFileSync as
|
|
5707
|
-
import { basename as basename2, join as
|
|
5852
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
5853
|
+
import { basename as basename2, join as join32 } from "path";
|
|
5708
5854
|
function slugify(name) {
|
|
5709
5855
|
const withoutScope = name.replace(/^@[^/]+\//, "");
|
|
5710
5856
|
const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -5712,7 +5858,7 @@ function slugify(name) {
|
|
|
5712
5858
|
}
|
|
5713
5859
|
function deriveProjectSlug(projectRoot) {
|
|
5714
5860
|
try {
|
|
5715
|
-
const pkg = JSON.parse(
|
|
5861
|
+
const pkg = JSON.parse(readFileSync27(join32(projectRoot, "package.json"), "utf8"));
|
|
5716
5862
|
if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
|
|
5717
5863
|
} catch {
|
|
5718
5864
|
}
|
|
@@ -5721,6 +5867,8 @@ function deriveProjectSlug(projectRoot) {
|
|
|
5721
5867
|
|
|
5722
5868
|
// src/init/utilities.ts
|
|
5723
5869
|
var TAILWIND_NAMESPACES = [
|
|
5870
|
+
"--opacity-",
|
|
5871
|
+
"--transition-duration-",
|
|
5724
5872
|
"--color-",
|
|
5725
5873
|
"--font-",
|
|
5726
5874
|
"--text-",
|
|
@@ -5742,21 +5890,60 @@ var TAILWIND_NAMESPACES = [
|
|
|
5742
5890
|
"--size-",
|
|
5743
5891
|
"--border-width-"
|
|
5744
5892
|
];
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
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
|
+
}
|
|
5748
5934
|
}
|
|
5749
5935
|
function declaredTokenNames(css) {
|
|
5750
5936
|
return [...css.matchAll(/(^|[;{\s])(--[\w-]+)\s*:/g)].map((m) => m[2]);
|
|
5751
5937
|
}
|
|
5752
|
-
function utilitiesBody(
|
|
5753
|
-
|
|
5938
|
+
function utilitiesBody(entries, version2) {
|
|
5939
|
+
const aliases = entries.map((e) => typeof e === "string" ? { alias: e, token: e } : e);
|
|
5940
|
+
if (aliases.length === 0) {
|
|
5754
5941
|
throw new Error(
|
|
5755
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."
|
|
5756
5943
|
);
|
|
5757
5944
|
}
|
|
5758
|
-
const width = Math.max(...
|
|
5759
|
-
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});`);
|
|
5760
5947
|
return `/* utilities.css \u2014 vendored from Jig v${version2}.
|
|
5761
5948
|
Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill.
|
|
5762
5949
|
|
|
@@ -5780,10 +5967,11 @@ function utilitiesBody(names, version2) {
|
|
|
5780
5967
|
order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
|
|
5781
5968
|
removes the utility, deleting Jig's removes the value.
|
|
5782
5969
|
|
|
5783
|
-
Only tokens
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
\`
|
|
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)\`.
|
|
5787
5975
|
|
|
5788
5976
|
Regenerate with \`jig init\` after the token layer changes; a token missing here
|
|
5789
5977
|
is a class that renders and matches nothing. */
|
|
@@ -5866,9 +6054,9 @@ function isValidSurfaceArray(v) {
|
|
|
5866
6054
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
5867
6055
|
if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
5868
6056
|
try {
|
|
5869
|
-
const parsed = JSON.parse(
|
|
6057
|
+
const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
5870
6058
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
5871
|
-
const brandResolves = brandCandidate ? existsSync19(
|
|
6059
|
+
const brandResolves = brandCandidate ? existsSync19(join33(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
5872
6060
|
return {
|
|
5873
6061
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
5874
6062
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -5881,7 +6069,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
|
|
|
5881
6069
|
if (!existsSync19(configAbsPath)) return { path: null };
|
|
5882
6070
|
let declared2;
|
|
5883
6071
|
try {
|
|
5884
|
-
declared2 = JSON.parse(
|
|
6072
|
+
declared2 = JSON.parse(readFileSync28(configAbsPath, "utf8")).brand;
|
|
5885
6073
|
} catch {
|
|
5886
6074
|
return { path: null };
|
|
5887
6075
|
}
|
|
@@ -5911,7 +6099,7 @@ function defaultTokenDir(detection) {
|
|
|
5911
6099
|
function readConfiguredSurfaces(configAbsPath) {
|
|
5912
6100
|
if (!existsSync19(configAbsPath)) return null;
|
|
5913
6101
|
try {
|
|
5914
|
-
const parsed = JSON.parse(
|
|
6102
|
+
const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
5915
6103
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
5916
6104
|
} catch {
|
|
5917
6105
|
return null;
|
|
@@ -5921,7 +6109,7 @@ function isCssModule(path) {
|
|
|
5921
6109
|
return /\.module\.(css|scss)$/i.test(path);
|
|
5922
6110
|
}
|
|
5923
6111
|
function legacyBrandFile(projectRoot) {
|
|
5924
|
-
const dir =
|
|
6112
|
+
const dir = join33(projectRoot, ".jig", "tokens");
|
|
5925
6113
|
if (!existsSync19(dir)) return void 0;
|
|
5926
6114
|
const found = readdirSync12(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
5927
6115
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
@@ -5953,7 +6141,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
5953
6141
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
5954
6142
|
);
|
|
5955
6143
|
function wireBarrel(absPath, barrelImport) {
|
|
5956
|
-
const content = existsSync19(absPath) ?
|
|
6144
|
+
const content = existsSync19(absPath) ? readFileSync28(absPath, "utf8") : "";
|
|
5957
6145
|
const line = `@import "${barrelImport}";`;
|
|
5958
6146
|
if (content.includes(line)) return { status: "already-present" };
|
|
5959
6147
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -5969,7 +6157,7 @@ function wireBarrel(absPath, barrelImport) {
|
|
|
5969
6157
|
|
|
5970
6158
|
` : `${line}
|
|
5971
6159
|
`);
|
|
5972
|
-
mkdirSync6(
|
|
6160
|
+
mkdirSync6(dirname8(absPath), { recursive: true });
|
|
5973
6161
|
writeFileSync4(next === content ? absPath : absPath, next, "utf8");
|
|
5974
6162
|
return { status: "wired" };
|
|
5975
6163
|
}
|
|
@@ -6048,7 +6236,7 @@ async function init(opts) {
|
|
|
6048
6236
|
else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
|
|
6049
6237
|
}
|
|
6050
6238
|
if (opts.yes) {
|
|
6051
|
-
const declared2 = readConfiguredSurfaces(
|
|
6239
|
+
const declared2 = readConfiguredSurfaces(join33(opts.projectRoot, "jig.config.json"));
|
|
6052
6240
|
const inEffect = declared2 ?? surfaces;
|
|
6053
6241
|
const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
|
|
6054
6242
|
log(
|
|
@@ -6067,7 +6255,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
|
|
|
6067
6255
|
const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
|
|
6068
6256
|
const projectSlug = deriveProjectSlug(opts.projectRoot);
|
|
6069
6257
|
const configRelPath = "jig.config.json";
|
|
6070
|
-
const configAbsPath =
|
|
6258
|
+
const configAbsPath = join33(opts.projectRoot, configRelPath);
|
|
6071
6259
|
const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
|
|
6072
6260
|
if (configuredBrand.refused) {
|
|
6073
6261
|
log(
|
|
@@ -6101,7 +6289,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
|
|
|
6101
6289
|
}
|
|
6102
6290
|
}
|
|
6103
6291
|
const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
|
|
6104
|
-
const brandAbsPath =
|
|
6292
|
+
const brandAbsPath = join33(opts.projectRoot, ...brandRelPath.split("/"));
|
|
6105
6293
|
const tokensRelDir = brandRelPath.split("/").slice(0, -1);
|
|
6106
6294
|
log(
|
|
6107
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.")
|
|
@@ -6134,15 +6322,15 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6134
6322
|
if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
|
|
6135
6323
|
}
|
|
6136
6324
|
if (brandAction === "written") {
|
|
6137
|
-
const defaultCssRaw =
|
|
6325
|
+
const defaultCssRaw = readFileSync28(join33(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
6138
6326
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
6139
|
-
mkdirSync6(
|
|
6327
|
+
mkdirSync6(dirname8(brandAbsPath), { recursive: true });
|
|
6140
6328
|
writeFileSync4(brandAbsPath, content, "utf8");
|
|
6141
6329
|
files[brandRelPath] = checksum(content);
|
|
6142
6330
|
}
|
|
6143
6331
|
if (relocateTo && configAction !== "written") {
|
|
6144
6332
|
try {
|
|
6145
|
-
const existing = JSON.parse(
|
|
6333
|
+
const existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
6146
6334
|
const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
|
|
6147
6335
|
`;
|
|
6148
6336
|
writeFileSync4(configAbsPath, content, "utf8");
|
|
@@ -6161,12 +6349,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6161
6349
|
} else if (configAction === "merged") {
|
|
6162
6350
|
let existing = {};
|
|
6163
6351
|
try {
|
|
6164
|
-
existing = JSON.parse(
|
|
6352
|
+
existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
|
|
6165
6353
|
} catch {
|
|
6166
6354
|
existing = {};
|
|
6167
6355
|
}
|
|
6168
6356
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
6169
|
-
const brandStillResolves = existingBrand ? existsSync19(
|
|
6357
|
+
const brandStillResolves = existingBrand ? existsSync19(join33(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
6170
6358
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
6171
6359
|
const merged = {
|
|
6172
6360
|
...existing,
|
|
@@ -6183,12 +6371,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6183
6371
|
}
|
|
6184
6372
|
const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
|
|
6185
6373
|
const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
|
|
6186
|
-
const wiringBrandAbsPath =
|
|
6374
|
+
const wiringBrandAbsPath = join33(opts.projectRoot, ...effectiveConfig.brand.split("/"));
|
|
6187
6375
|
const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
|
|
6188
6376
|
const modeAbsPaths = {};
|
|
6189
6377
|
for (const mode of declaredModes) {
|
|
6190
6378
|
const modeFileName = `mode.${mode}.css`;
|
|
6191
|
-
const modeAbsPath2 =
|
|
6379
|
+
const modeAbsPath2 = join33(opts.projectRoot, ...tokensRelDir, modeFileName);
|
|
6192
6380
|
modeAbsPaths[mode] = modeAbsPath2;
|
|
6193
6381
|
const modeRelPath = relKey(...tokensRelDir, modeFileName);
|
|
6194
6382
|
const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
|
|
@@ -6196,9 +6384,9 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6196
6384
|
if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
|
|
6197
6385
|
else if (modeState.modified) modeAction = "skipped-edited";
|
|
6198
6386
|
if (modeAction === "written") {
|
|
6199
|
-
const modeSourceRaw =
|
|
6387
|
+
const modeSourceRaw = readFileSync28(join33(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
6200
6388
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
6201
|
-
mkdirSync6(
|
|
6389
|
+
mkdirSync6(dirname8(modeAbsPath2), { recursive: true });
|
|
6202
6390
|
writeFileSync4(modeAbsPath2, content, "utf8");
|
|
6203
6391
|
files[modeRelPath] = checksum(content);
|
|
6204
6392
|
} else {
|
|
@@ -6211,20 +6399,20 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6211
6399
|
const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
|
|
6212
6400
|
for (const mode of declaredModes) {
|
|
6213
6401
|
const rel = relKey(...tokensRelDir, barrelFor(mode));
|
|
6214
|
-
const abs =
|
|
6402
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
|
|
6215
6403
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6216
6404
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
6217
6405
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
6218
6406
|
continue;
|
|
6219
6407
|
}
|
|
6220
6408
|
const content = barrelBody(barrelFor(mode), brandFileOnly, mode, opts.version);
|
|
6221
|
-
mkdirSync6(
|
|
6409
|
+
mkdirSync6(dirname8(abs), { recursive: true });
|
|
6222
6410
|
writeFileSync4(abs, content, "utf8");
|
|
6223
6411
|
files[rel] = checksum(content);
|
|
6224
6412
|
}
|
|
6225
6413
|
if (multiMode) {
|
|
6226
6414
|
const rel = relKey(...tokensRelDir, "theme.css");
|
|
6227
|
-
const abs =
|
|
6415
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
6228
6416
|
if (existsSync19(abs)) {
|
|
6229
6417
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6230
6418
|
if (state.tracked && !state.modified) {
|
|
@@ -6240,13 +6428,13 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6240
6428
|
const declared2 = declaredTokenNames(
|
|
6241
6429
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
6242
6430
|
try {
|
|
6243
|
-
return
|
|
6431
|
+
return readFileSync28(f, "utf8");
|
|
6244
6432
|
} catch {
|
|
6245
6433
|
return "";
|
|
6246
6434
|
}
|
|
6247
6435
|
}).join("\n")
|
|
6248
6436
|
);
|
|
6249
|
-
const aliasable =
|
|
6437
|
+
const aliasable = compiledAliases(opts.projectRoot, declared2) ?? listedAliases(declared2);
|
|
6250
6438
|
const rel = relKey(...tokensRelDir, "utilities.css");
|
|
6251
6439
|
if (aliasable.length === 0) {
|
|
6252
6440
|
log(`
|
|
@@ -6265,19 +6453,19 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6265
6453
|
log(` This changes how components are written. Both styles are correct.`);
|
|
6266
6454
|
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
6267
6455
|
if (answer === "y" || answer === "yes") {
|
|
6268
|
-
const abs =
|
|
6456
|
+
const abs = join33(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
6269
6457
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6270
6458
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
6271
6459
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
6272
6460
|
} else {
|
|
6273
6461
|
const content = utilitiesBody(aliasable, opts.version);
|
|
6274
|
-
mkdirSync6(
|
|
6462
|
+
mkdirSync6(dirname8(abs), { recursive: true });
|
|
6275
6463
|
writeFileSync4(abs, content, "utf8");
|
|
6276
6464
|
files[rel] = checksum(content);
|
|
6277
6465
|
const target = findWireTarget(detection);
|
|
6278
6466
|
const importPath = target ? relativeImportPath(
|
|
6279
|
-
|
|
6280
|
-
|
|
6467
|
+
dirname8(join33(opts.projectRoot, target)),
|
|
6468
|
+
join33(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
6281
6469
|
) : `./${rel}`;
|
|
6282
6470
|
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
6283
6471
|
log(` @import "tailwindcss";`);
|
|
@@ -6296,11 +6484,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6296
6484
|
writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
|
|
6297
6485
|
if (relocateTo && currentBrand) {
|
|
6298
6486
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
6299
|
-
const oldAbsDir =
|
|
6487
|
+
const oldAbsDir = join33(opts.projectRoot, ...oldDir);
|
|
6300
6488
|
if (existsSync19(oldAbsDir)) {
|
|
6301
6489
|
for (const name of readdirSync12(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
6302
6490
|
const rel = relKey(...oldDir, name);
|
|
6303
|
-
const abs =
|
|
6491
|
+
const abs = join33(oldAbsDir, name);
|
|
6304
6492
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
6305
6493
|
if (!state.tracked || state.modified) {
|
|
6306
6494
|
log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
|
|
@@ -6314,14 +6502,14 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
6314
6502
|
const wireTarget = findWireTarget(detection);
|
|
6315
6503
|
let wiring;
|
|
6316
6504
|
if (multiMode) {
|
|
6317
|
-
const bare2 =
|
|
6505
|
+
const bare2 = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
6318
6506
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
6319
|
-
const abs =
|
|
6507
|
+
const abs = join33(opts.projectRoot, file);
|
|
6320
6508
|
try {
|
|
6321
|
-
const before =
|
|
6509
|
+
const before = readFileSync28(abs, "utf8");
|
|
6322
6510
|
const after = before.split("\n").filter((l) => {
|
|
6323
6511
|
const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
|
|
6324
|
-
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(
|
|
6512
|
+
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname8(abs), m[1]) === bare2);
|
|
6325
6513
|
}).join("\n");
|
|
6326
6514
|
if (after !== before) {
|
|
6327
6515
|
writeFileSync4(abs, after, "utf8");
|
|
@@ -6340,21 +6528,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
|
|
|
6340
6528
|
snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
|
|
6341
6529
|
};
|
|
6342
6530
|
} else if (wireTarget) {
|
|
6343
|
-
const targetAbsDir =
|
|
6531
|
+
const targetAbsDir = dirname8(join33(opts.projectRoot, wireTarget));
|
|
6344
6532
|
const barrelImport = relativeImportPath(
|
|
6345
6533
|
targetAbsDir,
|
|
6346
|
-
|
|
6534
|
+
join33(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
6347
6535
|
);
|
|
6348
6536
|
const snippet = `@import "${barrelImport}";`;
|
|
6349
6537
|
try {
|
|
6350
6538
|
if (relocateTo && currentBrand) {
|
|
6351
|
-
const abs =
|
|
6539
|
+
const abs = join33(opts.projectRoot, wireTarget);
|
|
6352
6540
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
6353
|
-
const before = existsSync19(abs) ?
|
|
6541
|
+
const before = existsSync19(abs) ? readFileSync28(abs, "utf8") : "";
|
|
6354
6542
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
6355
6543
|
if (after !== before) writeFileSync4(abs, after, "utf8");
|
|
6356
6544
|
}
|
|
6357
|
-
const outcome = wireBarrel(
|
|
6545
|
+
const outcome = wireBarrel(join33(opts.projectRoot, wireTarget), barrelImport);
|
|
6358
6546
|
wiring = { target: wireTarget, status: outcome.status, snippet };
|
|
6359
6547
|
const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
|
|
6360
6548
|
log(`
|
|
@@ -6371,7 +6559,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6371
6559
|
} else {
|
|
6372
6560
|
const barrelImport = relativeImportPath(
|
|
6373
6561
|
opts.projectRoot,
|
|
6374
|
-
|
|
6562
|
+
join33(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
6375
6563
|
);
|
|
6376
6564
|
const snippet = `@import "${barrelImport}";`;
|
|
6377
6565
|
wiring = { target: null, status: "print-only", snippet };
|
|
@@ -6380,7 +6568,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6380
6568
|
if (pasteTargets.length > 0) {
|
|
6381
6569
|
log("Paste the block for whichever of these is your global stylesheet:");
|
|
6382
6570
|
for (const target of pasteTargets) {
|
|
6383
|
-
const dir =
|
|
6571
|
+
const dir = dirname8(join33(opts.projectRoot, target));
|
|
6384
6572
|
const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
|
|
6385
6573
|
@import "${relativeImportPath(dir, modeAbsPath)}";`;
|
|
6386
6574
|
log(`
|
|
@@ -6430,29 +6618,29 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
6430
6618
|
}
|
|
6431
6619
|
|
|
6432
6620
|
// src/commands/verdicts.ts
|
|
6433
|
-
import { execFileSync as
|
|
6434
|
-
import { existsSync as existsSync25, readFileSync as
|
|
6435
|
-
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";
|
|
6436
6624
|
|
|
6437
6625
|
// src/rules/citations.ts
|
|
6438
|
-
import { join as
|
|
6626
|
+
import { join as join34 } from "path";
|
|
6439
6627
|
function citableIds(packageRoot2) {
|
|
6440
|
-
const rulesDir =
|
|
6628
|
+
const rulesDir = join34(packageRoot2, "rules");
|
|
6441
6629
|
const ids = [
|
|
6442
|
-
...loadRules(rulesDir,
|
|
6630
|
+
...loadRules(rulesDir, join34(packageRoot2, "rules.index.json")).map((r) => r.id),
|
|
6443
6631
|
...loadSpecs(rulesDir).map((s) => s.id)
|
|
6444
6632
|
];
|
|
6445
6633
|
return [...new Set(ids)].sort();
|
|
6446
6634
|
}
|
|
6447
6635
|
|
|
6448
6636
|
// src/probe/check.ts
|
|
6449
|
-
import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as
|
|
6450
|
-
import { join as
|
|
6637
|
+
import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync32 } from "fs";
|
|
6638
|
+
import { join as join39 } from "path";
|
|
6451
6639
|
|
|
6452
6640
|
// src/probe/save.ts
|
|
6453
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync14, readFileSync as
|
|
6641
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync14, readFileSync as readFileSync31, writeFileSync as writeFileSync5 } from "fs";
|
|
6454
6642
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6455
|
-
import { join as
|
|
6643
|
+
import { join as join38, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
6456
6644
|
|
|
6457
6645
|
// src/probe/script.ts
|
|
6458
6646
|
var PROBE_VERSION = 9;
|
|
@@ -6739,7 +6927,7 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6739
6927
|
import { spawn } from "child_process";
|
|
6740
6928
|
import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync13, rmSync as rmSync3 } from "fs";
|
|
6741
6929
|
import { homedir as homedir2, tmpdir } from "os";
|
|
6742
|
-
import { join as
|
|
6930
|
+
import { join as join35 } from "path";
|
|
6743
6931
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
6744
6932
|
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
|
|
6745
6933
|
var CHROME_APPS = [
|
|
@@ -6751,14 +6939,14 @@ var CHROME_APPS = [
|
|
|
6751
6939
|
// Every Windows machine has Edge; Chrome is often installed per user.
|
|
6752
6940
|
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6753
6941
|
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6754
|
-
...process.env.LOCALAPPDATA ? [
|
|
6942
|
+
...process.env.LOCALAPPDATA ? [join35(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
|
|
6755
6943
|
];
|
|
6756
6944
|
function cachedBrowsers() {
|
|
6757
6945
|
const roots = [
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
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")
|
|
6762
6950
|
];
|
|
6763
6951
|
const found = [];
|
|
6764
6952
|
for (const root of roots) {
|
|
@@ -6778,7 +6966,7 @@ function cachedBrowsers() {
|
|
|
6778
6966
|
["chrome-win", "chrome.exe"],
|
|
6779
6967
|
["chrome-win64", "chrome.exe"]
|
|
6780
6968
|
]) {
|
|
6781
|
-
const candidate =
|
|
6969
|
+
const candidate = join35(root, entry, ...rel);
|
|
6782
6970
|
if (existsSync20(candidate)) found.push(candidate);
|
|
6783
6971
|
}
|
|
6784
6972
|
}
|
|
@@ -6804,7 +6992,7 @@ function which(command) {
|
|
|
6804
6992
|
for (const dir of dirs) {
|
|
6805
6993
|
if (!dir) continue;
|
|
6806
6994
|
for (const ext of exts) {
|
|
6807
|
-
const candidate =
|
|
6995
|
+
const candidate = join35(dir, command + ext);
|
|
6808
6996
|
if (existsSync20(candidate)) return candidate;
|
|
6809
6997
|
}
|
|
6810
6998
|
}
|
|
@@ -6817,7 +7005,7 @@ async function runProbe(opts) {
|
|
|
6817
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."
|
|
6818
7006
|
);
|
|
6819
7007
|
}
|
|
6820
|
-
const profile = mkdtempSync(
|
|
7008
|
+
const profile = mkdtempSync(join35(tmpdir(), "jig-probe-"));
|
|
6821
7009
|
const child = spawn(chrome, [
|
|
6822
7010
|
"--headless=new",
|
|
6823
7011
|
"--remote-debugging-port=0",
|
|
@@ -6921,8 +7109,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
|
6921
7109
|
|
|
6922
7110
|
// src/probe/serve.ts
|
|
6923
7111
|
import { createServer } from "http";
|
|
6924
|
-
import { existsSync as existsSync21, readFileSync as
|
|
6925
|
-
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";
|
|
6926
7114
|
var TYPES = {
|
|
6927
7115
|
".html": "text/html; charset=utf-8",
|
|
6928
7116
|
".htm": "text/html; charset=utf-8",
|
|
@@ -6949,19 +7137,19 @@ async function serveDirectory(dir) {
|
|
|
6949
7137
|
const root = resolve3(dir);
|
|
6950
7138
|
const server = createServer((req, res) => {
|
|
6951
7139
|
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6952
|
-
let file = resolve3(
|
|
7140
|
+
let file = resolve3(join36(root, path));
|
|
6953
7141
|
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6954
7142
|
res.writeHead(403).end();
|
|
6955
7143
|
return;
|
|
6956
7144
|
}
|
|
6957
|
-
if (existsSync21(file) && statSync3(file).isDirectory()) file =
|
|
7145
|
+
if (existsSync21(file) && statSync3(file).isDirectory()) file = join36(file, "index.html");
|
|
6958
7146
|
else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
|
|
6959
7147
|
if (!existsSync21(file)) {
|
|
6960
7148
|
res.writeHead(404).end();
|
|
6961
7149
|
return;
|
|
6962
7150
|
}
|
|
6963
7151
|
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6964
|
-
res.end(
|
|
7152
|
+
res.end(readFileSync29(file));
|
|
6965
7153
|
});
|
|
6966
7154
|
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6967
7155
|
const address = server.address();
|
|
@@ -6973,8 +7161,8 @@ async function serveDirectory(dir) {
|
|
|
6973
7161
|
}
|
|
6974
7162
|
|
|
6975
7163
|
// src/probe/switches.ts
|
|
6976
|
-
import { readFileSync as
|
|
6977
|
-
import { join as
|
|
7164
|
+
import { readFileSync as readFileSync30 } from "fs";
|
|
7165
|
+
import { join as join37 } from "path";
|
|
6978
7166
|
var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
|
|
6979
7167
|
function declaredSwitches(projectRoot) {
|
|
6980
7168
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6982,7 +7170,7 @@ function declaredSwitches(projectRoot) {
|
|
|
6982
7170
|
if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
|
|
6983
7171
|
let text;
|
|
6984
7172
|
try {
|
|
6985
|
-
text =
|
|
7173
|
+
text = readFileSync30(join37(projectRoot, file), "utf8");
|
|
6986
7174
|
} catch {
|
|
6987
7175
|
continue;
|
|
6988
7176
|
}
|
|
@@ -7017,11 +7205,11 @@ function saveProbe(opts) {
|
|
|
7017
7205
|
}
|
|
7018
7206
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
7019
7207
|
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
7020
|
-
probe.pageChecksum = checksum(
|
|
7208
|
+
probe.pageChecksum = checksum(readFileSync31(page, "utf8"));
|
|
7021
7209
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7022
|
-
const dir =
|
|
7210
|
+
const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7023
7211
|
mkdirSync7(dir, { recursive: true });
|
|
7024
|
-
const path =
|
|
7212
|
+
const path = join38(dir, `probe-${width}.json`);
|
|
7025
7213
|
writeFileSync5(path, JSON.stringify(probe), "utf8");
|
|
7026
7214
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
7027
7215
|
}
|
|
@@ -7070,11 +7258,11 @@ async function withPageUrl(projectRoot, abs, serve, use) {
|
|
|
7070
7258
|
}
|
|
7071
7259
|
}
|
|
7072
7260
|
function recordedPage(projectRoot, surface) {
|
|
7073
|
-
const dir =
|
|
7261
|
+
const dir = join38(projectRoot, ".jig", "critique", surface);
|
|
7074
7262
|
if (!existsSync22(dir)) return void 0;
|
|
7075
7263
|
for (const f of readdirSync14(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
|
|
7076
7264
|
try {
|
|
7077
|
-
const probe = JSON.parse(
|
|
7265
|
+
const probe = JSON.parse(readFileSync31(join38(dir, f), "utf8"));
|
|
7078
7266
|
if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
|
|
7079
7267
|
} catch {
|
|
7080
7268
|
}
|
|
@@ -7082,11 +7270,11 @@ function recordedPage(projectRoot, surface) {
|
|
|
7082
7270
|
return void 0;
|
|
7083
7271
|
}
|
|
7084
7272
|
async function ensureProbes(opts) {
|
|
7085
|
-
const dir =
|
|
7273
|
+
const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7086
7274
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
7087
7275
|
let current;
|
|
7088
7276
|
try {
|
|
7089
|
-
current = checksum(
|
|
7277
|
+
current = checksum(readFileSync31(abs, "utf8"));
|
|
7090
7278
|
} catch {
|
|
7091
7279
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
7092
7280
|
}
|
|
@@ -7094,7 +7282,7 @@ async function ensureProbes(opts) {
|
|
|
7094
7282
|
const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
|
|
7095
7283
|
const missing = widths.filter((width) => {
|
|
7096
7284
|
try {
|
|
7097
|
-
const probe = JSON.parse(
|
|
7285
|
+
const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
|
|
7098
7286
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
7099
7287
|
} catch {
|
|
7100
7288
|
return true;
|
|
@@ -7105,7 +7293,7 @@ async function ensureProbes(opts) {
|
|
|
7105
7293
|
let serve;
|
|
7106
7294
|
for (const width of widths) {
|
|
7107
7295
|
try {
|
|
7108
|
-
const probe = JSON.parse(
|
|
7296
|
+
const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
|
|
7109
7297
|
if (probe.serveRoot) {
|
|
7110
7298
|
serve = probe.serveRoot;
|
|
7111
7299
|
break;
|
|
@@ -7121,9 +7309,9 @@ async function ensureProbes(opts) {
|
|
|
7121
7309
|
return { recorded: missing };
|
|
7122
7310
|
}
|
|
7123
7311
|
function critiquedSurfaces(projectRoot) {
|
|
7124
|
-
const root =
|
|
7312
|
+
const root = join38(projectRoot, ".jig", "critique");
|
|
7125
7313
|
if (!existsSync22(root)) return [];
|
|
7126
|
-
return readdirSync14(root).filter((surface) => !surface.startsWith("_") && (existsSync22(
|
|
7314
|
+
return readdirSync14(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join38(root, surface, "screen.json")) || existsSync22(join38(root, surface, "code.json"))));
|
|
7127
7315
|
}
|
|
7128
7316
|
|
|
7129
7317
|
// src/probe/check.ts
|
|
@@ -7135,7 +7323,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
7135
7323
|
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
7136
7324
|
let current;
|
|
7137
7325
|
try {
|
|
7138
|
-
current =
|
|
7326
|
+
current = readFileSync32(page, "utf8");
|
|
7139
7327
|
} catch {
|
|
7140
7328
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
7141
7329
|
}
|
|
@@ -7149,7 +7337,7 @@ function readProbes(projectRoot, dir, errors) {
|
|
|
7149
7337
|
const probes = [];
|
|
7150
7338
|
for (const f of readdirSync15(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
7151
7339
|
try {
|
|
7152
|
-
const p = JSON.parse(
|
|
7340
|
+
const p = JSON.parse(readFileSync32(join39(dir, f), "utf8"));
|
|
7153
7341
|
if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
|
|
7154
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\`.`);
|
|
7155
7343
|
continue;
|
|
@@ -7274,18 +7462,18 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
|
|
|
7274
7462
|
|
|
7275
7463
|
// src/check/spec-shape.ts
|
|
7276
7464
|
import { existsSync as existsSync24, readdirSync as readdirSync16, statSync as statSync4 } from "fs";
|
|
7277
|
-
import { join as
|
|
7465
|
+
import { join as join40 } from "path";
|
|
7278
7466
|
function newestSpec(projectRoot) {
|
|
7279
|
-
const dir =
|
|
7467
|
+
const dir = join40(projectRoot, ".jig", "specs");
|
|
7280
7468
|
if (!existsSync24(dir)) return void 0;
|
|
7281
7469
|
const files = readdirSync16(dir).filter((f) => f.endsWith(".md"));
|
|
7282
7470
|
if (files.length === 0) return void 0;
|
|
7283
|
-
const newest = files.map((f) => ({ f, at: statSync4(
|
|
7284
|
-
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)) };
|
|
7285
7473
|
}
|
|
7286
7474
|
function specFor(projectRoot, surface) {
|
|
7287
7475
|
if (surface) {
|
|
7288
|
-
const path =
|
|
7476
|
+
const path = join40(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7289
7477
|
if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
|
|
7290
7478
|
}
|
|
7291
7479
|
return newestSpec(projectRoot);
|
|
@@ -7443,14 +7631,14 @@ function reasonProblems(file, judged, errors) {
|
|
|
7443
7631
|
function readJson(path, errors) {
|
|
7444
7632
|
if (!existsSync25(path)) return null;
|
|
7445
7633
|
try {
|
|
7446
|
-
return JSON.parse(
|
|
7634
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
7447
7635
|
} catch (e) {
|
|
7448
7636
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
7449
7637
|
return {};
|
|
7450
7638
|
}
|
|
7451
7639
|
}
|
|
7452
7640
|
function specIndexable(projectRoot, surface) {
|
|
7453
|
-
const path =
|
|
7641
|
+
const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7454
7642
|
let front = "";
|
|
7455
7643
|
try {
|
|
7456
7644
|
front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
@@ -7463,7 +7651,7 @@ function specIndexable(projectRoot, surface) {
|
|
|
7463
7651
|
}
|
|
7464
7652
|
function projectMode(projectRoot) {
|
|
7465
7653
|
try {
|
|
7466
|
-
const config = JSON.parse(readText(
|
|
7654
|
+
const config = JSON.parse(readText(join41(projectRoot, "jig.config.json")));
|
|
7467
7655
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s?.mode).filter((m) => typeof m === "string"))];
|
|
7468
7656
|
return modes.length === 1 ? modes[0].toLowerCase() : void 0;
|
|
7469
7657
|
} catch {
|
|
@@ -7471,7 +7659,7 @@ function projectMode(projectRoot) {
|
|
|
7471
7659
|
}
|
|
7472
7660
|
}
|
|
7473
7661
|
function specNeedsNav(projectRoot, surface) {
|
|
7474
|
-
const path =
|
|
7662
|
+
const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7475
7663
|
if (!existsSync25(path)) return false;
|
|
7476
7664
|
const front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
7477
7665
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
@@ -7550,7 +7738,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
7550
7738
|
function checkDecisions(projectRoot, dir, errors) {
|
|
7551
7739
|
const required = decisionNames(projectRoot);
|
|
7552
7740
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
7553
|
-
const file = readJson(
|
|
7741
|
+
const file = readJson(join41(dir, "decisions.json"), errors);
|
|
7554
7742
|
if (!file) {
|
|
7555
7743
|
errors.push(
|
|
7556
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.`
|
|
@@ -7603,9 +7791,9 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7603
7791
|
if (unjudged.length === 0) return [];
|
|
7604
7792
|
const path = decisionsFile(projectRoot);
|
|
7605
7793
|
if (!path) return [];
|
|
7606
|
-
const git = (args) =>
|
|
7794
|
+
const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7607
7795
|
try {
|
|
7608
|
-
const verdictFile = relative4(projectRoot,
|
|
7796
|
+
const verdictFile = relative4(projectRoot, join41(dir, "decisions.json")).split(sep6).join("/");
|
|
7609
7797
|
if (git(["status", "--porcelain", "--", verdictFile])) return [];
|
|
7610
7798
|
const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
|
|
7611
7799
|
if (!judgedIn) return [];
|
|
@@ -7629,7 +7817,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7629
7817
|
function verifyVerdicts(opts) {
|
|
7630
7818
|
const root = opts.packageRoot ?? assetRoot();
|
|
7631
7819
|
const errors = [];
|
|
7632
|
-
const index = JSON.parse(
|
|
7820
|
+
const index = JSON.parse(readFileSync34(join41(root, "rules.index.json"), "utf8"));
|
|
7633
7821
|
const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
|
|
7634
7822
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
7635
7823
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -7638,9 +7826,9 @@ function verifyVerdicts(opts) {
|
|
|
7638
7826
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
7639
7827
|
]);
|
|
7640
7828
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
7641
|
-
const dir =
|
|
7642
|
-
const screenFile = readJson(
|
|
7643
|
-
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);
|
|
7644
7832
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
7645
7833
|
const decisionList = decisionNames(opts.projectRoot);
|
|
7646
7834
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
|
|
@@ -7670,7 +7858,7 @@ function verifyVerdicts(opts) {
|
|
|
7670
7858
|
const measured = [];
|
|
7671
7859
|
let specFront = "";
|
|
7672
7860
|
try {
|
|
7673
|
-
specFront = readText(
|
|
7861
|
+
specFront = readText(join41(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
7674
7862
|
} catch {
|
|
7675
7863
|
}
|
|
7676
7864
|
if (specIndexableField(specFront) === "unreadable") {
|
|
@@ -7696,7 +7884,7 @@ function verifyVerdicts(opts) {
|
|
|
7696
7884
|
return { ok: errors.length === 0, errors, screen, code, decisions, rendered, measured, line, ...previous ? { previous } : {} };
|
|
7697
7885
|
}
|
|
7698
7886
|
function previousFindings(projectRoot, dir) {
|
|
7699
|
-
const git = (args) =>
|
|
7887
|
+
const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7700
7888
|
const rel = relative4(projectRoot, dir).split("\\").join("/");
|
|
7701
7889
|
const files = ["screen.json", "code.json", "decisions.json"];
|
|
7702
7890
|
const paths = files.map((f) => `${rel}/${f}`);
|
|
@@ -7715,7 +7903,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
7715
7903
|
const out = /* @__PURE__ */ new Map();
|
|
7716
7904
|
let text;
|
|
7717
7905
|
try {
|
|
7718
|
-
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(
|
|
7906
|
+
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join41(dir, f));
|
|
7719
7907
|
} catch {
|
|
7720
7908
|
return out;
|
|
7721
7909
|
}
|
|
@@ -7797,17 +7985,17 @@ function sourceChanges(git, from, to) {
|
|
|
7797
7985
|
}
|
|
7798
7986
|
|
|
7799
7987
|
// src/commands/gate.ts
|
|
7800
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync18, readFileSync as
|
|
7988
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync18, readFileSync as readFileSync38, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7801
7989
|
import { createHash as createHash3 } from "crypto";
|
|
7802
|
-
import { join as
|
|
7990
|
+
import { join as join46 } from "path";
|
|
7803
7991
|
|
|
7804
7992
|
// src/check/mockup-drawing.ts
|
|
7805
|
-
import { join as
|
|
7993
|
+
import { join as join42 } from "path";
|
|
7806
7994
|
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
7807
7995
|
function mockupDrawingProblems(root, specBody, at) {
|
|
7808
7996
|
let html;
|
|
7809
7997
|
try {
|
|
7810
|
-
html = readText(
|
|
7998
|
+
html = readText(join42(root, at));
|
|
7811
7999
|
} catch {
|
|
7812
8000
|
return [];
|
|
7813
8001
|
}
|
|
@@ -7868,7 +8056,7 @@ function frameRegionProblems(frames, specBody, at) {
|
|
|
7868
8056
|
function approvedDrawingProblems(root, specBody, at) {
|
|
7869
8057
|
let html;
|
|
7870
8058
|
try {
|
|
7871
|
-
html = readText(
|
|
8059
|
+
html = readText(join42(root, at));
|
|
7872
8060
|
} catch {
|
|
7873
8061
|
return [];
|
|
7874
8062
|
}
|
|
@@ -7971,15 +8159,15 @@ function sizeBlock2(sizes, size) {
|
|
|
7971
8159
|
}
|
|
7972
8160
|
|
|
7973
8161
|
// src/commands/gate.ts
|
|
7974
|
-
import { execFileSync as
|
|
8162
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
7975
8163
|
|
|
7976
8164
|
// src/commands/ship.ts
|
|
7977
|
-
import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as
|
|
7978
|
-
import { join as
|
|
8165
|
+
import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync36 } from "fs";
|
|
8166
|
+
import { join as join44 } from "path";
|
|
7979
8167
|
|
|
7980
8168
|
// src/commands/seo.ts
|
|
7981
|
-
import { readFileSync as
|
|
7982
|
-
import { join as
|
|
8169
|
+
import { readFileSync as readFileSync35 } from "fs";
|
|
8170
|
+
import { join as join43 } from "path";
|
|
7983
8171
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
7984
8172
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
7985
8173
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -8020,7 +8208,7 @@ function seo(opts) {
|
|
|
8020
8208
|
for (const file of files) {
|
|
8021
8209
|
let raw;
|
|
8022
8210
|
try {
|
|
8023
|
-
raw =
|
|
8211
|
+
raw = readFileSync35(join43(root, file), "utf8");
|
|
8024
8212
|
} catch {
|
|
8025
8213
|
continue;
|
|
8026
8214
|
}
|
|
@@ -8085,7 +8273,7 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
8085
8273
|
const own = specCritique(specBody);
|
|
8086
8274
|
if (own) return own === "at-ship";
|
|
8087
8275
|
try {
|
|
8088
|
-
return JSON.parse(
|
|
8276
|
+
return JSON.parse(readFileSync36(join44(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
|
|
8089
8277
|
} catch {
|
|
8090
8278
|
return false;
|
|
8091
8279
|
}
|
|
@@ -8093,14 +8281,14 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
8093
8281
|
function tweakDeferred(dir) {
|
|
8094
8282
|
let t;
|
|
8095
8283
|
try {
|
|
8096
|
-
t = JSON.parse(
|
|
8284
|
+
t = JSON.parse(readFileSync36(join44(dir, "tweak.json"), "utf8"));
|
|
8097
8285
|
} catch {
|
|
8098
8286
|
return false;
|
|
8099
8287
|
}
|
|
8100
8288
|
if (t.deferred === void 0 || t.deferred === false) return false;
|
|
8101
8289
|
let judged;
|
|
8102
8290
|
try {
|
|
8103
|
-
judged = JSON.parse(
|
|
8291
|
+
judged = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).tweak;
|
|
8104
8292
|
} catch {
|
|
8105
8293
|
}
|
|
8106
8294
|
return judged !== t.at;
|
|
@@ -8111,14 +8299,14 @@ function pageChecksum(html) {
|
|
|
8111
8299
|
function changedSinceLock(projectRoot, dir) {
|
|
8112
8300
|
let lock;
|
|
8113
8301
|
try {
|
|
8114
|
-
lock = JSON.parse(
|
|
8302
|
+
lock = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8"));
|
|
8115
8303
|
} catch {
|
|
8116
8304
|
return void 0;
|
|
8117
8305
|
}
|
|
8118
8306
|
const file = lock.page?.file;
|
|
8119
8307
|
if (!file || !lock.page?.checksum) return void 0;
|
|
8120
8308
|
try {
|
|
8121
|
-
const html =
|
|
8309
|
+
const html = readFileSync36(join44(projectRoot, file), "utf8");
|
|
8122
8310
|
const seen = [pageChecksum(html), checksum(html)];
|
|
8123
8311
|
return seen.includes(lock.page.checksum) ? void 0 : file;
|
|
8124
8312
|
} catch {
|
|
@@ -8130,7 +8318,7 @@ function supersededBy(front) {
|
|
|
8130
8318
|
return named ? named : void 0;
|
|
8131
8319
|
}
|
|
8132
8320
|
function pageStatus(projectRoot, surface) {
|
|
8133
|
-
const spec = readText(
|
|
8321
|
+
const spec = readText(join44(projectRoot, ".jig", "specs", `${surface}.spec.md`));
|
|
8134
8322
|
const front = spec.split(/^---\s*$/m)[1] ?? "";
|
|
8135
8323
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
|
|
8136
8324
|
return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
|
|
@@ -8138,10 +8326,10 @@ function pageStatus(projectRoot, surface) {
|
|
|
8138
8326
|
const successor = supersededBy(front);
|
|
8139
8327
|
if (successor !== void 0) {
|
|
8140
8328
|
const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
|
|
8141
|
-
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` };
|
|
8142
8330
|
}
|
|
8143
|
-
const dir =
|
|
8144
|
-
if (!existsSync26(
|
|
8331
|
+
const dir = join44(projectRoot, ".jig", "critique", surface);
|
|
8332
|
+
if (!existsSync26(join44(dir, "screen.json")) && !existsSync26(join44(dir, "code.json"))) {
|
|
8145
8333
|
return { surface, state: "never", detail: "never critiqued" };
|
|
8146
8334
|
}
|
|
8147
8335
|
if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
|
|
@@ -8151,7 +8339,7 @@ function pageStatus(projectRoot, surface) {
|
|
|
8151
8339
|
const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
|
|
8152
8340
|
let judgedPage = false;
|
|
8153
8341
|
try {
|
|
8154
|
-
judgedPage = !!JSON.parse(
|
|
8342
|
+
judgedPage = !!JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).page?.checksum;
|
|
8155
8343
|
} catch {
|
|
8156
8344
|
}
|
|
8157
8345
|
if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
|
|
@@ -8171,7 +8359,7 @@ function ship(opts) {
|
|
|
8171
8359
|
const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
8172
8360
|
const seoResult = seo({ projectRoot: root });
|
|
8173
8361
|
const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
|
|
8174
|
-
const specsDir =
|
|
8362
|
+
const specsDir = join44(root, ".jig", "specs");
|
|
8175
8363
|
const surfaces = existsSync26(specsDir) ? readdirSync17(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
|
|
8176
8364
|
const pages = surfaces.map((s) => pageStatus(root, s));
|
|
8177
8365
|
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
|
|
@@ -8193,9 +8381,9 @@ function ship(opts) {
|
|
|
8193
8381
|
}
|
|
8194
8382
|
|
|
8195
8383
|
// src/check/spec-checked.ts
|
|
8196
|
-
import { existsSync as existsSync27, readFileSync as
|
|
8384
|
+
import { existsSync as existsSync27, readFileSync as readFileSync37 } from "fs";
|
|
8197
8385
|
import { createHash as createHash2 } from "crypto";
|
|
8198
|
-
import { join as
|
|
8386
|
+
import { join as join45 } from "path";
|
|
8199
8387
|
|
|
8200
8388
|
// src/check/owner-word.ts
|
|
8201
8389
|
var SAYS = {
|
|
@@ -8280,7 +8468,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8280
8468
|
const recordPath = `.jig/specs/${spec.slug}.checked.json`;
|
|
8281
8469
|
let record;
|
|
8282
8470
|
try {
|
|
8283
|
-
record = JSON.parse(
|
|
8471
|
+
record = JSON.parse(readFileSync37(join45(root, recordPath), "utf8"));
|
|
8284
8472
|
} catch {
|
|
8285
8473
|
problems.push(
|
|
8286
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.`
|
|
@@ -8309,10 +8497,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8309
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.`);
|
|
8310
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) => {
|
|
8311
8499
|
const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
|
|
8312
|
-
if (!file || !existsSync27(
|
|
8500
|
+
if (!file || !existsSync27(join45(root, file))) return true;
|
|
8313
8501
|
if (!line) return false;
|
|
8314
8502
|
try {
|
|
8315
|
-
return Number(line) >
|
|
8503
|
+
return Number(line) > readFileSync37(join45(root, file), "utf8").split("\n").length;
|
|
8316
8504
|
} catch {
|
|
8317
8505
|
return true;
|
|
8318
8506
|
}
|
|
@@ -8329,7 +8517,7 @@ function lastJigInvocation(transcriptPath) {
|
|
|
8329
8517
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8330
8518
|
let text;
|
|
8331
8519
|
try {
|
|
8332
|
-
text =
|
|
8520
|
+
text = readFileSync38(transcriptPath, "utf8");
|
|
8333
8521
|
} catch {
|
|
8334
8522
|
return void 0;
|
|
8335
8523
|
}
|
|
@@ -8357,7 +8545,7 @@ function ownerWords(transcriptPath) {
|
|
|
8357
8545
|
if (!transcriptPath || !existsSync28(transcriptPath)) return "";
|
|
8358
8546
|
const said = [];
|
|
8359
8547
|
try {
|
|
8360
|
-
for (const line of
|
|
8548
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8361
8549
|
if (!line.includes('"user"')) continue;
|
|
8362
8550
|
let entry;
|
|
8363
8551
|
try {
|
|
@@ -8379,7 +8567,7 @@ function ownerWords(transcriptPath) {
|
|
|
8379
8567
|
}
|
|
8380
8568
|
function namedSpec(root, surface) {
|
|
8381
8569
|
if (!surface) return specFor(root, void 0);
|
|
8382
|
-
return existsSync28(
|
|
8570
|
+
return existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
|
|
8383
8571
|
}
|
|
8384
8572
|
function isConfirmed(body) {
|
|
8385
8573
|
return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
|
|
@@ -8388,7 +8576,7 @@ function ownerReplyAfter(transcriptPath, asked) {
|
|
|
8388
8576
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8389
8577
|
const turns = [];
|
|
8390
8578
|
try {
|
|
8391
|
-
for (const line of
|
|
8579
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8392
8580
|
let entry;
|
|
8393
8581
|
try {
|
|
8394
8582
|
entry = JSON.parse(line);
|
|
@@ -8416,7 +8604,7 @@ function lastAssistantText(transcriptPath) {
|
|
|
8416
8604
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8417
8605
|
let found;
|
|
8418
8606
|
try {
|
|
8419
|
-
for (const line of
|
|
8607
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8420
8608
|
if (!line.includes('"assistant"')) continue;
|
|
8421
8609
|
let entry;
|
|
8422
8610
|
try {
|
|
@@ -8442,7 +8630,7 @@ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak
|
|
|
8442
8630
|
function sessionStart(transcriptPath) {
|
|
8443
8631
|
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8444
8632
|
try {
|
|
8445
|
-
for (const line of
|
|
8633
|
+
for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
|
|
8446
8634
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
8447
8635
|
if (m) {
|
|
8448
8636
|
const t = Date.parse(m[1]);
|
|
@@ -8457,29 +8645,29 @@ function verdictsMtime(dir) {
|
|
|
8457
8645
|
let newest = 0;
|
|
8458
8646
|
for (const name of VERDICT_FILES) {
|
|
8459
8647
|
try {
|
|
8460
|
-
newest = Math.max(newest, statSync5(
|
|
8648
|
+
newest = Math.max(newest, statSync5(join46(dir, name)).mtimeMs);
|
|
8461
8649
|
} catch {
|
|
8462
8650
|
}
|
|
8463
8651
|
}
|
|
8464
8652
|
return newest;
|
|
8465
8653
|
}
|
|
8466
8654
|
function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
8467
|
-
const critiqueDir =
|
|
8655
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8468
8656
|
if (!existsSync28(critiqueDir)) return [];
|
|
8469
8657
|
const all = readdirSync18(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
8470
8658
|
const start = sessionStart(transcriptPath);
|
|
8471
8659
|
if (start === void 0) return all;
|
|
8472
8660
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
8473
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
8661
|
+
return all.filter((s) => s === current || verdictsMtime(join46(critiqueDir, s)) >= start - 1e3);
|
|
8474
8662
|
}
|
|
8475
8663
|
function surfacesToProbe(root, input) {
|
|
8476
8664
|
const invocation = lastJigInvocation(input.transcript_path);
|
|
8477
8665
|
return surfacesInPlay(root, invocation?.command, input.transcript_path, invocation?.surface);
|
|
8478
8666
|
}
|
|
8479
8667
|
function fileAtSessionStart(root, path, start) {
|
|
8480
|
-
const git = (args) =>
|
|
8668
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8481
8669
|
try {
|
|
8482
|
-
if (start !== void 0 && statSync5(
|
|
8670
|
+
if (start !== void 0 && statSync5(join46(root, path)).mtimeMs < start - 1e3) return readText(join46(root, path));
|
|
8483
8671
|
} catch {
|
|
8484
8672
|
}
|
|
8485
8673
|
try {
|
|
@@ -8490,14 +8678,14 @@ function fileAtSessionStart(root, path, start) {
|
|
|
8490
8678
|
}
|
|
8491
8679
|
}
|
|
8492
8680
|
function changedOutsideRecords(root, start) {
|
|
8493
|
-
const git = (args) =>
|
|
8681
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8494
8682
|
try {
|
|
8495
8683
|
const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
|
|
8496
8684
|
if (!base) return [];
|
|
8497
8685
|
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
8498
8686
|
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
8499
8687
|
try {
|
|
8500
|
-
return start === void 0 || statSync5(
|
|
8688
|
+
return start === void 0 || statSync5(join46(root, f)).mtimeMs >= start - 1e3;
|
|
8501
8689
|
} catch {
|
|
8502
8690
|
return false;
|
|
8503
8691
|
}
|
|
@@ -8508,12 +8696,12 @@ function changedOutsideRecords(root, start) {
|
|
|
8508
8696
|
}
|
|
8509
8697
|
}
|
|
8510
8698
|
function drawingChangedAfterApproval(root, specPath, at) {
|
|
8511
|
-
const git = (args) =>
|
|
8699
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8512
8700
|
try {
|
|
8513
8701
|
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
8514
8702
|
if (!approvedIn) return void 0;
|
|
8515
8703
|
const then = lf(git(["show", `${approvedIn}:./${at}`]));
|
|
8516
|
-
return then !== readText(
|
|
8704
|
+
return then !== readText(join46(root, at)) ? approvedIn.slice(0, 7) : void 0;
|
|
8517
8705
|
} catch {
|
|
8518
8706
|
return void 0;
|
|
8519
8707
|
}
|
|
@@ -8525,7 +8713,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8525
8713
|
const found = decisionsFile(root);
|
|
8526
8714
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
8527
8715
|
else {
|
|
8528
|
-
const body = readText(
|
|
8716
|
+
const body = readText(join46(root, found));
|
|
8529
8717
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
8530
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.`");
|
|
8531
8719
|
}
|
|
@@ -8535,7 +8723,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8535
8723
|
if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
|
|
8536
8724
|
}
|
|
8537
8725
|
}
|
|
8538
|
-
if (command === "spec" && surface && !existsSync28(
|
|
8726
|
+
if (command === "spec" && surface && !existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`))) {
|
|
8539
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.`);
|
|
8540
8728
|
return problems;
|
|
8541
8729
|
}
|
|
@@ -8558,10 +8746,10 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8558
8746
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8559
8747
|
if (/^approved/i.test(mockup)) {
|
|
8560
8748
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
8561
|
-
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.`);
|
|
8562
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.`);
|
|
8563
8751
|
}
|
|
8564
|
-
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));
|
|
8565
8753
|
}
|
|
8566
8754
|
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
8567
8755
|
if ((command === "spec" || command === "make") && spec) {
|
|
@@ -8589,7 +8777,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8589
8777
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8590
8778
|
const mockup = mockupField(spec.body);
|
|
8591
8779
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8592
|
-
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))) {
|
|
8593
8781
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
8594
8782
|
if (changedAfter) {
|
|
8595
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.`);
|
|
@@ -8601,7 +8789,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8601
8789
|
if (found) {
|
|
8602
8790
|
let now = "";
|
|
8603
8791
|
try {
|
|
8604
|
-
now = readText(
|
|
8792
|
+
now = readText(join46(root, found));
|
|
8605
8793
|
} catch {
|
|
8606
8794
|
}
|
|
8607
8795
|
const then = fileAtSessionStart(root, found, start);
|
|
@@ -8615,13 +8803,13 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8615
8803
|
if (found) {
|
|
8616
8804
|
let now = "";
|
|
8617
8805
|
try {
|
|
8618
|
-
now = readText(
|
|
8806
|
+
now = readText(join46(root, found));
|
|
8619
8807
|
} catch {
|
|
8620
8808
|
}
|
|
8621
8809
|
const then = fileAtSessionStart(root, found, start);
|
|
8622
8810
|
if (now && now !== then) {
|
|
8623
8811
|
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
8624
|
-
const change = readTweak(
|
|
8812
|
+
const change = readTweak(join46(root, ".jig", "critique", spec.slug))?.change;
|
|
8625
8813
|
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
8626
8814
|
}
|
|
8627
8815
|
}
|
|
@@ -8630,7 +8818,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8630
8818
|
for (const file of modeFiles(root)) {
|
|
8631
8819
|
let now = "";
|
|
8632
8820
|
try {
|
|
8633
|
-
now = readText(
|
|
8821
|
+
now = readText(join46(root, file));
|
|
8634
8822
|
} catch {
|
|
8635
8823
|
continue;
|
|
8636
8824
|
}
|
|
@@ -8641,8 +8829,8 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8641
8829
|
}
|
|
8642
8830
|
}
|
|
8643
8831
|
if (command === "critique") {
|
|
8644
|
-
const dir =
|
|
8645
|
-
const surfaces = existsSync28(dir) ? readdirSync18(dir).filter((s) => existsSync28(
|
|
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"))) : [];
|
|
8646
8834
|
for (const surface2 of surfaces) {
|
|
8647
8835
|
const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
|
|
8648
8836
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -8653,7 +8841,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8653
8841
|
}
|
|
8654
8842
|
}
|
|
8655
8843
|
const own = specFor(root, surface)?.slug;
|
|
8656
|
-
if (own && surfaces.includes(own)) problems.push(...reportOmissions(
|
|
8844
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join46(dir, own), own));
|
|
8657
8845
|
if (surfaces.length === 0) {
|
|
8658
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.");
|
|
8659
8847
|
}
|
|
@@ -8666,7 +8854,7 @@ function drawingBeforeAsking(root, surface) {
|
|
|
8666
8854
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8667
8855
|
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8668
8856
|
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
8669
|
-
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 [];
|
|
8670
8858
|
const found = mockupDrawingProblems(root, spec.body, at);
|
|
8671
8859
|
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
8672
8860
|
}
|
|
@@ -8674,14 +8862,14 @@ function specChecked(root, spec, start, owner) {
|
|
|
8674
8862
|
const found = decisionsFile(root);
|
|
8675
8863
|
let decisions = "";
|
|
8676
8864
|
try {
|
|
8677
|
-
if (found) decisions = readText(
|
|
8865
|
+
if (found) decisions = readText(join46(root, found));
|
|
8678
8866
|
} catch {
|
|
8679
8867
|
}
|
|
8680
8868
|
return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
|
|
8681
8869
|
}
|
|
8682
8870
|
function modeFiles(root) {
|
|
8683
8871
|
try {
|
|
8684
|
-
const state = JSON.parse(
|
|
8872
|
+
const state = JSON.parse(readFileSync38(join46(root, ".jig", "state.json"), "utf8"));
|
|
8685
8873
|
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
8686
8874
|
} catch {
|
|
8687
8875
|
return [];
|
|
@@ -8690,7 +8878,7 @@ function modeFiles(root) {
|
|
|
8690
8878
|
function reportOmissions(dir, surface) {
|
|
8691
8879
|
let report2;
|
|
8692
8880
|
try {
|
|
8693
|
-
report2 = readText(
|
|
8881
|
+
report2 = readText(join46(dir, "REPORT.md")).toLowerCase();
|
|
8694
8882
|
} catch {
|
|
8695
8883
|
return [];
|
|
8696
8884
|
}
|
|
@@ -8698,7 +8886,7 @@ function reportOmissions(dir, surface) {
|
|
|
8698
8886
|
for (const f of VERDICT_FILES) {
|
|
8699
8887
|
let file;
|
|
8700
8888
|
try {
|
|
8701
|
-
file = JSON.parse(
|
|
8889
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
8702
8890
|
} catch {
|
|
8703
8891
|
continue;
|
|
8704
8892
|
}
|
|
@@ -8719,16 +8907,16 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8719
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\`.`);
|
|
8720
8908
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
8721
8909
|
const surface = spec.slug;
|
|
8722
|
-
const record = readTweak(
|
|
8910
|
+
const record = readTweak(join46(root, ".jig", "critique", surface));
|
|
8723
8911
|
if (!record) {
|
|
8724
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\`).`);
|
|
8725
8913
|
} else {
|
|
8726
8914
|
if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
|
|
8727
|
-
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)) {
|
|
8728
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.`);
|
|
8729
8917
|
}
|
|
8730
8918
|
try {
|
|
8731
|
-
const raw = JSON.parse(
|
|
8919
|
+
const raw = JSON.parse(readFileSync38(join46(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
|
|
8732
8920
|
const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
|
|
8733
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.`);
|
|
8734
8922
|
} catch {
|
|
@@ -8743,7 +8931,7 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8743
8931
|
}
|
|
8744
8932
|
return problems;
|
|
8745
8933
|
}
|
|
8746
|
-
const unjudged = unjudgedTweakIds(
|
|
8934
|
+
const unjudged = unjudgedTweakIds(join46(root, ".jig", "critique", surface), record);
|
|
8747
8935
|
if (unjudged.length) {
|
|
8748
8936
|
problems.push(
|
|
8749
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.`
|
|
@@ -8753,7 +8941,7 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8753
8941
|
return problems;
|
|
8754
8942
|
}
|
|
8755
8943
|
function structureSinceApproval(root, spec, front) {
|
|
8756
|
-
const git = (args) =>
|
|
8944
|
+
const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8757
8945
|
let baseline;
|
|
8758
8946
|
let judged = true;
|
|
8759
8947
|
try {
|
|
@@ -8779,9 +8967,9 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8779
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\`.`);
|
|
8780
8968
|
}
|
|
8781
8969
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8782
|
-
if (at && !/^https?:/i.test(at) && existsSync28(
|
|
8970
|
+
if (at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
|
|
8783
8971
|
try {
|
|
8784
|
-
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(
|
|
8972
|
+
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join46(root, at))) {
|
|
8785
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\`.`);
|
|
8786
8974
|
}
|
|
8787
8975
|
} catch {
|
|
@@ -8791,7 +8979,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8791
8979
|
}
|
|
8792
8980
|
function readTweak(dir) {
|
|
8793
8981
|
try {
|
|
8794
|
-
const raw = JSON.parse(
|
|
8982
|
+
const raw = JSON.parse(readFileSync38(join46(dir, "tweak.json"), "utf8"));
|
|
8795
8983
|
const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
|
|
8796
8984
|
return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
|
|
8797
8985
|
} catch {
|
|
@@ -8804,13 +8992,13 @@ function surfacePage(projectRoot, surface) {
|
|
|
8804
8992
|
const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8805
8993
|
const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
|
|
8806
8994
|
for (const candidate of candidates) {
|
|
8807
|
-
if (candidate && existsSync28(
|
|
8995
|
+
if (candidate && existsSync28(join46(projectRoot, candidate))) return candidate;
|
|
8808
8996
|
}
|
|
8809
8997
|
return void 0;
|
|
8810
8998
|
}
|
|
8811
8999
|
function gate(opts) {
|
|
8812
9000
|
const root = opts.projectRoot;
|
|
8813
|
-
if (!existsSync28(
|
|
9001
|
+
if (!existsSync28(join46(root, "jig.config.json")) && !existsSync28(join46(root, ".jig"))) {
|
|
8814
9002
|
return { block: false, reason: "" };
|
|
8815
9003
|
}
|
|
8816
9004
|
const invocation = lastJigInvocation(opts.input.transcript_path);
|
|
@@ -8846,7 +9034,7 @@ ${result.report}`);
|
|
|
8846
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.`);
|
|
8847
9035
|
}
|
|
8848
9036
|
if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
|
|
8849
|
-
if (spec && existsSync28(
|
|
9037
|
+
if (spec && existsSync28(join46(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
|
|
8850
9038
|
problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
|
|
8851
9039
|
}
|
|
8852
9040
|
}
|
|
@@ -8873,15 +9061,15 @@ ${result.report}`);
|
|
|
8873
9061
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8874
9062
|
problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8875
9063
|
const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
|
|
8876
|
-
const now = existsSync28(
|
|
9064
|
+
const now = existsSync28(join46(root, f)) ? readText(join46(root, f)) : "";
|
|
8877
9065
|
return fileAtSessionStart(root, f, start) !== now;
|
|
8878
9066
|
});
|
|
8879
9067
|
if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
|
|
8880
|
-
const critiqueDir =
|
|
9068
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8881
9069
|
if (existsSync28(critiqueDir)) {
|
|
8882
9070
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
|
|
8883
|
-
const dir =
|
|
8884
|
-
if (!existsSync28(
|
|
9071
|
+
const dir = join46(critiqueDir, surface);
|
|
9072
|
+
if (!existsSync28(join46(dir, "screen.json")) && !existsSync28(join46(dir, "code.json"))) continue;
|
|
8885
9073
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
8886
9074
|
if (!v.ok) {
|
|
8887
9075
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -8911,10 +9099,10 @@ ${problems.join("\n\n")}`
|
|
|
8911
9099
|
}
|
|
8912
9100
|
const session = opts.input.session_id ?? "unknown";
|
|
8913
9101
|
const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
8914
|
-
const stateFile =
|
|
9102
|
+
const stateFile = join46(root, ".jig", "gate.json");
|
|
8915
9103
|
let state = {};
|
|
8916
9104
|
try {
|
|
8917
|
-
state = JSON.parse(
|
|
9105
|
+
state = JSON.parse(readFileSync38(stateFile, "utf8"));
|
|
8918
9106
|
} catch {
|
|
8919
9107
|
}
|
|
8920
9108
|
if (problems.length === 0) {
|
|
@@ -8949,7 +9137,7 @@ function verdictDigests(dir) {
|
|
|
8949
9137
|
for (const f of VERDICT_FILES) {
|
|
8950
9138
|
let file;
|
|
8951
9139
|
try {
|
|
8952
|
-
file = JSON.parse(
|
|
9140
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
8953
9141
|
} catch {
|
|
8954
9142
|
continue;
|
|
8955
9143
|
}
|
|
@@ -8964,7 +9152,7 @@ function writeLock(root, surface, lockPath, dir, now) {
|
|
|
8964
9152
|
const pageFile2 = recordedPage(root, surface);
|
|
8965
9153
|
let page;
|
|
8966
9154
|
try {
|
|
8967
|
-
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(
|
|
9155
|
+
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync38(join46(root, pageFile2), "utf8")) };
|
|
8968
9156
|
} catch {
|
|
8969
9157
|
}
|
|
8970
9158
|
const tweakAt = readTweak(dir)?.at;
|
|
@@ -8977,7 +9165,7 @@ function verdictChecksum(dir) {
|
|
|
8977
9165
|
const parts = VERDICT_FILES.map((f) => {
|
|
8978
9166
|
try {
|
|
8979
9167
|
return `${f}
|
|
8980
|
-
${
|
|
9168
|
+
${readFileSync38(join46(dir, f), "utf8")}`;
|
|
8981
9169
|
} catch {
|
|
8982
9170
|
return "";
|
|
8983
9171
|
}
|
|
@@ -8985,15 +9173,15 @@ ${readFileSync37(join44(dir, f), "utf8")}`;
|
|
|
8985
9173
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
8986
9174
|
}
|
|
8987
9175
|
function verdictGuard(root, command, inPlay) {
|
|
8988
|
-
const critiqueDir =
|
|
9176
|
+
const critiqueDir = join46(root, ".jig", "critique");
|
|
8989
9177
|
if (!existsSync28(critiqueDir)) return [];
|
|
8990
9178
|
const problems = [];
|
|
8991
9179
|
for (const surface of readdirSync18(critiqueDir)) {
|
|
8992
9180
|
if (surface.startsWith("_")) continue;
|
|
8993
|
-
const dir =
|
|
9181
|
+
const dir = join46(critiqueDir, surface);
|
|
8994
9182
|
const now = verdictChecksum(dir);
|
|
8995
9183
|
if (!now) continue;
|
|
8996
|
-
const lockPath =
|
|
9184
|
+
const lockPath = join46(dir, LOCK);
|
|
8997
9185
|
if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
|
|
8998
9186
|
writeLock(root, surface, lockPath, dir, now);
|
|
8999
9187
|
problems.push(...lockLeftBehind(root, surface));
|
|
@@ -9001,7 +9189,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
9001
9189
|
}
|
|
9002
9190
|
let lock = {};
|
|
9003
9191
|
try {
|
|
9004
|
-
lock = JSON.parse(
|
|
9192
|
+
lock = JSON.parse(readFileSync38(lockPath, "utf8"));
|
|
9005
9193
|
} catch {
|
|
9006
9194
|
continue;
|
|
9007
9195
|
}
|
|
@@ -9026,7 +9214,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
9026
9214
|
function lockLeftBehind(root, surface) {
|
|
9027
9215
|
const rel = `.jig/critique/${surface}`;
|
|
9028
9216
|
try {
|
|
9029
|
-
const status = (paths) =>
|
|
9217
|
+
const status = (paths) => execFileSync5("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
9030
9218
|
if (!status([`${rel}/${LOCK}`])) return [];
|
|
9031
9219
|
if (status(VERDICT_FILES.map((f) => `${rel}/${f}`))) return [];
|
|
9032
9220
|
} catch {
|
|
@@ -9038,7 +9226,7 @@ function probesLeftBehind(root, surfaces) {
|
|
|
9038
9226
|
if (!surfaces.length) return [];
|
|
9039
9227
|
let lines;
|
|
9040
9228
|
try {
|
|
9041
|
-
lines =
|
|
9229
|
+
lines = execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
|
|
9042
9230
|
} catch {
|
|
9043
9231
|
return [];
|
|
9044
9232
|
}
|
|
@@ -9077,7 +9265,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
9077
9265
|
for (const f of VERDICT_FILES) {
|
|
9078
9266
|
let file;
|
|
9079
9267
|
try {
|
|
9080
|
-
file = JSON.parse(
|
|
9268
|
+
file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
|
|
9081
9269
|
} catch {
|
|
9082
9270
|
continue;
|
|
9083
9271
|
}
|
|
@@ -9089,7 +9277,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
9089
9277
|
}
|
|
9090
9278
|
function save(file, state) {
|
|
9091
9279
|
try {
|
|
9092
|
-
mkdirSync8(
|
|
9280
|
+
mkdirSync8(join46(file, ".."), { recursive: true });
|
|
9093
9281
|
writeFileSync6(file, JSON.stringify(state), "utf8");
|
|
9094
9282
|
} catch {
|
|
9095
9283
|
}
|
|
@@ -9097,7 +9285,7 @@ function save(file, state) {
|
|
|
9097
9285
|
|
|
9098
9286
|
// src/index.ts
|
|
9099
9287
|
var packageRoot = getPackageRoot();
|
|
9100
|
-
var { version } = JSON.parse(
|
|
9288
|
+
var { version } = JSON.parse(readFileSync39(join47(packageRoot, "package.json"), "utf8"));
|
|
9101
9289
|
function warnIfUnpublishedPin() {
|
|
9102
9290
|
if (isDevVersion(version)) {
|
|
9103
9291
|
console.warn(
|
|
@@ -9209,12 +9397,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
9209
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) => {
|
|
9210
9398
|
const projectRoot = findProjectRoot(process.cwd());
|
|
9211
9399
|
const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
|
|
9212
|
-
const path =
|
|
9400
|
+
const path = join47(projectRoot, ".jig", "specs", `${slug}.spec.md`);
|
|
9213
9401
|
if (!existsSync29(path)) {
|
|
9214
9402
|
console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
|
|
9215
9403
|
process.exit(1);
|
|
9216
9404
|
}
|
|
9217
|
-
console.log(specChecksum(
|
|
9405
|
+
console.log(specChecksum(readFileSync39(path, "utf8")));
|
|
9218
9406
|
});
|
|
9219
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) => {
|
|
9220
9408
|
const projectRoot = findProjectRoot(process.cwd());
|
|
@@ -9348,7 +9536,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
9348
9536
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
9349
9537
|
let input = {};
|
|
9350
9538
|
try {
|
|
9351
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
9539
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync39(0, "utf8") || "{}");
|
|
9352
9540
|
} catch {
|
|
9353
9541
|
}
|
|
9354
9542
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -9389,7 +9577,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
9389
9577
|
});
|
|
9390
9578
|
function resolveSurfaceMode(projectRoot) {
|
|
9391
9579
|
try {
|
|
9392
|
-
const config = JSON.parse(
|
|
9580
|
+
const config = JSON.parse(readFileSync39(join47(projectRoot, "jig.config.json"), "utf8"));
|
|
9393
9581
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
9394
9582
|
return modes.length === 1 ? modes[0] : void 0;
|
|
9395
9583
|
} catch {
|