@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/index.cjs
CHANGED
|
@@ -228,9 +228,9 @@ function createRL() {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
async function ask(rl, question) {
|
|
231
|
-
return new Promise((
|
|
231
|
+
return new Promise((resolve12) => {
|
|
232
232
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
233
|
+
resolve12(answer.trim());
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -3166,12 +3166,12 @@ async function runBaseline(options = {}) {
|
|
|
3166
3166
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3167
3167
|
let manifest$1;
|
|
3168
3168
|
if (manifestPath !== void 0) {
|
|
3169
|
-
const { readFileSync:
|
|
3169
|
+
const { readFileSync: readFileSync9 } = await import('fs');
|
|
3170
3170
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3171
3171
|
if (!fs.existsSync(absPath)) {
|
|
3172
3172
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3173
3173
|
}
|
|
3174
|
-
manifest$1 = JSON.parse(
|
|
3174
|
+
manifest$1 = JSON.parse(readFileSync9(absPath, "utf-8"));
|
|
3175
3175
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3176
3176
|
`);
|
|
3177
3177
|
} else {
|
|
@@ -3332,6 +3332,479 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3332
3332
|
}
|
|
3333
3333
|
);
|
|
3334
3334
|
}
|
|
3335
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3336
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3337
|
+
const compliancePath = path.resolve(baselineDir, "compliance.json");
|
|
3338
|
+
if (!fs.existsSync(compliancePath)) return null;
|
|
3339
|
+
const raw = JSON.parse(fs.readFileSync(compliancePath, "utf-8"));
|
|
3340
|
+
return raw;
|
|
3341
|
+
}
|
|
3342
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3343
|
+
const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3344
|
+
if (!fs.existsSync(jsonPath)) return null;
|
|
3345
|
+
return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
3346
|
+
}
|
|
3347
|
+
var _pool5 = null;
|
|
3348
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3349
|
+
if (_pool5 === null) {
|
|
3350
|
+
_pool5 = new render.BrowserPool({
|
|
3351
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3352
|
+
viewportWidth,
|
|
3353
|
+
viewportHeight
|
|
3354
|
+
});
|
|
3355
|
+
await _pool5.init();
|
|
3356
|
+
}
|
|
3357
|
+
return _pool5;
|
|
3358
|
+
}
|
|
3359
|
+
async function shutdownPool5() {
|
|
3360
|
+
if (_pool5 !== null) {
|
|
3361
|
+
await _pool5.close();
|
|
3362
|
+
_pool5 = null;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3366
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3367
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3368
|
+
const slot = await pool.acquire();
|
|
3369
|
+
const { page } = slot;
|
|
3370
|
+
try {
|
|
3371
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3372
|
+
await page.waitForFunction(
|
|
3373
|
+
() => {
|
|
3374
|
+
const w = window;
|
|
3375
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3376
|
+
},
|
|
3377
|
+
{ timeout: 15e3 }
|
|
3378
|
+
);
|
|
3379
|
+
const renderError = await page.evaluate(() => {
|
|
3380
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3381
|
+
});
|
|
3382
|
+
if (renderError !== null) {
|
|
3383
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3384
|
+
}
|
|
3385
|
+
const rootDir = process.cwd();
|
|
3386
|
+
const classes = await page.evaluate(() => {
|
|
3387
|
+
const set = /* @__PURE__ */ new Set();
|
|
3388
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3389
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3390
|
+
if (c) set.add(c);
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
return [...set];
|
|
3394
|
+
});
|
|
3395
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3396
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3397
|
+
await page.addStyleTag({ content: projectCss });
|
|
3398
|
+
}
|
|
3399
|
+
const startMs = performance.now();
|
|
3400
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3401
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3402
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3403
|
+
throw new Error(
|
|
3404
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
const PAD = 24;
|
|
3408
|
+
const MIN_W = 320;
|
|
3409
|
+
const MIN_H = 200;
|
|
3410
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3411
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3412
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3413
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3414
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3415
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3416
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3417
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3418
|
+
const screenshot = await page.screenshot({
|
|
3419
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3420
|
+
type: "png"
|
|
3421
|
+
});
|
|
3422
|
+
const computedStylesRaw = {};
|
|
3423
|
+
const styles = await page.evaluate((sel) => {
|
|
3424
|
+
const el = document.querySelector(sel);
|
|
3425
|
+
if (el === null) return {};
|
|
3426
|
+
const computed = window.getComputedStyle(el);
|
|
3427
|
+
const out = {};
|
|
3428
|
+
for (const prop of [
|
|
3429
|
+
"display",
|
|
3430
|
+
"width",
|
|
3431
|
+
"height",
|
|
3432
|
+
"color",
|
|
3433
|
+
"backgroundColor",
|
|
3434
|
+
"fontSize",
|
|
3435
|
+
"fontFamily",
|
|
3436
|
+
"padding",
|
|
3437
|
+
"margin"
|
|
3438
|
+
]) {
|
|
3439
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3440
|
+
}
|
|
3441
|
+
return out;
|
|
3442
|
+
}, "[data-reactscope-root] > *");
|
|
3443
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3444
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3445
|
+
return {
|
|
3446
|
+
screenshot,
|
|
3447
|
+
width: Math.round(safeW),
|
|
3448
|
+
height: Math.round(safeH),
|
|
3449
|
+
renderTimeMs,
|
|
3450
|
+
computedStyles: computedStylesRaw
|
|
3451
|
+
};
|
|
3452
|
+
} finally {
|
|
3453
|
+
pool.release(slot);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3457
|
+
const flat = {};
|
|
3458
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3459
|
+
Object.assign(flat, styles);
|
|
3460
|
+
}
|
|
3461
|
+
const colors = {};
|
|
3462
|
+
const spacing = {};
|
|
3463
|
+
const typography = {};
|
|
3464
|
+
const borders = {};
|
|
3465
|
+
const shadows = {};
|
|
3466
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3467
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3468
|
+
colors[prop] = value;
|
|
3469
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3470
|
+
spacing[prop] = value;
|
|
3471
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3472
|
+
typography[prop] = value;
|
|
3473
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3474
|
+
borders[prop] = value;
|
|
3475
|
+
} else if (prop === "boxShadow") {
|
|
3476
|
+
shadows[prop] = value;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3480
|
+
}
|
|
3481
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3482
|
+
if (entry.renderFailed) return "unchanged";
|
|
3483
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3484
|
+
return "added";
|
|
3485
|
+
}
|
|
3486
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3487
|
+
return "removed";
|
|
3488
|
+
}
|
|
3489
|
+
const delta = entry.complianceDelta;
|
|
3490
|
+
if (delta !== null) {
|
|
3491
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3492
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3493
|
+
}
|
|
3494
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3495
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3496
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3497
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3498
|
+
}
|
|
3499
|
+
return "unchanged";
|
|
3500
|
+
}
|
|
3501
|
+
async function runDiff(options = {}) {
|
|
3502
|
+
const {
|
|
3503
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3504
|
+
componentsGlob,
|
|
3505
|
+
manifestPath,
|
|
3506
|
+
viewportWidth = 375,
|
|
3507
|
+
viewportHeight = 812,
|
|
3508
|
+
regressionThreshold = 0.01
|
|
3509
|
+
} = options;
|
|
3510
|
+
const startTime = performance.now();
|
|
3511
|
+
const rootDir = process.cwd();
|
|
3512
|
+
const baselineDir = path.resolve(rootDir, baselineDirRaw);
|
|
3513
|
+
if (!fs.existsSync(baselineDir)) {
|
|
3514
|
+
throw new Error(
|
|
3515
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
const baselineManifestPath = path.resolve(baselineDir, "manifest.json");
|
|
3519
|
+
if (!fs.existsSync(baselineManifestPath)) {
|
|
3520
|
+
throw new Error(
|
|
3521
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
const baselineManifest = JSON.parse(fs.readFileSync(baselineManifestPath, "utf-8"));
|
|
3525
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3526
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3527
|
+
process.stderr.write(
|
|
3528
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3529
|
+
`
|
|
3530
|
+
);
|
|
3531
|
+
let currentManifest;
|
|
3532
|
+
if (manifestPath !== void 0) {
|
|
3533
|
+
const absPath = path.resolve(rootDir, manifestPath);
|
|
3534
|
+
if (!fs.existsSync(absPath)) {
|
|
3535
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3536
|
+
}
|
|
3537
|
+
currentManifest = JSON.parse(fs.readFileSync(absPath, "utf-8"));
|
|
3538
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3539
|
+
`);
|
|
3540
|
+
} else {
|
|
3541
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3542
|
+
currentManifest = await manifest.generateManifest({ rootDir });
|
|
3543
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3544
|
+
process.stderr.write(`Found ${count} components.
|
|
3545
|
+
`);
|
|
3546
|
+
}
|
|
3547
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3548
|
+
if (componentsGlob !== void 0) {
|
|
3549
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3550
|
+
process.stderr.write(
|
|
3551
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3552
|
+
`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3556
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3557
|
+
);
|
|
3558
|
+
const total = componentNames.length;
|
|
3559
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3560
|
+
`);
|
|
3561
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3562
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3563
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3564
|
+
let completed = 0;
|
|
3565
|
+
const CONCURRENCY = 4;
|
|
3566
|
+
let nextIdx = 0;
|
|
3567
|
+
const renderOne = async (name) => {
|
|
3568
|
+
const descriptor = currentManifest.components[name];
|
|
3569
|
+
if (descriptor === void 0) return;
|
|
3570
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3571
|
+
const outcome = await render.safeRender(
|
|
3572
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3573
|
+
{
|
|
3574
|
+
props: {},
|
|
3575
|
+
sourceLocation: {
|
|
3576
|
+
file: descriptor.filePath,
|
|
3577
|
+
line: descriptor.loc.start,
|
|
3578
|
+
column: 0
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
);
|
|
3582
|
+
completed++;
|
|
3583
|
+
const pct = Math.round(completed / total * 100);
|
|
3584
|
+
if (isTTY()) {
|
|
3585
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3586
|
+
}
|
|
3587
|
+
if (outcome.crashed) {
|
|
3588
|
+
renderFailures.add(name);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
const result = outcome.result;
|
|
3592
|
+
currentRenderMeta.set(name, {
|
|
3593
|
+
width: result.width,
|
|
3594
|
+
height: result.height,
|
|
3595
|
+
renderTimeMs: result.renderTimeMs
|
|
3596
|
+
});
|
|
3597
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3598
|
+
};
|
|
3599
|
+
if (total > 0) {
|
|
3600
|
+
const worker = async () => {
|
|
3601
|
+
while (nextIdx < componentNames.length) {
|
|
3602
|
+
const i = nextIdx++;
|
|
3603
|
+
const name = componentNames[i];
|
|
3604
|
+
if (name !== void 0) {
|
|
3605
|
+
await renderOne(name);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
const workers = [];
|
|
3610
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3611
|
+
workers.push(worker());
|
|
3612
|
+
}
|
|
3613
|
+
await Promise.all(workers);
|
|
3614
|
+
}
|
|
3615
|
+
await shutdownPool5();
|
|
3616
|
+
if (isTTY() && total > 0) {
|
|
3617
|
+
process.stderr.write("\n");
|
|
3618
|
+
}
|
|
3619
|
+
const resolver = new tokens.TokenResolver([]);
|
|
3620
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
3621
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3622
|
+
const entries = [];
|
|
3623
|
+
for (const name of componentNames) {
|
|
3624
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3625
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3626
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3627
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3628
|
+
const failed = renderFailures.has(name);
|
|
3629
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3630
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3631
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3632
|
+
const partial = {
|
|
3633
|
+
name,
|
|
3634
|
+
baselineCompliance: baselineComplianceScore,
|
|
3635
|
+
currentCompliance: currentComplianceScore,
|
|
3636
|
+
complianceDelta: delta,
|
|
3637
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3638
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3639
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3640
|
+
renderFailed: failed
|
|
3641
|
+
};
|
|
3642
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3643
|
+
}
|
|
3644
|
+
for (const name of removedNames) {
|
|
3645
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3646
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3647
|
+
entries.push({
|
|
3648
|
+
name,
|
|
3649
|
+
status: "removed",
|
|
3650
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3651
|
+
currentCompliance: null,
|
|
3652
|
+
complianceDelta: null,
|
|
3653
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3654
|
+
currentDimensions: null,
|
|
3655
|
+
renderTimeMs: null,
|
|
3656
|
+
renderFailed: false
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
const summary = {
|
|
3660
|
+
total: entries.length,
|
|
3661
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3662
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3663
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3664
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3665
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3666
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3667
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3668
|
+
};
|
|
3669
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3670
|
+
const wallClockMs = performance.now() - startTime;
|
|
3671
|
+
return {
|
|
3672
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3673
|
+
baselineDir,
|
|
3674
|
+
summary,
|
|
3675
|
+
components: entries,
|
|
3676
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3677
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3678
|
+
hasRegressions,
|
|
3679
|
+
wallClockMs
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
var STATUS_ICON = {
|
|
3683
|
+
added: "+",
|
|
3684
|
+
removed: "-",
|
|
3685
|
+
unchanged: " ",
|
|
3686
|
+
compliance_regressed: "\u2193",
|
|
3687
|
+
compliance_improved: "\u2191",
|
|
3688
|
+
size_changed: "~"
|
|
3689
|
+
};
|
|
3690
|
+
var STATUS_LABEL = {
|
|
3691
|
+
added: "added",
|
|
3692
|
+
removed: "removed",
|
|
3693
|
+
unchanged: "ok",
|
|
3694
|
+
compliance_regressed: "regressed",
|
|
3695
|
+
compliance_improved: "improved",
|
|
3696
|
+
size_changed: "size changed"
|
|
3697
|
+
};
|
|
3698
|
+
function formatDiffReport(result) {
|
|
3699
|
+
const lines = [];
|
|
3700
|
+
const title = "Scope Report Diff";
|
|
3701
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3702
|
+
lines.push(title, rule2);
|
|
3703
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3704
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3705
|
+
lines.push(
|
|
3706
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3707
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3708
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3709
|
+
""
|
|
3710
|
+
);
|
|
3711
|
+
const s = result.summary;
|
|
3712
|
+
lines.push(
|
|
3713
|
+
`Components: ${s.total} total ` + [
|
|
3714
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3715
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3716
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3717
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3718
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3719
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3720
|
+
].filter(Boolean).join(" "),
|
|
3721
|
+
""
|
|
3722
|
+
);
|
|
3723
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3724
|
+
if (notable.length === 0) {
|
|
3725
|
+
lines.push(" No changes detected.");
|
|
3726
|
+
} else {
|
|
3727
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3728
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3729
|
+
const divider = "-".repeat(header.length);
|
|
3730
|
+
lines.push(header, divider);
|
|
3731
|
+
for (const entry of notable) {
|
|
3732
|
+
const icon = STATUS_ICON[entry.status];
|
|
3733
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3734
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3735
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3736
|
+
if (entry.complianceDelta !== null) {
|
|
3737
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3738
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3739
|
+
}
|
|
3740
|
+
let dimStr = "\u2014";
|
|
3741
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3742
|
+
const b = entry.baselineDimensions;
|
|
3743
|
+
const c = entry.currentDimensions;
|
|
3744
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3745
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3746
|
+
} else {
|
|
3747
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3748
|
+
}
|
|
3749
|
+
} else if (entry.currentDimensions !== null) {
|
|
3750
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3751
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3752
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3753
|
+
}
|
|
3754
|
+
if (entry.renderFailed) {
|
|
3755
|
+
dimStr = "render failed";
|
|
3756
|
+
}
|
|
3757
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
lines.push(
|
|
3761
|
+
"",
|
|
3762
|
+
rule2,
|
|
3763
|
+
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`
|
|
3764
|
+
);
|
|
3765
|
+
return lines.join("\n");
|
|
3766
|
+
}
|
|
3767
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3768
|
+
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(
|
|
3769
|
+
"--regression-threshold <n>",
|
|
3770
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3771
|
+
"0.01"
|
|
3772
|
+
).action(
|
|
3773
|
+
async (opts) => {
|
|
3774
|
+
try {
|
|
3775
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3776
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3777
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3778
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3779
|
+
const result = await runDiff({
|
|
3780
|
+
baselineDir: opts.baseline,
|
|
3781
|
+
componentsGlob: opts.components,
|
|
3782
|
+
manifestPath: opts.manifest,
|
|
3783
|
+
viewportWidth,
|
|
3784
|
+
viewportHeight,
|
|
3785
|
+
regressionThreshold
|
|
3786
|
+
});
|
|
3787
|
+
if (opts.output !== void 0) {
|
|
3788
|
+
fs.writeFileSync(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3789
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3790
|
+
`);
|
|
3791
|
+
}
|
|
3792
|
+
if (opts.json) {
|
|
3793
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3794
|
+
`);
|
|
3795
|
+
} else {
|
|
3796
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3797
|
+
`);
|
|
3798
|
+
}
|
|
3799
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3800
|
+
} catch (err) {
|
|
3801
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3802
|
+
`);
|
|
3803
|
+
process.exit(2);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3335
3808
|
|
|
3336
3809
|
// src/tree-formatter.ts
|
|
3337
3810
|
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
@@ -4149,6 +4622,7 @@ function createProgram(options = {}) {
|
|
|
4149
4622
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
4150
4623
|
if (existingReportCmd !== void 0) {
|
|
4151
4624
|
registerBaselineSubCommand(existingReportCmd);
|
|
4625
|
+
registerDiffSubCommand(existingReportCmd);
|
|
4152
4626
|
}
|
|
4153
4627
|
return program;
|
|
4154
4628
|
}
|