@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/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rea
|
|
|
2
2
|
import { join, resolve, dirname } from 'path';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix,
|
|
5
|
+
import { SpriteSheetGenerator, BrowserPool, safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
6
6
|
import * as esbuild from 'esbuild';
|
|
7
7
|
import { generateManifest } from '@agent-scope/manifest';
|
|
8
8
|
import { chromium } from 'playwright';
|
|
9
9
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
|
-
import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine } from '@agent-scope/tokens';
|
|
11
|
+
import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
12
12
|
|
|
13
13
|
// src/init/index.ts
|
|
14
14
|
function hasConfigFile(dir, stem) {
|
|
@@ -205,9 +205,9 @@ function createRL() {
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
async function ask(rl, question) {
|
|
208
|
-
return new Promise((
|
|
208
|
+
return new Promise((resolve14) => {
|
|
209
209
|
rl.question(question, (answer) => {
|
|
210
|
-
|
|
210
|
+
resolve14(answer.trim());
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
}
|
|
@@ -356,7 +356,7 @@ function createInitCommand() {
|
|
|
356
356
|
}
|
|
357
357
|
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
358
358
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
359
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
359
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
360
360
|
}
|
|
361
361
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
362
362
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -443,7 +443,9 @@ ${msg}`);
|
|
|
443
443
|
return outputFile.text;
|
|
444
444
|
}
|
|
445
445
|
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
446
|
-
const projectStyleBlock = ""
|
|
446
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
447
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
448
|
+
</style>` : "";
|
|
447
449
|
return `<!DOCTYPE html>
|
|
448
450
|
<html lang="en">
|
|
449
451
|
<head>
|
|
@@ -2807,8 +2809,8 @@ Available: ${available}`
|
|
|
2807
2809
|
`
|
|
2808
2810
|
);
|
|
2809
2811
|
if (opts.sprite !== void 0) {
|
|
2810
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2811
|
-
const gen = new
|
|
2812
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2813
|
+
const gen = new SpriteSheetGenerator2();
|
|
2812
2814
|
const sheet = await gen.generate(result);
|
|
2813
2815
|
const spritePath = resolve(process.cwd(), opts.sprite);
|
|
2814
2816
|
writeFileSync(spritePath, sheet.png);
|
|
@@ -2817,8 +2819,8 @@ Available: ${available}`
|
|
|
2817
2819
|
}
|
|
2818
2820
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2819
2821
|
if (fmt === "file") {
|
|
2820
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2821
|
-
const gen = new
|
|
2822
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2823
|
+
const gen = new SpriteSheetGenerator2();
|
|
2822
2824
|
const sheet = await gen.generate(result);
|
|
2823
2825
|
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2824
2826
|
mkdirSync(dir, { recursive: true });
|
|
@@ -2835,8 +2837,8 @@ Available: ${available}`
|
|
|
2835
2837
|
} else if (fmt === "png") {
|
|
2836
2838
|
if (opts.sprite !== void 0) {
|
|
2837
2839
|
} else {
|
|
2838
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2839
|
-
const gen = new
|
|
2840
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2841
|
+
const gen = new SpriteSheetGenerator2();
|
|
2840
2842
|
const sheet = await gen.generate(result);
|
|
2841
2843
|
process.stdout.write(sheet.png);
|
|
2842
2844
|
}
|
|
@@ -3143,12 +3145,12 @@ async function runBaseline(options = {}) {
|
|
|
3143
3145
|
mkdirSync(rendersDir, { recursive: true });
|
|
3144
3146
|
let manifest;
|
|
3145
3147
|
if (manifestPath !== void 0) {
|
|
3146
|
-
const { readFileSync:
|
|
3148
|
+
const { readFileSync: readFileSync10 } = await import('fs');
|
|
3147
3149
|
const absPath = resolve(rootDir, manifestPath);
|
|
3148
3150
|
if (!existsSync(absPath)) {
|
|
3149
3151
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3150
3152
|
}
|
|
3151
|
-
manifest = JSON.parse(
|
|
3153
|
+
manifest = JSON.parse(readFileSync10(absPath, "utf-8"));
|
|
3152
3154
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3153
3155
|
`);
|
|
3154
3156
|
} else {
|
|
@@ -3309,6 +3311,479 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3309
3311
|
}
|
|
3310
3312
|
);
|
|
3311
3313
|
}
|
|
3314
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3315
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3316
|
+
const compliancePath = resolve(baselineDir, "compliance.json");
|
|
3317
|
+
if (!existsSync(compliancePath)) return null;
|
|
3318
|
+
const raw = JSON.parse(readFileSync(compliancePath, "utf-8"));
|
|
3319
|
+
return raw;
|
|
3320
|
+
}
|
|
3321
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3322
|
+
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3323
|
+
if (!existsSync(jsonPath)) return null;
|
|
3324
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
3325
|
+
}
|
|
3326
|
+
var _pool5 = null;
|
|
3327
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3328
|
+
if (_pool5 === null) {
|
|
3329
|
+
_pool5 = new BrowserPool({
|
|
3330
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3331
|
+
viewportWidth,
|
|
3332
|
+
viewportHeight
|
|
3333
|
+
});
|
|
3334
|
+
await _pool5.init();
|
|
3335
|
+
}
|
|
3336
|
+
return _pool5;
|
|
3337
|
+
}
|
|
3338
|
+
async function shutdownPool5() {
|
|
3339
|
+
if (_pool5 !== null) {
|
|
3340
|
+
await _pool5.close();
|
|
3341
|
+
_pool5 = null;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3345
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3346
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3347
|
+
const slot = await pool.acquire();
|
|
3348
|
+
const { page } = slot;
|
|
3349
|
+
try {
|
|
3350
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3351
|
+
await page.waitForFunction(
|
|
3352
|
+
() => {
|
|
3353
|
+
const w = window;
|
|
3354
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3355
|
+
},
|
|
3356
|
+
{ timeout: 15e3 }
|
|
3357
|
+
);
|
|
3358
|
+
const renderError = await page.evaluate(() => {
|
|
3359
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3360
|
+
});
|
|
3361
|
+
if (renderError !== null) {
|
|
3362
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3363
|
+
}
|
|
3364
|
+
const rootDir = process.cwd();
|
|
3365
|
+
const classes = await page.evaluate(() => {
|
|
3366
|
+
const set = /* @__PURE__ */ new Set();
|
|
3367
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3368
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3369
|
+
if (c) set.add(c);
|
|
3370
|
+
}
|
|
3371
|
+
});
|
|
3372
|
+
return [...set];
|
|
3373
|
+
});
|
|
3374
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3375
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3376
|
+
await page.addStyleTag({ content: projectCss });
|
|
3377
|
+
}
|
|
3378
|
+
const startMs = performance.now();
|
|
3379
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3380
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3381
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3382
|
+
throw new Error(
|
|
3383
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
const PAD = 24;
|
|
3387
|
+
const MIN_W = 320;
|
|
3388
|
+
const MIN_H = 200;
|
|
3389
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3390
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3391
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3392
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3393
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3394
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3395
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3396
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3397
|
+
const screenshot = await page.screenshot({
|
|
3398
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3399
|
+
type: "png"
|
|
3400
|
+
});
|
|
3401
|
+
const computedStylesRaw = {};
|
|
3402
|
+
const styles = await page.evaluate((sel) => {
|
|
3403
|
+
const el = document.querySelector(sel);
|
|
3404
|
+
if (el === null) return {};
|
|
3405
|
+
const computed = window.getComputedStyle(el);
|
|
3406
|
+
const out = {};
|
|
3407
|
+
for (const prop of [
|
|
3408
|
+
"display",
|
|
3409
|
+
"width",
|
|
3410
|
+
"height",
|
|
3411
|
+
"color",
|
|
3412
|
+
"backgroundColor",
|
|
3413
|
+
"fontSize",
|
|
3414
|
+
"fontFamily",
|
|
3415
|
+
"padding",
|
|
3416
|
+
"margin"
|
|
3417
|
+
]) {
|
|
3418
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3419
|
+
}
|
|
3420
|
+
return out;
|
|
3421
|
+
}, "[data-reactscope-root] > *");
|
|
3422
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3423
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3424
|
+
return {
|
|
3425
|
+
screenshot,
|
|
3426
|
+
width: Math.round(safeW),
|
|
3427
|
+
height: Math.round(safeH),
|
|
3428
|
+
renderTimeMs,
|
|
3429
|
+
computedStyles: computedStylesRaw
|
|
3430
|
+
};
|
|
3431
|
+
} finally {
|
|
3432
|
+
pool.release(slot);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3436
|
+
const flat = {};
|
|
3437
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3438
|
+
Object.assign(flat, styles);
|
|
3439
|
+
}
|
|
3440
|
+
const colors = {};
|
|
3441
|
+
const spacing = {};
|
|
3442
|
+
const typography = {};
|
|
3443
|
+
const borders = {};
|
|
3444
|
+
const shadows = {};
|
|
3445
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3446
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3447
|
+
colors[prop] = value;
|
|
3448
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3449
|
+
spacing[prop] = value;
|
|
3450
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3451
|
+
typography[prop] = value;
|
|
3452
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3453
|
+
borders[prop] = value;
|
|
3454
|
+
} else if (prop === "boxShadow") {
|
|
3455
|
+
shadows[prop] = value;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3459
|
+
}
|
|
3460
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3461
|
+
if (entry.renderFailed) return "unchanged";
|
|
3462
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3463
|
+
return "added";
|
|
3464
|
+
}
|
|
3465
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3466
|
+
return "removed";
|
|
3467
|
+
}
|
|
3468
|
+
const delta = entry.complianceDelta;
|
|
3469
|
+
if (delta !== null) {
|
|
3470
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3471
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3472
|
+
}
|
|
3473
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3474
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3475
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3476
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3477
|
+
}
|
|
3478
|
+
return "unchanged";
|
|
3479
|
+
}
|
|
3480
|
+
async function runDiff(options = {}) {
|
|
3481
|
+
const {
|
|
3482
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3483
|
+
componentsGlob,
|
|
3484
|
+
manifestPath,
|
|
3485
|
+
viewportWidth = 375,
|
|
3486
|
+
viewportHeight = 812,
|
|
3487
|
+
regressionThreshold = 0.01
|
|
3488
|
+
} = options;
|
|
3489
|
+
const startTime = performance.now();
|
|
3490
|
+
const rootDir = process.cwd();
|
|
3491
|
+
const baselineDir = resolve(rootDir, baselineDirRaw);
|
|
3492
|
+
if (!existsSync(baselineDir)) {
|
|
3493
|
+
throw new Error(
|
|
3494
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
const baselineManifestPath = resolve(baselineDir, "manifest.json");
|
|
3498
|
+
if (!existsSync(baselineManifestPath)) {
|
|
3499
|
+
throw new Error(
|
|
3500
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
const baselineManifest = JSON.parse(readFileSync(baselineManifestPath, "utf-8"));
|
|
3504
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3505
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3506
|
+
process.stderr.write(
|
|
3507
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3508
|
+
`
|
|
3509
|
+
);
|
|
3510
|
+
let currentManifest;
|
|
3511
|
+
if (manifestPath !== void 0) {
|
|
3512
|
+
const absPath = resolve(rootDir, manifestPath);
|
|
3513
|
+
if (!existsSync(absPath)) {
|
|
3514
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3515
|
+
}
|
|
3516
|
+
currentManifest = JSON.parse(readFileSync(absPath, "utf-8"));
|
|
3517
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3518
|
+
`);
|
|
3519
|
+
} else {
|
|
3520
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3521
|
+
currentManifest = await generateManifest({ rootDir });
|
|
3522
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3523
|
+
process.stderr.write(`Found ${count} components.
|
|
3524
|
+
`);
|
|
3525
|
+
}
|
|
3526
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3527
|
+
if (componentsGlob !== void 0) {
|
|
3528
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3529
|
+
process.stderr.write(
|
|
3530
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3531
|
+
`
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3535
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3536
|
+
);
|
|
3537
|
+
const total = componentNames.length;
|
|
3538
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3539
|
+
`);
|
|
3540
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3541
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3542
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3543
|
+
let completed = 0;
|
|
3544
|
+
const CONCURRENCY = 4;
|
|
3545
|
+
let nextIdx = 0;
|
|
3546
|
+
const renderOne = async (name) => {
|
|
3547
|
+
const descriptor = currentManifest.components[name];
|
|
3548
|
+
if (descriptor === void 0) return;
|
|
3549
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3550
|
+
const outcome = await safeRender(
|
|
3551
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3552
|
+
{
|
|
3553
|
+
props: {},
|
|
3554
|
+
sourceLocation: {
|
|
3555
|
+
file: descriptor.filePath,
|
|
3556
|
+
line: descriptor.loc.start,
|
|
3557
|
+
column: 0
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
);
|
|
3561
|
+
completed++;
|
|
3562
|
+
const pct = Math.round(completed / total * 100);
|
|
3563
|
+
if (isTTY()) {
|
|
3564
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3565
|
+
}
|
|
3566
|
+
if (outcome.crashed) {
|
|
3567
|
+
renderFailures.add(name);
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
const result = outcome.result;
|
|
3571
|
+
currentRenderMeta.set(name, {
|
|
3572
|
+
width: result.width,
|
|
3573
|
+
height: result.height,
|
|
3574
|
+
renderTimeMs: result.renderTimeMs
|
|
3575
|
+
});
|
|
3576
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3577
|
+
};
|
|
3578
|
+
if (total > 0) {
|
|
3579
|
+
const worker = async () => {
|
|
3580
|
+
while (nextIdx < componentNames.length) {
|
|
3581
|
+
const i = nextIdx++;
|
|
3582
|
+
const name = componentNames[i];
|
|
3583
|
+
if (name !== void 0) {
|
|
3584
|
+
await renderOne(name);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3588
|
+
const workers = [];
|
|
3589
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3590
|
+
workers.push(worker());
|
|
3591
|
+
}
|
|
3592
|
+
await Promise.all(workers);
|
|
3593
|
+
}
|
|
3594
|
+
await shutdownPool5();
|
|
3595
|
+
if (isTTY() && total > 0) {
|
|
3596
|
+
process.stderr.write("\n");
|
|
3597
|
+
}
|
|
3598
|
+
const resolver = new TokenResolver([]);
|
|
3599
|
+
const engine = new ComplianceEngine(resolver);
|
|
3600
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3601
|
+
const entries = [];
|
|
3602
|
+
for (const name of componentNames) {
|
|
3603
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3604
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3605
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3606
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3607
|
+
const failed = renderFailures.has(name);
|
|
3608
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3609
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3610
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3611
|
+
const partial = {
|
|
3612
|
+
name,
|
|
3613
|
+
baselineCompliance: baselineComplianceScore,
|
|
3614
|
+
currentCompliance: currentComplianceScore,
|
|
3615
|
+
complianceDelta: delta,
|
|
3616
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3617
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3618
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3619
|
+
renderFailed: failed
|
|
3620
|
+
};
|
|
3621
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3622
|
+
}
|
|
3623
|
+
for (const name of removedNames) {
|
|
3624
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3625
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3626
|
+
entries.push({
|
|
3627
|
+
name,
|
|
3628
|
+
status: "removed",
|
|
3629
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3630
|
+
currentCompliance: null,
|
|
3631
|
+
complianceDelta: null,
|
|
3632
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3633
|
+
currentDimensions: null,
|
|
3634
|
+
renderTimeMs: null,
|
|
3635
|
+
renderFailed: false
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
const summary = {
|
|
3639
|
+
total: entries.length,
|
|
3640
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3641
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3642
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3643
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3644
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3645
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3646
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3647
|
+
};
|
|
3648
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3649
|
+
const wallClockMs = performance.now() - startTime;
|
|
3650
|
+
return {
|
|
3651
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3652
|
+
baselineDir,
|
|
3653
|
+
summary,
|
|
3654
|
+
components: entries,
|
|
3655
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3656
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3657
|
+
hasRegressions,
|
|
3658
|
+
wallClockMs
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
var STATUS_ICON = {
|
|
3662
|
+
added: "+",
|
|
3663
|
+
removed: "-",
|
|
3664
|
+
unchanged: " ",
|
|
3665
|
+
compliance_regressed: "\u2193",
|
|
3666
|
+
compliance_improved: "\u2191",
|
|
3667
|
+
size_changed: "~"
|
|
3668
|
+
};
|
|
3669
|
+
var STATUS_LABEL = {
|
|
3670
|
+
added: "added",
|
|
3671
|
+
removed: "removed",
|
|
3672
|
+
unchanged: "ok",
|
|
3673
|
+
compliance_regressed: "regressed",
|
|
3674
|
+
compliance_improved: "improved",
|
|
3675
|
+
size_changed: "size changed"
|
|
3676
|
+
};
|
|
3677
|
+
function formatDiffReport(result) {
|
|
3678
|
+
const lines = [];
|
|
3679
|
+
const title = "Scope Report Diff";
|
|
3680
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3681
|
+
lines.push(title, rule2);
|
|
3682
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3683
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3684
|
+
lines.push(
|
|
3685
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3686
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3687
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3688
|
+
""
|
|
3689
|
+
);
|
|
3690
|
+
const s = result.summary;
|
|
3691
|
+
lines.push(
|
|
3692
|
+
`Components: ${s.total} total ` + [
|
|
3693
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3694
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3695
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3696
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3697
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3698
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3699
|
+
].filter(Boolean).join(" "),
|
|
3700
|
+
""
|
|
3701
|
+
);
|
|
3702
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3703
|
+
if (notable.length === 0) {
|
|
3704
|
+
lines.push(" No changes detected.");
|
|
3705
|
+
} else {
|
|
3706
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3707
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3708
|
+
const divider = "-".repeat(header.length);
|
|
3709
|
+
lines.push(header, divider);
|
|
3710
|
+
for (const entry of notable) {
|
|
3711
|
+
const icon = STATUS_ICON[entry.status];
|
|
3712
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3713
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3714
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3715
|
+
if (entry.complianceDelta !== null) {
|
|
3716
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3717
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3718
|
+
}
|
|
3719
|
+
let dimStr = "\u2014";
|
|
3720
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3721
|
+
const b = entry.baselineDimensions;
|
|
3722
|
+
const c = entry.currentDimensions;
|
|
3723
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3724
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3725
|
+
} else {
|
|
3726
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3727
|
+
}
|
|
3728
|
+
} else if (entry.currentDimensions !== null) {
|
|
3729
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3730
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3731
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3732
|
+
}
|
|
3733
|
+
if (entry.renderFailed) {
|
|
3734
|
+
dimStr = "render failed";
|
|
3735
|
+
}
|
|
3736
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
lines.push(
|
|
3740
|
+
"",
|
|
3741
|
+
rule2,
|
|
3742
|
+
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`
|
|
3743
|
+
);
|
|
3744
|
+
return lines.join("\n");
|
|
3745
|
+
}
|
|
3746
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3747
|
+
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(
|
|
3748
|
+
"--regression-threshold <n>",
|
|
3749
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3750
|
+
"0.01"
|
|
3751
|
+
).action(
|
|
3752
|
+
async (opts) => {
|
|
3753
|
+
try {
|
|
3754
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3755
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3756
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3757
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3758
|
+
const result = await runDiff({
|
|
3759
|
+
baselineDir: opts.baseline,
|
|
3760
|
+
componentsGlob: opts.components,
|
|
3761
|
+
manifestPath: opts.manifest,
|
|
3762
|
+
viewportWidth,
|
|
3763
|
+
viewportHeight,
|
|
3764
|
+
regressionThreshold
|
|
3765
|
+
});
|
|
3766
|
+
if (opts.output !== void 0) {
|
|
3767
|
+
writeFileSync(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3768
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3769
|
+
`);
|
|
3770
|
+
}
|
|
3771
|
+
if (opts.json) {
|
|
3772
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3773
|
+
`);
|
|
3774
|
+
} else {
|
|
3775
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3776
|
+
`);
|
|
3777
|
+
}
|
|
3778
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3779
|
+
} catch (err) {
|
|
3780
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3781
|
+
`);
|
|
3782
|
+
process.exit(2);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
);
|
|
3786
|
+
}
|
|
3312
3787
|
|
|
3313
3788
|
// src/tree-formatter.ts
|
|
3314
3789
|
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
@@ -3587,10 +4062,178 @@ function buildStructuredReport(report) {
|
|
|
3587
4062
|
route: report.route?.pattern ?? null
|
|
3588
4063
|
};
|
|
3589
4064
|
}
|
|
4065
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4066
|
+
function loadStylesFile(stylesPath) {
|
|
4067
|
+
const absPath = resolve(process.cwd(), stylesPath);
|
|
4068
|
+
if (!existsSync(absPath)) {
|
|
4069
|
+
throw new Error(
|
|
4070
|
+
`Compliance styles file not found at ${absPath}.
|
|
4071
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4072
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
4075
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
4076
|
+
let parsed;
|
|
4077
|
+
try {
|
|
4078
|
+
parsed = JSON.parse(raw);
|
|
4079
|
+
} catch (err) {
|
|
4080
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4081
|
+
}
|
|
4082
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4083
|
+
throw new Error(
|
|
4084
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
return parsed;
|
|
4088
|
+
}
|
|
4089
|
+
function categoryForProperty(property) {
|
|
4090
|
+
const lower = property.toLowerCase();
|
|
4091
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4092
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4093
|
+
return "color";
|
|
4094
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4095
|
+
return "spacing";
|
|
4096
|
+
if (lower.includes("border")) return "border";
|
|
4097
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4098
|
+
return "typography";
|
|
4099
|
+
return "spacing";
|
|
4100
|
+
}
|
|
4101
|
+
function buildCategorySummary(batch) {
|
|
4102
|
+
const cats = {
|
|
4103
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4104
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4105
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4106
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4107
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4108
|
+
};
|
|
4109
|
+
for (const report of Object.values(batch.components)) {
|
|
4110
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4111
|
+
const cat = categoryForProperty(property);
|
|
4112
|
+
const summary = cats[cat];
|
|
4113
|
+
if (summary === void 0) continue;
|
|
4114
|
+
summary.total++;
|
|
4115
|
+
if (result.status === "on_system") {
|
|
4116
|
+
summary.onSystem++;
|
|
4117
|
+
} else {
|
|
4118
|
+
summary.offSystem++;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
for (const summary of Object.values(cats)) {
|
|
4123
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4124
|
+
}
|
|
4125
|
+
return cats;
|
|
4126
|
+
}
|
|
4127
|
+
function collectOffenders(batch, limit = 10) {
|
|
4128
|
+
const offenders = [];
|
|
4129
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4130
|
+
name,
|
|
4131
|
+
report,
|
|
4132
|
+
offSystemCount: report.offSystem
|
|
4133
|
+
}));
|
|
4134
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4135
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4136
|
+
if (offSystemCount === 0) continue;
|
|
4137
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4138
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4139
|
+
offenders.push({
|
|
4140
|
+
component: name,
|
|
4141
|
+
property,
|
|
4142
|
+
value: result.value,
|
|
4143
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4144
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4145
|
+
offSystemCount
|
|
4146
|
+
});
|
|
4147
|
+
if (offenders.length >= limit) break;
|
|
4148
|
+
}
|
|
4149
|
+
if (offenders.length >= limit) break;
|
|
4150
|
+
}
|
|
4151
|
+
return offenders;
|
|
4152
|
+
}
|
|
4153
|
+
function formatPct(n) {
|
|
4154
|
+
return `${Math.round(n * 100)}%`;
|
|
4155
|
+
}
|
|
4156
|
+
function truncate(s, max) {
|
|
4157
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4158
|
+
}
|
|
4159
|
+
function formatComplianceReport(batch, threshold) {
|
|
4160
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4161
|
+
const lines = [];
|
|
4162
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4163
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4164
|
+
lines.push("");
|
|
4165
|
+
const cats = buildCategorySummary(batch);
|
|
4166
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4167
|
+
if (catEntries.length > 0) {
|
|
4168
|
+
lines.push("By category:");
|
|
4169
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4170
|
+
for (const [cat, summary] of catEntries) {
|
|
4171
|
+
const label = cat.padEnd(catWidth);
|
|
4172
|
+
lines.push(
|
|
4173
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4174
|
+
);
|
|
4175
|
+
}
|
|
4176
|
+
lines.push("");
|
|
4177
|
+
}
|
|
4178
|
+
const offenders = collectOffenders(batch);
|
|
4179
|
+
if (offenders.length > 0) {
|
|
4180
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4181
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4182
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4183
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4184
|
+
for (const offender of offenders) {
|
|
4185
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4186
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4187
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4188
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4189
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4190
|
+
}
|
|
4191
|
+
} else {
|
|
4192
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4193
|
+
}
|
|
4194
|
+
return lines.join("\n");
|
|
4195
|
+
}
|
|
4196
|
+
function registerCompliance(tokensCmd) {
|
|
4197
|
+
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) => {
|
|
4198
|
+
try {
|
|
4199
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4200
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4201
|
+
const resolver = new TokenResolver(tokens);
|
|
4202
|
+
const engine = new ComplianceEngine(resolver);
|
|
4203
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4204
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4205
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4206
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4207
|
+
componentMap.set(name, styles);
|
|
4208
|
+
}
|
|
4209
|
+
if (componentMap.size === 0) {
|
|
4210
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4211
|
+
`);
|
|
4212
|
+
}
|
|
4213
|
+
const batch = engine.auditBatch(componentMap);
|
|
4214
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4215
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4216
|
+
if (useJson) {
|
|
4217
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4218
|
+
`);
|
|
4219
|
+
} else {
|
|
4220
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4221
|
+
`);
|
|
4222
|
+
}
|
|
4223
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
} catch (err) {
|
|
4227
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4228
|
+
`);
|
|
4229
|
+
process.exit(1);
|
|
4230
|
+
}
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
3590
4233
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
3591
4234
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3592
4235
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3593
|
-
function
|
|
4236
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
3594
4237
|
if (fileFlag !== void 0) {
|
|
3595
4238
|
return resolve(process.cwd(), fileFlag);
|
|
3596
4239
|
}
|
|
@@ -3624,7 +4267,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
3624
4267
|
}
|
|
3625
4268
|
const format = opts.format;
|
|
3626
4269
|
try {
|
|
3627
|
-
const filePath =
|
|
4270
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
3628
4271
|
if (!existsSync(filePath)) {
|
|
3629
4272
|
throw new Error(
|
|
3630
4273
|
`Token file not found at ${filePath}.
|
|
@@ -3688,6 +4331,311 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
3688
4331
|
}
|
|
3689
4332
|
);
|
|
3690
4333
|
}
|
|
4334
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4335
|
+
var SEVERITY_EMOJI = {
|
|
4336
|
+
none: "\u25CB",
|
|
4337
|
+
subtle: "\u25D4",
|
|
4338
|
+
moderate: "\u25D1",
|
|
4339
|
+
significant: "\u25CF"
|
|
4340
|
+
};
|
|
4341
|
+
function formatImpactReport(report) {
|
|
4342
|
+
const lines = [];
|
|
4343
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4344
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4345
|
+
if (report.components.length === 0) {
|
|
4346
|
+
lines.push("");
|
|
4347
|
+
lines.push("No components reference this token.");
|
|
4348
|
+
return lines.join("\n");
|
|
4349
|
+
}
|
|
4350
|
+
lines.push("");
|
|
4351
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4352
|
+
const propWidth = Math.max(
|
|
4353
|
+
8,
|
|
4354
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4355
|
+
);
|
|
4356
|
+
for (const comp of report.components) {
|
|
4357
|
+
for (const property of comp.affectedProperties) {
|
|
4358
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4359
|
+
const prop = property.padEnd(propWidth);
|
|
4360
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4361
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
lines.push("");
|
|
4365
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4366
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4367
|
+
lines.push(
|
|
4368
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4369
|
+
);
|
|
4370
|
+
if (report.colorDelta !== void 0) {
|
|
4371
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4372
|
+
}
|
|
4373
|
+
return lines.join("\n");
|
|
4374
|
+
}
|
|
4375
|
+
function formatImpactSummary(report) {
|
|
4376
|
+
if (report.components.length === 0) {
|
|
4377
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4378
|
+
}
|
|
4379
|
+
const parts = report.components.map(
|
|
4380
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4381
|
+
);
|
|
4382
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4383
|
+
}
|
|
4384
|
+
function registerImpact(tokensCmd) {
|
|
4385
|
+
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(
|
|
4386
|
+
(tokenPath, opts) => {
|
|
4387
|
+
try {
|
|
4388
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4389
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4390
|
+
const resolver = new TokenResolver(tokens);
|
|
4391
|
+
const engine = new ComplianceEngine(resolver);
|
|
4392
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4393
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4394
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4395
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4396
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4397
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4398
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4399
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4400
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4401
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4402
|
+
if (useJson) {
|
|
4403
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4404
|
+
`);
|
|
4405
|
+
} else {
|
|
4406
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4407
|
+
`);
|
|
4408
|
+
if (isTTY()) {
|
|
4409
|
+
process.stdout.write(`
|
|
4410
|
+
${formatImpactSummary(report)}
|
|
4411
|
+
`);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
} catch (err) {
|
|
4415
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4416
|
+
`);
|
|
4417
|
+
process.exit(1);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4422
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4423
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4424
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4425
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4426
|
+
const htmlHarness = await buildComponentHarness(
|
|
4427
|
+
filePath,
|
|
4428
|
+
componentName,
|
|
4429
|
+
{},
|
|
4430
|
+
// no props
|
|
4431
|
+
vpWidth,
|
|
4432
|
+
cssOverride
|
|
4433
|
+
// injected as <style>
|
|
4434
|
+
);
|
|
4435
|
+
const pool = new BrowserPool({
|
|
4436
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4437
|
+
viewportWidth: vpWidth,
|
|
4438
|
+
viewportHeight: vpHeight
|
|
4439
|
+
});
|
|
4440
|
+
await pool.init();
|
|
4441
|
+
const slot = await pool.acquire();
|
|
4442
|
+
const { page } = slot;
|
|
4443
|
+
try {
|
|
4444
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4445
|
+
await page.waitForFunction(
|
|
4446
|
+
() => {
|
|
4447
|
+
const w = window;
|
|
4448
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4449
|
+
},
|
|
4450
|
+
{ timeout: timeoutMs }
|
|
4451
|
+
);
|
|
4452
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
4453
|
+
const bb = await rootLocator.boundingBox();
|
|
4454
|
+
const PAD = 16;
|
|
4455
|
+
const MIN_W = 320;
|
|
4456
|
+
const MIN_H = 120;
|
|
4457
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
4458
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
4459
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
4460
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
4461
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
4462
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
4463
|
+
const screenshot = await page.screenshot({
|
|
4464
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
4465
|
+
type: "png"
|
|
4466
|
+
});
|
|
4467
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
4468
|
+
} finally {
|
|
4469
|
+
pool.release(slot);
|
|
4470
|
+
await pool.close().catch(() => void 0);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
function registerPreview(tokensCmd) {
|
|
4474
|
+
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(
|
|
4475
|
+
async (tokenPath, opts) => {
|
|
4476
|
+
try {
|
|
4477
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4478
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4479
|
+
const resolver = new TokenResolver(tokens);
|
|
4480
|
+
const engine = new ComplianceEngine(resolver);
|
|
4481
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
4482
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4483
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4484
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4485
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4486
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4487
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4488
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
4489
|
+
if (impactReport.components.length === 0) {
|
|
4490
|
+
process.stdout.write(
|
|
4491
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
4492
|
+
`
|
|
4493
|
+
);
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
4496
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
4497
|
+
process.stderr.write(
|
|
4498
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
4499
|
+
`
|
|
4500
|
+
);
|
|
4501
|
+
const manifest = loadManifest(opts.manifest);
|
|
4502
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
4503
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
4504
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
4505
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
4506
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
4507
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
4508
|
+
const renders = [];
|
|
4509
|
+
for (const componentName of affectedNames) {
|
|
4510
|
+
const descriptor = manifest.components[componentName];
|
|
4511
|
+
if (descriptor === void 0) {
|
|
4512
|
+
process.stderr.write(
|
|
4513
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
4514
|
+
`
|
|
4515
|
+
);
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
4519
|
+
`);
|
|
4520
|
+
const before = await renderComponentWithCssOverride(
|
|
4521
|
+
descriptor.filePath,
|
|
4522
|
+
componentName,
|
|
4523
|
+
beforeCss,
|
|
4524
|
+
vpWidth,
|
|
4525
|
+
vpHeight,
|
|
4526
|
+
timeout
|
|
4527
|
+
);
|
|
4528
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
4529
|
+
`);
|
|
4530
|
+
const after = await renderComponentWithCssOverride(
|
|
4531
|
+
descriptor.filePath,
|
|
4532
|
+
componentName,
|
|
4533
|
+
afterCss,
|
|
4534
|
+
vpWidth,
|
|
4535
|
+
vpHeight,
|
|
4536
|
+
timeout
|
|
4537
|
+
);
|
|
4538
|
+
renders.push({ name: componentName, before, after });
|
|
4539
|
+
}
|
|
4540
|
+
if (renders.length === 0) {
|
|
4541
|
+
process.stderr.write(
|
|
4542
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
4543
|
+
);
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
4547
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
4548
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
4549
|
+
{
|
|
4550
|
+
props: { version: "before", component: r.name },
|
|
4551
|
+
result: {
|
|
4552
|
+
screenshot: r.before.screenshot,
|
|
4553
|
+
width: cellW,
|
|
4554
|
+
height: cellH,
|
|
4555
|
+
renderTimeMs: 0,
|
|
4556
|
+
computedStyles: {}
|
|
4557
|
+
},
|
|
4558
|
+
index: colIdx * 2,
|
|
4559
|
+
axisIndices: [0, colIdx]
|
|
4560
|
+
},
|
|
4561
|
+
{
|
|
4562
|
+
props: { version: "after", component: r.name },
|
|
4563
|
+
result: {
|
|
4564
|
+
screenshot: r.after.screenshot,
|
|
4565
|
+
width: cellW,
|
|
4566
|
+
height: cellH,
|
|
4567
|
+
renderTimeMs: 0,
|
|
4568
|
+
computedStyles: {}
|
|
4569
|
+
},
|
|
4570
|
+
index: colIdx * 2 + 1,
|
|
4571
|
+
axisIndices: [1, colIdx]
|
|
4572
|
+
}
|
|
4573
|
+
]);
|
|
4574
|
+
const matrixResult = {
|
|
4575
|
+
cells,
|
|
4576
|
+
axes: [
|
|
4577
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
4578
|
+
{ name: "version", values: ["before", "after"] }
|
|
4579
|
+
],
|
|
4580
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
4581
|
+
rows: 2,
|
|
4582
|
+
cols: renders.length,
|
|
4583
|
+
stats: {
|
|
4584
|
+
totalCells: cells.length,
|
|
4585
|
+
totalRenderTimeMs: 0,
|
|
4586
|
+
avgRenderTimeMs: 0,
|
|
4587
|
+
minRenderTimeMs: 0,
|
|
4588
|
+
maxRenderTimeMs: 0,
|
|
4589
|
+
wallClockTimeMs: 0
|
|
4590
|
+
}
|
|
4591
|
+
};
|
|
4592
|
+
const generator = new SpriteSheetGenerator({
|
|
4593
|
+
cellPadding: 8,
|
|
4594
|
+
borderWidth: 1,
|
|
4595
|
+
labelHeight: 32,
|
|
4596
|
+
labelWidth: 120
|
|
4597
|
+
});
|
|
4598
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
4599
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
4600
|
+
const outputPath = opts.output ?? resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
4601
|
+
const outputDir = resolve(outputPath, "..");
|
|
4602
|
+
mkdirSync(outputDir, { recursive: true });
|
|
4603
|
+
writeFileSync(outputPath, spriteResult.png);
|
|
4604
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4605
|
+
if (useJson) {
|
|
4606
|
+
process.stdout.write(
|
|
4607
|
+
`${JSON.stringify(
|
|
4608
|
+
{
|
|
4609
|
+
tokenPath,
|
|
4610
|
+
oldValue: currentValue,
|
|
4611
|
+
newValue: opts.newValue,
|
|
4612
|
+
outputPath,
|
|
4613
|
+
width: spriteResult.width,
|
|
4614
|
+
height: spriteResult.height,
|
|
4615
|
+
components: renders.map((r) => r.name),
|
|
4616
|
+
cells: spriteResult.coordinates.length
|
|
4617
|
+
},
|
|
4618
|
+
null,
|
|
4619
|
+
2
|
|
4620
|
+
)}
|
|
4621
|
+
`
|
|
4622
|
+
);
|
|
4623
|
+
} else {
|
|
4624
|
+
process.stdout.write(
|
|
4625
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
4626
|
+
`
|
|
4627
|
+
);
|
|
4628
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
4629
|
+
`);
|
|
4630
|
+
}
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4633
|
+
`);
|
|
4634
|
+
process.exit(1);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
3691
4639
|
|
|
3692
4640
|
// src/tokens/commands.ts
|
|
3693
4641
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
@@ -3709,7 +4657,7 @@ function buildTable2(headers, rows) {
|
|
|
3709
4657
|
);
|
|
3710
4658
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
3711
4659
|
}
|
|
3712
|
-
function
|
|
4660
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3713
4661
|
if (fileFlag !== void 0) {
|
|
3714
4662
|
return resolve(process.cwd(), fileFlag);
|
|
3715
4663
|
}
|
|
@@ -3772,7 +4720,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
3772
4720
|
function registerGet2(tokensCmd) {
|
|
3773
4721
|
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) => {
|
|
3774
4722
|
try {
|
|
3775
|
-
const filePath =
|
|
4723
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3776
4724
|
const { tokens } = loadTokens(filePath);
|
|
3777
4725
|
const resolver = new TokenResolver(tokens);
|
|
3778
4726
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -3798,7 +4746,7 @@ function registerList2(tokensCmd) {
|
|
|
3798
4746
|
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(
|
|
3799
4747
|
(category, opts) => {
|
|
3800
4748
|
try {
|
|
3801
|
-
const filePath =
|
|
4749
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3802
4750
|
const { tokens } = loadTokens(filePath);
|
|
3803
4751
|
const resolver = new TokenResolver(tokens);
|
|
3804
4752
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -3828,7 +4776,7 @@ function registerSearch(tokensCmd) {
|
|
|
3828
4776
|
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(
|
|
3829
4777
|
(value, opts) => {
|
|
3830
4778
|
try {
|
|
3831
|
-
const filePath =
|
|
4779
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3832
4780
|
const { tokens } = loadTokens(filePath);
|
|
3833
4781
|
const resolver = new TokenResolver(tokens);
|
|
3834
4782
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -3910,7 +4858,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
3910
4858
|
function registerResolve(tokensCmd) {
|
|
3911
4859
|
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) => {
|
|
3912
4860
|
try {
|
|
3913
|
-
const filePath =
|
|
4861
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3914
4862
|
const absFilePath = filePath;
|
|
3915
4863
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
3916
4864
|
const resolver = new TokenResolver(tokens);
|
|
@@ -3947,7 +4895,7 @@ function registerValidate(tokensCmd) {
|
|
|
3947
4895
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
3948
4896
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
3949
4897
|
try {
|
|
3950
|
-
const filePath =
|
|
4898
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3951
4899
|
if (!existsSync(filePath)) {
|
|
3952
4900
|
throw new Error(
|
|
3953
4901
|
`Token file not found at ${filePath}.
|
|
@@ -4031,6 +4979,9 @@ function createTokensCommand() {
|
|
|
4031
4979
|
registerResolve(tokensCmd);
|
|
4032
4980
|
registerValidate(tokensCmd);
|
|
4033
4981
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
4982
|
+
registerCompliance(tokensCmd);
|
|
4983
|
+
registerImpact(tokensCmd);
|
|
4984
|
+
registerPreview(tokensCmd);
|
|
4034
4985
|
return tokensCmd;
|
|
4035
4986
|
}
|
|
4036
4987
|
|
|
@@ -4126,10 +5077,11 @@ function createProgram(options = {}) {
|
|
|
4126
5077
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
4127
5078
|
if (existingReportCmd !== void 0) {
|
|
4128
5079
|
registerBaselineSubCommand(existingReportCmd);
|
|
5080
|
+
registerDiffSubCommand(existingReportCmd);
|
|
4129
5081
|
}
|
|
4130
5082
|
return program;
|
|
4131
5083
|
}
|
|
4132
5084
|
|
|
4133
|
-
export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath, runInit };
|
|
5085
|
+
export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runInit };
|
|
4134
5086
|
//# sourceMappingURL=index.js.map
|
|
4135
5087
|
//# sourceMappingURL=index.js.map
|