@agent-scope/cli 1.12.0 → 1.13.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 +518 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +478 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +478 -4
- 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 readFileSync8 } 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((resolve12) => {
|
|
259
259
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
260
|
+
resolve12(answer.trim());
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -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: readFileSync9 } = 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(readFileSync9(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,25 +4144,25 @@ 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 existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4148
|
+
import { resolve as resolve11 } from "path";
|
|
3669
4149
|
import {
|
|
3670
4150
|
parseTokenFileSync as parseTokenFileSync2,
|
|
3671
4151
|
TokenParseError,
|
|
3672
|
-
TokenResolver as
|
|
4152
|
+
TokenResolver as TokenResolver4,
|
|
3673
4153
|
TokenValidationError,
|
|
3674
4154
|
validateTokenFile
|
|
3675
4155
|
} from "@agent-scope/tokens";
|
|
3676
4156
|
import { Command as Command7 } from "commander";
|
|
3677
4157
|
|
|
3678
4158
|
// src/tokens/export.ts
|
|
3679
|
-
import { existsSync as
|
|
3680
|
-
import { resolve as
|
|
4159
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4160
|
+
import { resolve as resolve10 } from "path";
|
|
3681
4161
|
import {
|
|
3682
4162
|
exportTokens,
|
|
3683
4163
|
parseTokenFileSync,
|
|
3684
4164
|
ThemeResolver,
|
|
3685
|
-
TokenResolver as
|
|
4165
|
+
TokenResolver as TokenResolver3
|
|
3686
4166
|
} from "@agent-scope/tokens";
|
|
3687
4167
|
import { Command as Command6 } from "commander";
|
|
3688
4168
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
@@ -3690,21 +4170,21 @@ var CONFIG_FILE = "reactscope.config.json";
|
|
|
3690
4170
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3691
4171
|
function resolveTokenFilePath(fileFlag) {
|
|
3692
4172
|
if (fileFlag !== void 0) {
|
|
3693
|
-
return
|
|
4173
|
+
return resolve10(process.cwd(), fileFlag);
|
|
3694
4174
|
}
|
|
3695
|
-
const configPath =
|
|
3696
|
-
if (
|
|
4175
|
+
const configPath = resolve10(process.cwd(), CONFIG_FILE);
|
|
4176
|
+
if (existsSync7(configPath)) {
|
|
3697
4177
|
try {
|
|
3698
|
-
const raw =
|
|
4178
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
3699
4179
|
const config = JSON.parse(raw);
|
|
3700
4180
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3701
4181
|
const file = config.tokens.file;
|
|
3702
|
-
return
|
|
4182
|
+
return resolve10(process.cwd(), file);
|
|
3703
4183
|
}
|
|
3704
4184
|
} catch {
|
|
3705
4185
|
}
|
|
3706
4186
|
}
|
|
3707
|
-
return
|
|
4187
|
+
return resolve10(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3708
4188
|
}
|
|
3709
4189
|
function createTokensExportCommand() {
|
|
3710
4190
|
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(
|
|
@@ -3723,13 +4203,13 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
3723
4203
|
const format = opts.format;
|
|
3724
4204
|
try {
|
|
3725
4205
|
const filePath = resolveTokenFilePath(opts.file);
|
|
3726
|
-
if (!
|
|
4206
|
+
if (!existsSync7(filePath)) {
|
|
3727
4207
|
throw new Error(
|
|
3728
4208
|
`Token file not found at ${filePath}.
|
|
3729
4209
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3730
4210
|
);
|
|
3731
4211
|
}
|
|
3732
|
-
const raw =
|
|
4212
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
3733
4213
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3734
4214
|
let themesMap;
|
|
3735
4215
|
if (opts.theme !== void 0) {
|
|
@@ -3740,7 +4220,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
3740
4220
|
Available themes: ${available}`
|
|
3741
4221
|
);
|
|
3742
4222
|
}
|
|
3743
|
-
const baseResolver = new
|
|
4223
|
+
const baseResolver = new TokenResolver3(tokens);
|
|
3744
4224
|
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3745
4225
|
baseResolver,
|
|
3746
4226
|
rawFile
|
|
@@ -3768,8 +4248,8 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
3768
4248
|
themes: themesMap
|
|
3769
4249
|
});
|
|
3770
4250
|
if (opts.out !== void 0) {
|
|
3771
|
-
const outPath =
|
|
3772
|
-
|
|
4251
|
+
const outPath = resolve10(process.cwd(), opts.out);
|
|
4252
|
+
writeFileSync7(outPath, output, "utf-8");
|
|
3773
4253
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3774
4254
|
`);
|
|
3775
4255
|
} else {
|
|
@@ -3809,30 +4289,30 @@ function buildTable2(headers, rows) {
|
|
|
3809
4289
|
}
|
|
3810
4290
|
function resolveTokenFilePath2(fileFlag) {
|
|
3811
4291
|
if (fileFlag !== void 0) {
|
|
3812
|
-
return
|
|
4292
|
+
return resolve11(process.cwd(), fileFlag);
|
|
3813
4293
|
}
|
|
3814
|
-
const configPath =
|
|
3815
|
-
if (
|
|
4294
|
+
const configPath = resolve11(process.cwd(), CONFIG_FILE2);
|
|
4295
|
+
if (existsSync8(configPath)) {
|
|
3816
4296
|
try {
|
|
3817
|
-
const raw =
|
|
4297
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
3818
4298
|
const config = JSON.parse(raw);
|
|
3819
4299
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3820
4300
|
const file = config.tokens.file;
|
|
3821
|
-
return
|
|
4301
|
+
return resolve11(process.cwd(), file);
|
|
3822
4302
|
}
|
|
3823
4303
|
} catch {
|
|
3824
4304
|
}
|
|
3825
4305
|
}
|
|
3826
|
-
return
|
|
4306
|
+
return resolve11(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
3827
4307
|
}
|
|
3828
4308
|
function loadTokens(absPath) {
|
|
3829
|
-
if (!
|
|
4309
|
+
if (!existsSync8(absPath)) {
|
|
3830
4310
|
throw new Error(
|
|
3831
4311
|
`Token file not found at ${absPath}.
|
|
3832
4312
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3833
4313
|
);
|
|
3834
4314
|
}
|
|
3835
|
-
const raw =
|
|
4315
|
+
const raw = readFileSync7(absPath, "utf-8");
|
|
3836
4316
|
return parseTokenFileSync2(raw);
|
|
3837
4317
|
}
|
|
3838
4318
|
function getRawValue(node, segments) {
|
|
@@ -3872,7 +4352,7 @@ function registerGet2(tokensCmd) {
|
|
|
3872
4352
|
try {
|
|
3873
4353
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3874
4354
|
const { tokens } = loadTokens(filePath);
|
|
3875
|
-
const resolver = new
|
|
4355
|
+
const resolver = new TokenResolver4(tokens);
|
|
3876
4356
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
3877
4357
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
3878
4358
|
if (useJson) {
|
|
@@ -3898,7 +4378,7 @@ function registerList2(tokensCmd) {
|
|
|
3898
4378
|
try {
|
|
3899
4379
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3900
4380
|
const { tokens } = loadTokens(filePath);
|
|
3901
|
-
const resolver = new
|
|
4381
|
+
const resolver = new TokenResolver4(tokens);
|
|
3902
4382
|
const filtered = resolver.list(opts.type, category);
|
|
3903
4383
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3904
4384
|
if (useJson) {
|
|
@@ -3928,7 +4408,7 @@ function registerSearch(tokensCmd) {
|
|
|
3928
4408
|
try {
|
|
3929
4409
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3930
4410
|
const { tokens } = loadTokens(filePath);
|
|
3931
|
-
const resolver = new
|
|
4411
|
+
const resolver = new TokenResolver4(tokens);
|
|
3932
4412
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3933
4413
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
3934
4414
|
"color",
|
|
@@ -4011,7 +4491,7 @@ function registerResolve(tokensCmd) {
|
|
|
4011
4491
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
4012
4492
|
const absFilePath = filePath;
|
|
4013
4493
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
4014
|
-
const resolver = new
|
|
4494
|
+
const resolver = new TokenResolver4(tokens);
|
|
4015
4495
|
resolver.resolve(tokenPath);
|
|
4016
4496
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
4017
4497
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -4046,13 +4526,13 @@ function registerValidate(tokensCmd) {
|
|
|
4046
4526
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4047
4527
|
try {
|
|
4048
4528
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
4049
|
-
if (!
|
|
4529
|
+
if (!existsSync8(filePath)) {
|
|
4050
4530
|
throw new Error(
|
|
4051
4531
|
`Token file not found at ${filePath}.
|
|
4052
4532
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
4053
4533
|
);
|
|
4054
4534
|
}
|
|
4055
|
-
const raw =
|
|
4535
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
4056
4536
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
4057
4537
|
const errors = [];
|
|
4058
4538
|
let parsed;
|
|
@@ -4207,7 +4687,7 @@ function createProgram(options = {}) {
|
|
|
4207
4687
|
}
|
|
4208
4688
|
);
|
|
4209
4689
|
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 =
|
|
4690
|
+
const raw = readFileSync8(tracePath, "utf-8");
|
|
4211
4691
|
const trace = loadTrace(raw);
|
|
4212
4692
|
const source = generateTest(trace, {
|
|
4213
4693
|
description: opts.description,
|
|
@@ -4224,6 +4704,7 @@ function createProgram(options = {}) {
|
|
|
4224
4704
|
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
4225
4705
|
if (existingReportCmd !== void 0) {
|
|
4226
4706
|
registerBaselineSubCommand(existingReportCmd);
|
|
4707
|
+
registerDiffSubCommand(existingReportCmd);
|
|
4227
4708
|
}
|
|
4228
4709
|
return program2;
|
|
4229
4710
|
}
|