@agent-scope/cli 1.12.0 → 1.14.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/dist/cli.js +1029 -51
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +973 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +975 -23
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
6
|
import { Command as Command8 } from "commander";
|
|
7
7
|
|
|
@@ -255,9 +255,9 @@ function createRL() {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
async function ask(rl, question) {
|
|
258
|
-
return new Promise((
|
|
258
|
+
return new Promise((resolve14) => {
|
|
259
259
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
260
|
+
resolve14(answer.trim());
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -2874,8 +2874,8 @@ Available: ${available}`
|
|
|
2874
2874
|
`
|
|
2875
2875
|
);
|
|
2876
2876
|
if (opts.sprite !== void 0) {
|
|
2877
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2878
|
-
const gen = new
|
|
2877
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
2878
|
+
const gen = new SpriteSheetGenerator2();
|
|
2879
2879
|
const sheet = await gen.generate(result);
|
|
2880
2880
|
const spritePath = resolve7(process.cwd(), opts.sprite);
|
|
2881
2881
|
writeFileSync4(spritePath, sheet.png);
|
|
@@ -2884,8 +2884,8 @@ Available: ${available}`
|
|
|
2884
2884
|
}
|
|
2885
2885
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2886
2886
|
if (fmt === "file") {
|
|
2887
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2888
|
-
const gen = new
|
|
2887
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
2888
|
+
const gen = new SpriteSheetGenerator2();
|
|
2889
2889
|
const sheet = await gen.generate(result);
|
|
2890
2890
|
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2891
2891
|
mkdirSync3(dir, { recursive: true });
|
|
@@ -2902,8 +2902,8 @@ Available: ${available}`
|
|
|
2902
2902
|
} else if (fmt === "png") {
|
|
2903
2903
|
if (opts.sprite !== void 0) {
|
|
2904
2904
|
} else {
|
|
2905
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2906
|
-
const gen = new
|
|
2905
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
2906
|
+
const gen = new SpriteSheetGenerator2();
|
|
2907
2907
|
const sheet = await gen.generate(result);
|
|
2908
2908
|
process.stdout.write(sheet.png);
|
|
2909
2909
|
}
|
|
@@ -3217,12 +3217,12 @@ async function runBaseline(options = {}) {
|
|
|
3217
3217
|
mkdirSync4(rendersDir, { recursive: true });
|
|
3218
3218
|
let manifest;
|
|
3219
3219
|
if (manifestPath !== void 0) {
|
|
3220
|
-
const { readFileSync:
|
|
3220
|
+
const { readFileSync: readFileSync10 } = await import("fs");
|
|
3221
3221
|
const absPath = resolve8(rootDir, manifestPath);
|
|
3222
3222
|
if (!existsSync5(absPath)) {
|
|
3223
3223
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3224
3224
|
}
|
|
3225
|
-
manifest = JSON.parse(
|
|
3225
|
+
manifest = JSON.parse(readFileSync10(absPath, "utf-8"));
|
|
3226
3226
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3227
3227
|
`);
|
|
3228
3228
|
} else {
|
|
@@ -3384,6 +3384,486 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3384
3384
|
);
|
|
3385
3385
|
}
|
|
3386
3386
|
|
|
3387
|
+
// src/report/diff.ts
|
|
3388
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3389
|
+
import { resolve as resolve9 } from "path";
|
|
3390
|
+
import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
|
|
3391
|
+
import { BrowserPool as BrowserPool5, safeRender as safeRender3 } from "@agent-scope/render";
|
|
3392
|
+
import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
|
|
3393
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3394
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3395
|
+
const compliancePath = resolve9(baselineDir, "compliance.json");
|
|
3396
|
+
if (!existsSync6(compliancePath)) return null;
|
|
3397
|
+
const raw = JSON.parse(readFileSync5(compliancePath, "utf-8"));
|
|
3398
|
+
return raw;
|
|
3399
|
+
}
|
|
3400
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3401
|
+
const jsonPath = resolve9(baselineDir, "renders", `${componentName}.json`);
|
|
3402
|
+
if (!existsSync6(jsonPath)) return null;
|
|
3403
|
+
return JSON.parse(readFileSync5(jsonPath, "utf-8"));
|
|
3404
|
+
}
|
|
3405
|
+
var _pool5 = null;
|
|
3406
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3407
|
+
if (_pool5 === null) {
|
|
3408
|
+
_pool5 = new BrowserPool5({
|
|
3409
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3410
|
+
viewportWidth,
|
|
3411
|
+
viewportHeight
|
|
3412
|
+
});
|
|
3413
|
+
await _pool5.init();
|
|
3414
|
+
}
|
|
3415
|
+
return _pool5;
|
|
3416
|
+
}
|
|
3417
|
+
async function shutdownPool5() {
|
|
3418
|
+
if (_pool5 !== null) {
|
|
3419
|
+
await _pool5.close();
|
|
3420
|
+
_pool5 = null;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3424
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3425
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3426
|
+
const slot = await pool.acquire();
|
|
3427
|
+
const { page } = slot;
|
|
3428
|
+
try {
|
|
3429
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3430
|
+
await page.waitForFunction(
|
|
3431
|
+
() => {
|
|
3432
|
+
const w = window;
|
|
3433
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3434
|
+
},
|
|
3435
|
+
{ timeout: 15e3 }
|
|
3436
|
+
);
|
|
3437
|
+
const renderError = await page.evaluate(() => {
|
|
3438
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3439
|
+
});
|
|
3440
|
+
if (renderError !== null) {
|
|
3441
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3442
|
+
}
|
|
3443
|
+
const rootDir = process.cwd();
|
|
3444
|
+
const classes = await page.evaluate(() => {
|
|
3445
|
+
const set = /* @__PURE__ */ new Set();
|
|
3446
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3447
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3448
|
+
if (c) set.add(c);
|
|
3449
|
+
}
|
|
3450
|
+
});
|
|
3451
|
+
return [...set];
|
|
3452
|
+
});
|
|
3453
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3454
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3455
|
+
await page.addStyleTag({ content: projectCss });
|
|
3456
|
+
}
|
|
3457
|
+
const startMs = performance.now();
|
|
3458
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3459
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3460
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3461
|
+
throw new Error(
|
|
3462
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
const PAD = 24;
|
|
3466
|
+
const MIN_W = 320;
|
|
3467
|
+
const MIN_H = 200;
|
|
3468
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3469
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3470
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3471
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3472
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3473
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3474
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3475
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3476
|
+
const screenshot = await page.screenshot({
|
|
3477
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3478
|
+
type: "png"
|
|
3479
|
+
});
|
|
3480
|
+
const computedStylesRaw = {};
|
|
3481
|
+
const styles = await page.evaluate((sel) => {
|
|
3482
|
+
const el = document.querySelector(sel);
|
|
3483
|
+
if (el === null) return {};
|
|
3484
|
+
const computed = window.getComputedStyle(el);
|
|
3485
|
+
const out = {};
|
|
3486
|
+
for (const prop of [
|
|
3487
|
+
"display",
|
|
3488
|
+
"width",
|
|
3489
|
+
"height",
|
|
3490
|
+
"color",
|
|
3491
|
+
"backgroundColor",
|
|
3492
|
+
"fontSize",
|
|
3493
|
+
"fontFamily",
|
|
3494
|
+
"padding",
|
|
3495
|
+
"margin"
|
|
3496
|
+
]) {
|
|
3497
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3498
|
+
}
|
|
3499
|
+
return out;
|
|
3500
|
+
}, "[data-reactscope-root] > *");
|
|
3501
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3502
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3503
|
+
return {
|
|
3504
|
+
screenshot,
|
|
3505
|
+
width: Math.round(safeW),
|
|
3506
|
+
height: Math.round(safeH),
|
|
3507
|
+
renderTimeMs,
|
|
3508
|
+
computedStyles: computedStylesRaw
|
|
3509
|
+
};
|
|
3510
|
+
} finally {
|
|
3511
|
+
pool.release(slot);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3515
|
+
const flat = {};
|
|
3516
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3517
|
+
Object.assign(flat, styles);
|
|
3518
|
+
}
|
|
3519
|
+
const colors = {};
|
|
3520
|
+
const spacing = {};
|
|
3521
|
+
const typography = {};
|
|
3522
|
+
const borders = {};
|
|
3523
|
+
const shadows = {};
|
|
3524
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3525
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3526
|
+
colors[prop] = value;
|
|
3527
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3528
|
+
spacing[prop] = value;
|
|
3529
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3530
|
+
typography[prop] = value;
|
|
3531
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3532
|
+
borders[prop] = value;
|
|
3533
|
+
} else if (prop === "boxShadow") {
|
|
3534
|
+
shadows[prop] = value;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3538
|
+
}
|
|
3539
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3540
|
+
if (entry.renderFailed) return "unchanged";
|
|
3541
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3542
|
+
return "added";
|
|
3543
|
+
}
|
|
3544
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3545
|
+
return "removed";
|
|
3546
|
+
}
|
|
3547
|
+
const delta = entry.complianceDelta;
|
|
3548
|
+
if (delta !== null) {
|
|
3549
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3550
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3551
|
+
}
|
|
3552
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3553
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3554
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3555
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3556
|
+
}
|
|
3557
|
+
return "unchanged";
|
|
3558
|
+
}
|
|
3559
|
+
async function runDiff(options = {}) {
|
|
3560
|
+
const {
|
|
3561
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3562
|
+
componentsGlob,
|
|
3563
|
+
manifestPath,
|
|
3564
|
+
viewportWidth = 375,
|
|
3565
|
+
viewportHeight = 812,
|
|
3566
|
+
regressionThreshold = 0.01
|
|
3567
|
+
} = options;
|
|
3568
|
+
const startTime = performance.now();
|
|
3569
|
+
const rootDir = process.cwd();
|
|
3570
|
+
const baselineDir = resolve9(rootDir, baselineDirRaw);
|
|
3571
|
+
if (!existsSync6(baselineDir)) {
|
|
3572
|
+
throw new Error(
|
|
3573
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
const baselineManifestPath = resolve9(baselineDir, "manifest.json");
|
|
3577
|
+
if (!existsSync6(baselineManifestPath)) {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
const baselineManifest = JSON.parse(readFileSync5(baselineManifestPath, "utf-8"));
|
|
3583
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3584
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3585
|
+
process.stderr.write(
|
|
3586
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3587
|
+
`
|
|
3588
|
+
);
|
|
3589
|
+
let currentManifest;
|
|
3590
|
+
if (manifestPath !== void 0) {
|
|
3591
|
+
const absPath = resolve9(rootDir, manifestPath);
|
|
3592
|
+
if (!existsSync6(absPath)) {
|
|
3593
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3594
|
+
}
|
|
3595
|
+
currentManifest = JSON.parse(readFileSync5(absPath, "utf-8"));
|
|
3596
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3597
|
+
`);
|
|
3598
|
+
} else {
|
|
3599
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3600
|
+
currentManifest = await generateManifest3({ rootDir });
|
|
3601
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3602
|
+
process.stderr.write(`Found ${count} components.
|
|
3603
|
+
`);
|
|
3604
|
+
}
|
|
3605
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3606
|
+
if (componentsGlob !== void 0) {
|
|
3607
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3608
|
+
process.stderr.write(
|
|
3609
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3610
|
+
`
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3614
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3615
|
+
);
|
|
3616
|
+
const total = componentNames.length;
|
|
3617
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3618
|
+
`);
|
|
3619
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3620
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3621
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3622
|
+
let completed = 0;
|
|
3623
|
+
const CONCURRENCY = 4;
|
|
3624
|
+
let nextIdx = 0;
|
|
3625
|
+
const renderOne = async (name) => {
|
|
3626
|
+
const descriptor = currentManifest.components[name];
|
|
3627
|
+
if (descriptor === void 0) return;
|
|
3628
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3629
|
+
const outcome = await safeRender3(
|
|
3630
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3631
|
+
{
|
|
3632
|
+
props: {},
|
|
3633
|
+
sourceLocation: {
|
|
3634
|
+
file: descriptor.filePath,
|
|
3635
|
+
line: descriptor.loc.start,
|
|
3636
|
+
column: 0
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
completed++;
|
|
3641
|
+
const pct = Math.round(completed / total * 100);
|
|
3642
|
+
if (isTTY()) {
|
|
3643
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3644
|
+
}
|
|
3645
|
+
if (outcome.crashed) {
|
|
3646
|
+
renderFailures.add(name);
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
const result = outcome.result;
|
|
3650
|
+
currentRenderMeta.set(name, {
|
|
3651
|
+
width: result.width,
|
|
3652
|
+
height: result.height,
|
|
3653
|
+
renderTimeMs: result.renderTimeMs
|
|
3654
|
+
});
|
|
3655
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3656
|
+
};
|
|
3657
|
+
if (total > 0) {
|
|
3658
|
+
const worker = async () => {
|
|
3659
|
+
while (nextIdx < componentNames.length) {
|
|
3660
|
+
const i = nextIdx++;
|
|
3661
|
+
const name = componentNames[i];
|
|
3662
|
+
if (name !== void 0) {
|
|
3663
|
+
await renderOne(name);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
const workers = [];
|
|
3668
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3669
|
+
workers.push(worker());
|
|
3670
|
+
}
|
|
3671
|
+
await Promise.all(workers);
|
|
3672
|
+
}
|
|
3673
|
+
await shutdownPool5();
|
|
3674
|
+
if (isTTY() && total > 0) {
|
|
3675
|
+
process.stderr.write("\n");
|
|
3676
|
+
}
|
|
3677
|
+
const resolver = new TokenResolver2([]);
|
|
3678
|
+
const engine = new ComplianceEngine2(resolver);
|
|
3679
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3680
|
+
const entries = [];
|
|
3681
|
+
for (const name of componentNames) {
|
|
3682
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3683
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3684
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3685
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3686
|
+
const failed = renderFailures.has(name);
|
|
3687
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3688
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3689
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3690
|
+
const partial = {
|
|
3691
|
+
name,
|
|
3692
|
+
baselineCompliance: baselineComplianceScore,
|
|
3693
|
+
currentCompliance: currentComplianceScore,
|
|
3694
|
+
complianceDelta: delta,
|
|
3695
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3696
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3697
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3698
|
+
renderFailed: failed
|
|
3699
|
+
};
|
|
3700
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3701
|
+
}
|
|
3702
|
+
for (const name of removedNames) {
|
|
3703
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3704
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3705
|
+
entries.push({
|
|
3706
|
+
name,
|
|
3707
|
+
status: "removed",
|
|
3708
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3709
|
+
currentCompliance: null,
|
|
3710
|
+
complianceDelta: null,
|
|
3711
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3712
|
+
currentDimensions: null,
|
|
3713
|
+
renderTimeMs: null,
|
|
3714
|
+
renderFailed: false
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
const summary = {
|
|
3718
|
+
total: entries.length,
|
|
3719
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3720
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3721
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3722
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3723
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3724
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3725
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3726
|
+
};
|
|
3727
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3728
|
+
const wallClockMs = performance.now() - startTime;
|
|
3729
|
+
return {
|
|
3730
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3731
|
+
baselineDir,
|
|
3732
|
+
summary,
|
|
3733
|
+
components: entries,
|
|
3734
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3735
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3736
|
+
hasRegressions,
|
|
3737
|
+
wallClockMs
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
var STATUS_ICON = {
|
|
3741
|
+
added: "+",
|
|
3742
|
+
removed: "-",
|
|
3743
|
+
unchanged: " ",
|
|
3744
|
+
compliance_regressed: "\u2193",
|
|
3745
|
+
compliance_improved: "\u2191",
|
|
3746
|
+
size_changed: "~"
|
|
3747
|
+
};
|
|
3748
|
+
var STATUS_LABEL = {
|
|
3749
|
+
added: "added",
|
|
3750
|
+
removed: "removed",
|
|
3751
|
+
unchanged: "ok",
|
|
3752
|
+
compliance_regressed: "regressed",
|
|
3753
|
+
compliance_improved: "improved",
|
|
3754
|
+
size_changed: "size changed"
|
|
3755
|
+
};
|
|
3756
|
+
function formatDiffReport(result) {
|
|
3757
|
+
const lines = [];
|
|
3758
|
+
const title = "Scope Report Diff";
|
|
3759
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3760
|
+
lines.push(title, rule2);
|
|
3761
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3762
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3763
|
+
lines.push(
|
|
3764
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3765
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3766
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3767
|
+
""
|
|
3768
|
+
);
|
|
3769
|
+
const s = result.summary;
|
|
3770
|
+
lines.push(
|
|
3771
|
+
`Components: ${s.total} total ` + [
|
|
3772
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3773
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3774
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3775
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3776
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3777
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3778
|
+
].filter(Boolean).join(" "),
|
|
3779
|
+
""
|
|
3780
|
+
);
|
|
3781
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3782
|
+
if (notable.length === 0) {
|
|
3783
|
+
lines.push(" No changes detected.");
|
|
3784
|
+
} else {
|
|
3785
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3786
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3787
|
+
const divider = "-".repeat(header.length);
|
|
3788
|
+
lines.push(header, divider);
|
|
3789
|
+
for (const entry of notable) {
|
|
3790
|
+
const icon = STATUS_ICON[entry.status];
|
|
3791
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3792
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3793
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3794
|
+
if (entry.complianceDelta !== null) {
|
|
3795
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3796
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3797
|
+
}
|
|
3798
|
+
let dimStr = "\u2014";
|
|
3799
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3800
|
+
const b = entry.baselineDimensions;
|
|
3801
|
+
const c = entry.currentDimensions;
|
|
3802
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3803
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3804
|
+
} else {
|
|
3805
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3806
|
+
}
|
|
3807
|
+
} else if (entry.currentDimensions !== null) {
|
|
3808
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3809
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3810
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3811
|
+
}
|
|
3812
|
+
if (entry.renderFailed) {
|
|
3813
|
+
dimStr = "render failed";
|
|
3814
|
+
}
|
|
3815
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
lines.push(
|
|
3819
|
+
"",
|
|
3820
|
+
rule2,
|
|
3821
|
+
result.hasRegressions ? `Diff complete: ${result.summary.complianceRegressed + result.summary.renderFailed} regression(s) detected in ${(result.wallClockMs / 1e3).toFixed(1)}s` : `Diff complete: no regressions in ${(result.wallClockMs / 1e3).toFixed(1)}s`
|
|
3822
|
+
);
|
|
3823
|
+
return lines.join("\n");
|
|
3824
|
+
}
|
|
3825
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3826
|
+
reportCmd.command("diff").description("Compare the current component library against a saved baseline snapshot").option("-b, --baseline <dir>", "Baseline directory to compare against", DEFAULT_BASELINE_DIR2).option("--components <glob>", "Glob pattern to diff a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").option("--json", "Output diff as JSON instead of human-readable text", false).option("-o, --output <path>", "Write the diff JSON to a file").option(
|
|
3827
|
+
"--regression-threshold <n>",
|
|
3828
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3829
|
+
"0.01"
|
|
3830
|
+
).action(
|
|
3831
|
+
async (opts) => {
|
|
3832
|
+
try {
|
|
3833
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3834
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3835
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3836
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3837
|
+
const result = await runDiff({
|
|
3838
|
+
baselineDir: opts.baseline,
|
|
3839
|
+
componentsGlob: opts.components,
|
|
3840
|
+
manifestPath: opts.manifest,
|
|
3841
|
+
viewportWidth,
|
|
3842
|
+
viewportHeight,
|
|
3843
|
+
regressionThreshold
|
|
3844
|
+
});
|
|
3845
|
+
if (opts.output !== void 0) {
|
|
3846
|
+
writeFileSync6(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3847
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3848
|
+
`);
|
|
3849
|
+
}
|
|
3850
|
+
if (opts.json) {
|
|
3851
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3852
|
+
`);
|
|
3853
|
+
} else {
|
|
3854
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3855
|
+
`);
|
|
3856
|
+
}
|
|
3857
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3858
|
+
} catch (err) {
|
|
3859
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3860
|
+
`);
|
|
3861
|
+
process.exit(2);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3387
3867
|
// src/tree-formatter.ts
|
|
3388
3868
|
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3389
3869
|
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
@@ -3664,47 +4144,223 @@ function buildStructuredReport(report) {
|
|
|
3664
4144
|
}
|
|
3665
4145
|
|
|
3666
4146
|
// src/tokens/commands.ts
|
|
3667
|
-
import { existsSync as
|
|
3668
|
-
import { resolve as
|
|
4147
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
4148
|
+
import { resolve as resolve13 } from "path";
|
|
3669
4149
|
import {
|
|
3670
4150
|
parseTokenFileSync as parseTokenFileSync2,
|
|
3671
4151
|
TokenParseError,
|
|
3672
|
-
TokenResolver as
|
|
4152
|
+
TokenResolver as TokenResolver7,
|
|
3673
4153
|
TokenValidationError,
|
|
3674
4154
|
validateTokenFile
|
|
3675
4155
|
} from "@agent-scope/tokens";
|
|
3676
4156
|
import { Command as Command7 } from "commander";
|
|
3677
4157
|
|
|
4158
|
+
// src/tokens/compliance.ts
|
|
4159
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
4160
|
+
import { resolve as resolve10 } from "path";
|
|
4161
|
+
import {
|
|
4162
|
+
ComplianceEngine as ComplianceEngine3,
|
|
4163
|
+
TokenResolver as TokenResolver3
|
|
4164
|
+
} from "@agent-scope/tokens";
|
|
4165
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4166
|
+
function loadStylesFile(stylesPath) {
|
|
4167
|
+
const absPath = resolve10(process.cwd(), stylesPath);
|
|
4168
|
+
if (!existsSync7(absPath)) {
|
|
4169
|
+
throw new Error(
|
|
4170
|
+
`Compliance styles file not found at ${absPath}.
|
|
4171
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4172
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
4176
|
+
let parsed;
|
|
4177
|
+
try {
|
|
4178
|
+
parsed = JSON.parse(raw);
|
|
4179
|
+
} catch (err) {
|
|
4180
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4181
|
+
}
|
|
4182
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4183
|
+
throw new Error(
|
|
4184
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
return parsed;
|
|
4188
|
+
}
|
|
4189
|
+
function categoryForProperty(property) {
|
|
4190
|
+
const lower = property.toLowerCase();
|
|
4191
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4192
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4193
|
+
return "color";
|
|
4194
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4195
|
+
return "spacing";
|
|
4196
|
+
if (lower.includes("border")) return "border";
|
|
4197
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4198
|
+
return "typography";
|
|
4199
|
+
return "spacing";
|
|
4200
|
+
}
|
|
4201
|
+
function buildCategorySummary(batch) {
|
|
4202
|
+
const cats = {
|
|
4203
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4204
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4205
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4206
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4207
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4208
|
+
};
|
|
4209
|
+
for (const report of Object.values(batch.components)) {
|
|
4210
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4211
|
+
const cat = categoryForProperty(property);
|
|
4212
|
+
const summary = cats[cat];
|
|
4213
|
+
if (summary === void 0) continue;
|
|
4214
|
+
summary.total++;
|
|
4215
|
+
if (result.status === "on_system") {
|
|
4216
|
+
summary.onSystem++;
|
|
4217
|
+
} else {
|
|
4218
|
+
summary.offSystem++;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
for (const summary of Object.values(cats)) {
|
|
4223
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4224
|
+
}
|
|
4225
|
+
return cats;
|
|
4226
|
+
}
|
|
4227
|
+
function collectOffenders(batch, limit = 10) {
|
|
4228
|
+
const offenders = [];
|
|
4229
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4230
|
+
name,
|
|
4231
|
+
report,
|
|
4232
|
+
offSystemCount: report.offSystem
|
|
4233
|
+
}));
|
|
4234
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4235
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4236
|
+
if (offSystemCount === 0) continue;
|
|
4237
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4238
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4239
|
+
offenders.push({
|
|
4240
|
+
component: name,
|
|
4241
|
+
property,
|
|
4242
|
+
value: result.value,
|
|
4243
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4244
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4245
|
+
offSystemCount
|
|
4246
|
+
});
|
|
4247
|
+
if (offenders.length >= limit) break;
|
|
4248
|
+
}
|
|
4249
|
+
if (offenders.length >= limit) break;
|
|
4250
|
+
}
|
|
4251
|
+
return offenders;
|
|
4252
|
+
}
|
|
4253
|
+
function formatPct(n) {
|
|
4254
|
+
return `${Math.round(n * 100)}%`;
|
|
4255
|
+
}
|
|
4256
|
+
function truncate(s, max) {
|
|
4257
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4258
|
+
}
|
|
4259
|
+
function formatComplianceReport(batch, threshold) {
|
|
4260
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4261
|
+
const lines = [];
|
|
4262
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4263
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4264
|
+
lines.push("");
|
|
4265
|
+
const cats = buildCategorySummary(batch);
|
|
4266
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4267
|
+
if (catEntries.length > 0) {
|
|
4268
|
+
lines.push("By category:");
|
|
4269
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4270
|
+
for (const [cat, summary] of catEntries) {
|
|
4271
|
+
const label = cat.padEnd(catWidth);
|
|
4272
|
+
lines.push(
|
|
4273
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
lines.push("");
|
|
4277
|
+
}
|
|
4278
|
+
const offenders = collectOffenders(batch);
|
|
4279
|
+
if (offenders.length > 0) {
|
|
4280
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4281
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4282
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4283
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4284
|
+
for (const offender of offenders) {
|
|
4285
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4286
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4287
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4288
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4289
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4290
|
+
}
|
|
4291
|
+
} else {
|
|
4292
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4293
|
+
}
|
|
4294
|
+
return lines.join("\n");
|
|
4295
|
+
}
|
|
4296
|
+
function registerCompliance(tokensCmd) {
|
|
4297
|
+
tokensCmd.command("compliance").description("Aggregate token compliance report across all components (Token Spec \xA73.3 format)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4298
|
+
try {
|
|
4299
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4300
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4301
|
+
const resolver = new TokenResolver3(tokens);
|
|
4302
|
+
const engine = new ComplianceEngine3(resolver);
|
|
4303
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4304
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4305
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4306
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4307
|
+
componentMap.set(name, styles);
|
|
4308
|
+
}
|
|
4309
|
+
if (componentMap.size === 0) {
|
|
4310
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4311
|
+
`);
|
|
4312
|
+
}
|
|
4313
|
+
const batch = engine.auditBatch(componentMap);
|
|
4314
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4315
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4316
|
+
if (useJson) {
|
|
4317
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4318
|
+
`);
|
|
4319
|
+
} else {
|
|
4320
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4321
|
+
`);
|
|
4322
|
+
}
|
|
4323
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4324
|
+
process.exit(1);
|
|
4325
|
+
}
|
|
4326
|
+
} catch (err) {
|
|
4327
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4328
|
+
`);
|
|
4329
|
+
process.exit(1);
|
|
4330
|
+
}
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
|
|
3678
4334
|
// src/tokens/export.ts
|
|
3679
|
-
import { existsSync as
|
|
3680
|
-
import { resolve as
|
|
4335
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4336
|
+
import { resolve as resolve11 } from "path";
|
|
3681
4337
|
import {
|
|
3682
4338
|
exportTokens,
|
|
3683
4339
|
parseTokenFileSync,
|
|
3684
4340
|
ThemeResolver,
|
|
3685
|
-
TokenResolver as
|
|
4341
|
+
TokenResolver as TokenResolver4
|
|
3686
4342
|
} from "@agent-scope/tokens";
|
|
3687
4343
|
import { Command as Command6 } from "commander";
|
|
3688
4344
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
3689
4345
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3690
4346
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3691
|
-
function
|
|
4347
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
3692
4348
|
if (fileFlag !== void 0) {
|
|
3693
|
-
return
|
|
4349
|
+
return resolve11(process.cwd(), fileFlag);
|
|
3694
4350
|
}
|
|
3695
|
-
const configPath =
|
|
3696
|
-
if (
|
|
4351
|
+
const configPath = resolve11(process.cwd(), CONFIG_FILE);
|
|
4352
|
+
if (existsSync8(configPath)) {
|
|
3697
4353
|
try {
|
|
3698
|
-
const raw =
|
|
4354
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
3699
4355
|
const config = JSON.parse(raw);
|
|
3700
4356
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3701
4357
|
const file = config.tokens.file;
|
|
3702
|
-
return
|
|
4358
|
+
return resolve11(process.cwd(), file);
|
|
3703
4359
|
}
|
|
3704
4360
|
} catch {
|
|
3705
4361
|
}
|
|
3706
4362
|
}
|
|
3707
|
-
return
|
|
4363
|
+
return resolve11(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3708
4364
|
}
|
|
3709
4365
|
function createTokensExportCommand() {
|
|
3710
4366
|
return new Command6("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
@@ -3722,14 +4378,14 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
3722
4378
|
}
|
|
3723
4379
|
const format = opts.format;
|
|
3724
4380
|
try {
|
|
3725
|
-
const filePath =
|
|
3726
|
-
if (!
|
|
4381
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4382
|
+
if (!existsSync8(filePath)) {
|
|
3727
4383
|
throw new Error(
|
|
3728
4384
|
`Token file not found at ${filePath}.
|
|
3729
4385
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3730
4386
|
);
|
|
3731
4387
|
}
|
|
3732
|
-
const raw =
|
|
4388
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3733
4389
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3734
4390
|
let themesMap;
|
|
3735
4391
|
if (opts.theme !== void 0) {
|
|
@@ -3740,7 +4396,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
3740
4396
|
Available themes: ${available}`
|
|
3741
4397
|
);
|
|
3742
4398
|
}
|
|
3743
|
-
const baseResolver = new
|
|
4399
|
+
const baseResolver = new TokenResolver4(tokens);
|
|
3744
4400
|
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3745
4401
|
baseResolver,
|
|
3746
4402
|
rawFile
|
|
@@ -3768,8 +4424,8 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
3768
4424
|
themes: themesMap
|
|
3769
4425
|
});
|
|
3770
4426
|
if (opts.out !== void 0) {
|
|
3771
|
-
const outPath =
|
|
3772
|
-
|
|
4427
|
+
const outPath = resolve11(process.cwd(), opts.out);
|
|
4428
|
+
writeFileSync7(outPath, output, "utf-8");
|
|
3773
4429
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3774
4430
|
`);
|
|
3775
4431
|
} else {
|
|
@@ -3787,6 +4443,324 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
3787
4443
|
);
|
|
3788
4444
|
}
|
|
3789
4445
|
|
|
4446
|
+
// src/tokens/impact.ts
|
|
4447
|
+
import {
|
|
4448
|
+
ComplianceEngine as ComplianceEngine4,
|
|
4449
|
+
ImpactAnalyzer,
|
|
4450
|
+
TokenResolver as TokenResolver5
|
|
4451
|
+
} from "@agent-scope/tokens";
|
|
4452
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4453
|
+
var SEVERITY_EMOJI = {
|
|
4454
|
+
none: "\u25CB",
|
|
4455
|
+
subtle: "\u25D4",
|
|
4456
|
+
moderate: "\u25D1",
|
|
4457
|
+
significant: "\u25CF"
|
|
4458
|
+
};
|
|
4459
|
+
function formatImpactReport(report) {
|
|
4460
|
+
const lines = [];
|
|
4461
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4462
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4463
|
+
if (report.components.length === 0) {
|
|
4464
|
+
lines.push("");
|
|
4465
|
+
lines.push("No components reference this token.");
|
|
4466
|
+
return lines.join("\n");
|
|
4467
|
+
}
|
|
4468
|
+
lines.push("");
|
|
4469
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4470
|
+
const propWidth = Math.max(
|
|
4471
|
+
8,
|
|
4472
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4473
|
+
);
|
|
4474
|
+
for (const comp of report.components) {
|
|
4475
|
+
for (const property of comp.affectedProperties) {
|
|
4476
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4477
|
+
const prop = property.padEnd(propWidth);
|
|
4478
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4479
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
lines.push("");
|
|
4483
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4484
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4485
|
+
lines.push(
|
|
4486
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4487
|
+
);
|
|
4488
|
+
if (report.colorDelta !== void 0) {
|
|
4489
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4490
|
+
}
|
|
4491
|
+
return lines.join("\n");
|
|
4492
|
+
}
|
|
4493
|
+
function formatImpactSummary(report) {
|
|
4494
|
+
if (report.components.length === 0) {
|
|
4495
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4496
|
+
}
|
|
4497
|
+
const parts = report.components.map(
|
|
4498
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4499
|
+
);
|
|
4500
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4501
|
+
}
|
|
4502
|
+
function registerImpact(tokensCmd) {
|
|
4503
|
+
tokensCmd.command("impact <path>").description("List all components and elements that consume a given token (Token Spec \xA74.3)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH2})`).option("--new-value <value>", "Proposed new value \u2014 report visual severity of the change").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action(
|
|
4504
|
+
(tokenPath, opts) => {
|
|
4505
|
+
try {
|
|
4506
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4507
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4508
|
+
const resolver = new TokenResolver5(tokens);
|
|
4509
|
+
const engine = new ComplianceEngine4(resolver);
|
|
4510
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4511
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4512
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4513
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4514
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4515
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4516
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4517
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4518
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4519
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4520
|
+
if (useJson) {
|
|
4521
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4522
|
+
`);
|
|
4523
|
+
} else {
|
|
4524
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4525
|
+
`);
|
|
4526
|
+
if (isTTY()) {
|
|
4527
|
+
process.stdout.write(`
|
|
4528
|
+
${formatImpactSummary(report)}
|
|
4529
|
+
`);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
} catch (err) {
|
|
4533
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4534
|
+
`);
|
|
4535
|
+
process.exit(1);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
);
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// src/tokens/preview.ts
|
|
4542
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
4543
|
+
import { resolve as resolve12 } from "path";
|
|
4544
|
+
import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
|
|
4545
|
+
import { ComplianceEngine as ComplianceEngine5, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver6 } from "@agent-scope/tokens";
|
|
4546
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4547
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4548
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4549
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4550
|
+
const htmlHarness = await buildComponentHarness(
|
|
4551
|
+
filePath,
|
|
4552
|
+
componentName,
|
|
4553
|
+
{},
|
|
4554
|
+
// no props
|
|
4555
|
+
vpWidth,
|
|
4556
|
+
cssOverride
|
|
4557
|
+
// injected as <style>
|
|
4558
|
+
);
|
|
4559
|
+
const pool = new BrowserPool6({
|
|
4560
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4561
|
+
viewportWidth: vpWidth,
|
|
4562
|
+
viewportHeight: vpHeight
|
|
4563
|
+
});
|
|
4564
|
+
await pool.init();
|
|
4565
|
+
const slot = await pool.acquire();
|
|
4566
|
+
const { page } = slot;
|
|
4567
|
+
try {
|
|
4568
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4569
|
+
await page.waitForFunction(
|
|
4570
|
+
() => {
|
|
4571
|
+
const w = window;
|
|
4572
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4573
|
+
},
|
|
4574
|
+
{ timeout: timeoutMs }
|
|
4575
|
+
);
|
|
4576
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
4577
|
+
const bb = await rootLocator.boundingBox();
|
|
4578
|
+
const PAD = 16;
|
|
4579
|
+
const MIN_W = 320;
|
|
4580
|
+
const MIN_H = 120;
|
|
4581
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
4582
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
4583
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
4584
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
4585
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
4586
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
4587
|
+
const screenshot = await page.screenshot({
|
|
4588
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
4589
|
+
type: "png"
|
|
4590
|
+
});
|
|
4591
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
4592
|
+
} finally {
|
|
4593
|
+
pool.release(slot);
|
|
4594
|
+
await pool.close().catch(() => void 0);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
function registerPreview(tokensCmd) {
|
|
4598
|
+
tokensCmd.command("preview <path>").description("Render before/after sprite sheet for components affected by a token change").requiredOption("--new-value <value>", "The proposed new resolved value for the token").option("--sprite", "Output a PNG sprite sheet (default when TTY)", false).option("-o, --output <path>", "Output PNG path (default: .reactscope/previews/<token>.png)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH3})`).option("--manifest <path>", "Path to manifest.json", DEFAULT_MANIFEST_PATH).option("--format <fmt>", "Output format: json or text (default: auto-detect)").option("--timeout <ms>", "Browser timeout per render (ms)", "10000").option("--viewport-width <px>", "Viewport width in pixels", "1280").option("--viewport-height <px>", "Viewport height in pixels", "720").action(
|
|
4599
|
+
async (tokenPath, opts) => {
|
|
4600
|
+
try {
|
|
4601
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4602
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4603
|
+
const resolver = new TokenResolver6(tokens);
|
|
4604
|
+
const engine = new ComplianceEngine5(resolver);
|
|
4605
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
4606
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4607
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4608
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4609
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4610
|
+
const analyzer = new ImpactAnalyzer2(resolver, complianceReports);
|
|
4611
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4612
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
4613
|
+
if (impactReport.components.length === 0) {
|
|
4614
|
+
process.stdout.write(
|
|
4615
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
4616
|
+
`
|
|
4617
|
+
);
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
4621
|
+
process.stderr.write(
|
|
4622
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
4623
|
+
`
|
|
4624
|
+
);
|
|
4625
|
+
const manifest = loadManifest(opts.manifest);
|
|
4626
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
4627
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
4628
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
4629
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
4630
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
4631
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
4632
|
+
const renders = [];
|
|
4633
|
+
for (const componentName of affectedNames) {
|
|
4634
|
+
const descriptor = manifest.components[componentName];
|
|
4635
|
+
if (descriptor === void 0) {
|
|
4636
|
+
process.stderr.write(
|
|
4637
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
4638
|
+
`
|
|
4639
|
+
);
|
|
4640
|
+
continue;
|
|
4641
|
+
}
|
|
4642
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
4643
|
+
`);
|
|
4644
|
+
const before = await renderComponentWithCssOverride(
|
|
4645
|
+
descriptor.filePath,
|
|
4646
|
+
componentName,
|
|
4647
|
+
beforeCss,
|
|
4648
|
+
vpWidth,
|
|
4649
|
+
vpHeight,
|
|
4650
|
+
timeout
|
|
4651
|
+
);
|
|
4652
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
4653
|
+
`);
|
|
4654
|
+
const after = await renderComponentWithCssOverride(
|
|
4655
|
+
descriptor.filePath,
|
|
4656
|
+
componentName,
|
|
4657
|
+
afterCss,
|
|
4658
|
+
vpWidth,
|
|
4659
|
+
vpHeight,
|
|
4660
|
+
timeout
|
|
4661
|
+
);
|
|
4662
|
+
renders.push({ name: componentName, before, after });
|
|
4663
|
+
}
|
|
4664
|
+
if (renders.length === 0) {
|
|
4665
|
+
process.stderr.write(
|
|
4666
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
4667
|
+
);
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
4671
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
4672
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
4673
|
+
{
|
|
4674
|
+
props: { version: "before", component: r.name },
|
|
4675
|
+
result: {
|
|
4676
|
+
screenshot: r.before.screenshot,
|
|
4677
|
+
width: cellW,
|
|
4678
|
+
height: cellH,
|
|
4679
|
+
renderTimeMs: 0,
|
|
4680
|
+
computedStyles: {}
|
|
4681
|
+
},
|
|
4682
|
+
index: colIdx * 2,
|
|
4683
|
+
axisIndices: [0, colIdx]
|
|
4684
|
+
},
|
|
4685
|
+
{
|
|
4686
|
+
props: { version: "after", component: r.name },
|
|
4687
|
+
result: {
|
|
4688
|
+
screenshot: r.after.screenshot,
|
|
4689
|
+
width: cellW,
|
|
4690
|
+
height: cellH,
|
|
4691
|
+
renderTimeMs: 0,
|
|
4692
|
+
computedStyles: {}
|
|
4693
|
+
},
|
|
4694
|
+
index: colIdx * 2 + 1,
|
|
4695
|
+
axisIndices: [1, colIdx]
|
|
4696
|
+
}
|
|
4697
|
+
]);
|
|
4698
|
+
const matrixResult = {
|
|
4699
|
+
cells,
|
|
4700
|
+
axes: [
|
|
4701
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
4702
|
+
{ name: "version", values: ["before", "after"] }
|
|
4703
|
+
],
|
|
4704
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
4705
|
+
rows: 2,
|
|
4706
|
+
cols: renders.length,
|
|
4707
|
+
stats: {
|
|
4708
|
+
totalCells: cells.length,
|
|
4709
|
+
totalRenderTimeMs: 0,
|
|
4710
|
+
avgRenderTimeMs: 0,
|
|
4711
|
+
minRenderTimeMs: 0,
|
|
4712
|
+
maxRenderTimeMs: 0,
|
|
4713
|
+
wallClockTimeMs: 0
|
|
4714
|
+
}
|
|
4715
|
+
};
|
|
4716
|
+
const generator = new SpriteSheetGenerator({
|
|
4717
|
+
cellPadding: 8,
|
|
4718
|
+
borderWidth: 1,
|
|
4719
|
+
labelHeight: 32,
|
|
4720
|
+
labelWidth: 120
|
|
4721
|
+
});
|
|
4722
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
4723
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
4724
|
+
const outputPath = opts.output ?? resolve12(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
4725
|
+
const outputDir = resolve12(outputPath, "..");
|
|
4726
|
+
mkdirSync5(outputDir, { recursive: true });
|
|
4727
|
+
writeFileSync8(outputPath, spriteResult.png);
|
|
4728
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4729
|
+
if (useJson) {
|
|
4730
|
+
process.stdout.write(
|
|
4731
|
+
`${JSON.stringify(
|
|
4732
|
+
{
|
|
4733
|
+
tokenPath,
|
|
4734
|
+
oldValue: currentValue,
|
|
4735
|
+
newValue: opts.newValue,
|
|
4736
|
+
outputPath,
|
|
4737
|
+
width: spriteResult.width,
|
|
4738
|
+
height: spriteResult.height,
|
|
4739
|
+
components: renders.map((r) => r.name),
|
|
4740
|
+
cells: spriteResult.coordinates.length
|
|
4741
|
+
},
|
|
4742
|
+
null,
|
|
4743
|
+
2
|
|
4744
|
+
)}
|
|
4745
|
+
`
|
|
4746
|
+
);
|
|
4747
|
+
} else {
|
|
4748
|
+
process.stdout.write(
|
|
4749
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
4750
|
+
`
|
|
4751
|
+
);
|
|
4752
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
4753
|
+
`);
|
|
4754
|
+
}
|
|
4755
|
+
} catch (err) {
|
|
4756
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4757
|
+
`);
|
|
4758
|
+
process.exit(1);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
);
|
|
4762
|
+
}
|
|
4763
|
+
|
|
3790
4764
|
// src/tokens/commands.ts
|
|
3791
4765
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3792
4766
|
var CONFIG_FILE2 = "reactscope.config.json";
|
|
@@ -3807,32 +4781,32 @@ function buildTable2(headers, rows) {
|
|
|
3807
4781
|
);
|
|
3808
4782
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
3809
4783
|
}
|
|
3810
|
-
function
|
|
4784
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3811
4785
|
if (fileFlag !== void 0) {
|
|
3812
|
-
return
|
|
4786
|
+
return resolve13(process.cwd(), fileFlag);
|
|
3813
4787
|
}
|
|
3814
|
-
const configPath =
|
|
3815
|
-
if (
|
|
4788
|
+
const configPath = resolve13(process.cwd(), CONFIG_FILE2);
|
|
4789
|
+
if (existsSync9(configPath)) {
|
|
3816
4790
|
try {
|
|
3817
|
-
const raw =
|
|
4791
|
+
const raw = readFileSync8(configPath, "utf-8");
|
|
3818
4792
|
const config = JSON.parse(raw);
|
|
3819
4793
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3820
4794
|
const file = config.tokens.file;
|
|
3821
|
-
return
|
|
4795
|
+
return resolve13(process.cwd(), file);
|
|
3822
4796
|
}
|
|
3823
4797
|
} catch {
|
|
3824
4798
|
}
|
|
3825
4799
|
}
|
|
3826
|
-
return
|
|
4800
|
+
return resolve13(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
3827
4801
|
}
|
|
3828
4802
|
function loadTokens(absPath) {
|
|
3829
|
-
if (!
|
|
4803
|
+
if (!existsSync9(absPath)) {
|
|
3830
4804
|
throw new Error(
|
|
3831
4805
|
`Token file not found at ${absPath}.
|
|
3832
4806
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3833
4807
|
);
|
|
3834
4808
|
}
|
|
3835
|
-
const raw =
|
|
4809
|
+
const raw = readFileSync8(absPath, "utf-8");
|
|
3836
4810
|
return parseTokenFileSync2(raw);
|
|
3837
4811
|
}
|
|
3838
4812
|
function getRawValue(node, segments) {
|
|
@@ -3870,9 +4844,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
3870
4844
|
function registerGet2(tokensCmd) {
|
|
3871
4845
|
tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
3872
4846
|
try {
|
|
3873
|
-
const filePath =
|
|
4847
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3874
4848
|
const { tokens } = loadTokens(filePath);
|
|
3875
|
-
const resolver = new
|
|
4849
|
+
const resolver = new TokenResolver7(tokens);
|
|
3876
4850
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
3877
4851
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
3878
4852
|
if (useJson) {
|
|
@@ -3896,9 +4870,9 @@ function registerList2(tokensCmd) {
|
|
|
3896
4870
|
tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
3897
4871
|
(category, opts) => {
|
|
3898
4872
|
try {
|
|
3899
|
-
const filePath =
|
|
4873
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3900
4874
|
const { tokens } = loadTokens(filePath);
|
|
3901
|
-
const resolver = new
|
|
4875
|
+
const resolver = new TokenResolver7(tokens);
|
|
3902
4876
|
const filtered = resolver.list(opts.type, category);
|
|
3903
4877
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3904
4878
|
if (useJson) {
|
|
@@ -3926,9 +4900,9 @@ function registerSearch(tokensCmd) {
|
|
|
3926
4900
|
tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
3927
4901
|
(value, opts) => {
|
|
3928
4902
|
try {
|
|
3929
|
-
const filePath =
|
|
4903
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3930
4904
|
const { tokens } = loadTokens(filePath);
|
|
3931
|
-
const resolver = new
|
|
4905
|
+
const resolver = new TokenResolver7(tokens);
|
|
3932
4906
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3933
4907
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
3934
4908
|
"color",
|
|
@@ -4008,10 +4982,10 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
4008
4982
|
function registerResolve(tokensCmd) {
|
|
4009
4983
|
tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
4010
4984
|
try {
|
|
4011
|
-
const filePath =
|
|
4985
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4012
4986
|
const absFilePath = filePath;
|
|
4013
4987
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
4014
|
-
const resolver = new
|
|
4988
|
+
const resolver = new TokenResolver7(tokens);
|
|
4015
4989
|
resolver.resolve(tokenPath);
|
|
4016
4990
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
4017
4991
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -4045,14 +5019,14 @@ function registerValidate(tokensCmd) {
|
|
|
4045
5019
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
4046
5020
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4047
5021
|
try {
|
|
4048
|
-
const filePath =
|
|
4049
|
-
if (!
|
|
5022
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
5023
|
+
if (!existsSync9(filePath)) {
|
|
4050
5024
|
throw new Error(
|
|
4051
5025
|
`Token file not found at ${filePath}.
|
|
4052
5026
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
4053
5027
|
);
|
|
4054
5028
|
}
|
|
4055
|
-
const raw =
|
|
5029
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
4056
5030
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
4057
5031
|
const errors = [];
|
|
4058
5032
|
let parsed;
|
|
@@ -4129,6 +5103,9 @@ function createTokensCommand() {
|
|
|
4129
5103
|
registerResolve(tokensCmd);
|
|
4130
5104
|
registerValidate(tokensCmd);
|
|
4131
5105
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
5106
|
+
registerCompliance(tokensCmd);
|
|
5107
|
+
registerImpact(tokensCmd);
|
|
5108
|
+
registerPreview(tokensCmd);
|
|
4132
5109
|
return tokensCmd;
|
|
4133
5110
|
}
|
|
4134
5111
|
|
|
@@ -4207,7 +5184,7 @@ function createProgram(options = {}) {
|
|
|
4207
5184
|
}
|
|
4208
5185
|
);
|
|
4209
5186
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
4210
|
-
const raw =
|
|
5187
|
+
const raw = readFileSync9(tracePath, "utf-8");
|
|
4211
5188
|
const trace = loadTrace(raw);
|
|
4212
5189
|
const source = generateTest(trace, {
|
|
4213
5190
|
description: opts.description,
|
|
@@ -4224,6 +5201,7 @@ function createProgram(options = {}) {
|
|
|
4224
5201
|
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
4225
5202
|
if (existingReportCmd !== void 0) {
|
|
4226
5203
|
registerBaselineSubCommand(existingReportCmd);
|
|
5204
|
+
registerDiffSubCommand(existingReportCmd);
|
|
4227
5205
|
}
|
|
4228
5206
|
return program2;
|
|
4229
5207
|
}
|