@agent-scope/cli 1.13.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 +547 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +499 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +501 -23
- 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((resolve14) => {
|
|
232
232
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
233
|
+
resolve14(answer.trim());
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -379,7 +379,7 @@ function createInitCommand() {
|
|
|
379
379
|
}
|
|
380
380
|
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
381
381
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
382
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
382
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
383
383
|
}
|
|
384
384
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
385
385
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -466,7 +466,9 @@ ${msg}`);
|
|
|
466
466
|
return outputFile.text;
|
|
467
467
|
}
|
|
468
468
|
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
469
|
-
const projectStyleBlock = ""
|
|
469
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
470
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
471
|
+
</style>` : "";
|
|
470
472
|
return `<!DOCTYPE html>
|
|
471
473
|
<html lang="en">
|
|
472
474
|
<head>
|
|
@@ -2830,8 +2832,8 @@ Available: ${available}`
|
|
|
2830
2832
|
`
|
|
2831
2833
|
);
|
|
2832
2834
|
if (opts.sprite !== void 0) {
|
|
2833
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2834
|
-
const gen = new
|
|
2835
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2836
|
+
const gen = new SpriteSheetGenerator2();
|
|
2835
2837
|
const sheet = await gen.generate(result);
|
|
2836
2838
|
const spritePath = path.resolve(process.cwd(), opts.sprite);
|
|
2837
2839
|
fs.writeFileSync(spritePath, sheet.png);
|
|
@@ -2840,8 +2842,8 @@ Available: ${available}`
|
|
|
2840
2842
|
}
|
|
2841
2843
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2842
2844
|
if (fmt === "file") {
|
|
2843
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2844
|
-
const gen = new
|
|
2845
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2846
|
+
const gen = new SpriteSheetGenerator2();
|
|
2845
2847
|
const sheet = await gen.generate(result);
|
|
2846
2848
|
const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2847
2849
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -2858,8 +2860,8 @@ Available: ${available}`
|
|
|
2858
2860
|
} else if (fmt === "png") {
|
|
2859
2861
|
if (opts.sprite !== void 0) {
|
|
2860
2862
|
} else {
|
|
2861
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2862
|
-
const gen = new
|
|
2863
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2864
|
+
const gen = new SpriteSheetGenerator2();
|
|
2863
2865
|
const sheet = await gen.generate(result);
|
|
2864
2866
|
process.stdout.write(sheet.png);
|
|
2865
2867
|
}
|
|
@@ -3166,12 +3168,12 @@ async function runBaseline(options = {}) {
|
|
|
3166
3168
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3167
3169
|
let manifest$1;
|
|
3168
3170
|
if (manifestPath !== void 0) {
|
|
3169
|
-
const { readFileSync:
|
|
3171
|
+
const { readFileSync: readFileSync10 } = await import('fs');
|
|
3170
3172
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3171
3173
|
if (!fs.existsSync(absPath)) {
|
|
3172
3174
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3173
3175
|
}
|
|
3174
|
-
manifest$1 = JSON.parse(
|
|
3176
|
+
manifest$1 = JSON.parse(readFileSync10(absPath, "utf-8"));
|
|
3175
3177
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3176
3178
|
`);
|
|
3177
3179
|
} else {
|
|
@@ -4083,10 +4085,178 @@ function buildStructuredReport(report) {
|
|
|
4083
4085
|
route: report.route?.pattern ?? null
|
|
4084
4086
|
};
|
|
4085
4087
|
}
|
|
4088
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4089
|
+
function loadStylesFile(stylesPath) {
|
|
4090
|
+
const absPath = path.resolve(process.cwd(), stylesPath);
|
|
4091
|
+
if (!fs.existsSync(absPath)) {
|
|
4092
|
+
throw new Error(
|
|
4093
|
+
`Compliance styles file not found at ${absPath}.
|
|
4094
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4095
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4096
|
+
);
|
|
4097
|
+
}
|
|
4098
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
4099
|
+
let parsed;
|
|
4100
|
+
try {
|
|
4101
|
+
parsed = JSON.parse(raw);
|
|
4102
|
+
} catch (err) {
|
|
4103
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4104
|
+
}
|
|
4105
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4106
|
+
throw new Error(
|
|
4107
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
return parsed;
|
|
4111
|
+
}
|
|
4112
|
+
function categoryForProperty(property) {
|
|
4113
|
+
const lower = property.toLowerCase();
|
|
4114
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4115
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4116
|
+
return "color";
|
|
4117
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4118
|
+
return "spacing";
|
|
4119
|
+
if (lower.includes("border")) return "border";
|
|
4120
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4121
|
+
return "typography";
|
|
4122
|
+
return "spacing";
|
|
4123
|
+
}
|
|
4124
|
+
function buildCategorySummary(batch) {
|
|
4125
|
+
const cats = {
|
|
4126
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4127
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4128
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4129
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4130
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4131
|
+
};
|
|
4132
|
+
for (const report of Object.values(batch.components)) {
|
|
4133
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4134
|
+
const cat = categoryForProperty(property);
|
|
4135
|
+
const summary = cats[cat];
|
|
4136
|
+
if (summary === void 0) continue;
|
|
4137
|
+
summary.total++;
|
|
4138
|
+
if (result.status === "on_system") {
|
|
4139
|
+
summary.onSystem++;
|
|
4140
|
+
} else {
|
|
4141
|
+
summary.offSystem++;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
for (const summary of Object.values(cats)) {
|
|
4146
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4147
|
+
}
|
|
4148
|
+
return cats;
|
|
4149
|
+
}
|
|
4150
|
+
function collectOffenders(batch, limit = 10) {
|
|
4151
|
+
const offenders = [];
|
|
4152
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4153
|
+
name,
|
|
4154
|
+
report,
|
|
4155
|
+
offSystemCount: report.offSystem
|
|
4156
|
+
}));
|
|
4157
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4158
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4159
|
+
if (offSystemCount === 0) continue;
|
|
4160
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4161
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4162
|
+
offenders.push({
|
|
4163
|
+
component: name,
|
|
4164
|
+
property,
|
|
4165
|
+
value: result.value,
|
|
4166
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4167
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4168
|
+
offSystemCount
|
|
4169
|
+
});
|
|
4170
|
+
if (offenders.length >= limit) break;
|
|
4171
|
+
}
|
|
4172
|
+
if (offenders.length >= limit) break;
|
|
4173
|
+
}
|
|
4174
|
+
return offenders;
|
|
4175
|
+
}
|
|
4176
|
+
function formatPct(n) {
|
|
4177
|
+
return `${Math.round(n * 100)}%`;
|
|
4178
|
+
}
|
|
4179
|
+
function truncate(s, max) {
|
|
4180
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4181
|
+
}
|
|
4182
|
+
function formatComplianceReport(batch, threshold) {
|
|
4183
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4184
|
+
const lines = [];
|
|
4185
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4186
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4187
|
+
lines.push("");
|
|
4188
|
+
const cats = buildCategorySummary(batch);
|
|
4189
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4190
|
+
if (catEntries.length > 0) {
|
|
4191
|
+
lines.push("By category:");
|
|
4192
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4193
|
+
for (const [cat, summary] of catEntries) {
|
|
4194
|
+
const label = cat.padEnd(catWidth);
|
|
4195
|
+
lines.push(
|
|
4196
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
lines.push("");
|
|
4200
|
+
}
|
|
4201
|
+
const offenders = collectOffenders(batch);
|
|
4202
|
+
if (offenders.length > 0) {
|
|
4203
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4204
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4205
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4206
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4207
|
+
for (const offender of offenders) {
|
|
4208
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4209
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4210
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4211
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4212
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4213
|
+
}
|
|
4214
|
+
} else {
|
|
4215
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4216
|
+
}
|
|
4217
|
+
return lines.join("\n");
|
|
4218
|
+
}
|
|
4219
|
+
function registerCompliance(tokensCmd) {
|
|
4220
|
+
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) => {
|
|
4221
|
+
try {
|
|
4222
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4223
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4224
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4225
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4226
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4227
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4228
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4229
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4230
|
+
componentMap.set(name, styles);
|
|
4231
|
+
}
|
|
4232
|
+
if (componentMap.size === 0) {
|
|
4233
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4234
|
+
`);
|
|
4235
|
+
}
|
|
4236
|
+
const batch = engine.auditBatch(componentMap);
|
|
4237
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4238
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4239
|
+
if (useJson) {
|
|
4240
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4241
|
+
`);
|
|
4242
|
+
} else {
|
|
4243
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4244
|
+
`);
|
|
4245
|
+
}
|
|
4246
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4247
|
+
process.exit(1);
|
|
4248
|
+
}
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4251
|
+
`);
|
|
4252
|
+
process.exit(1);
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4086
4256
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
4087
4257
|
var CONFIG_FILE = "reactscope.config.json";
|
|
4088
4258
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
4089
|
-
function
|
|
4259
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
4090
4260
|
if (fileFlag !== void 0) {
|
|
4091
4261
|
return path.resolve(process.cwd(), fileFlag);
|
|
4092
4262
|
}
|
|
@@ -4120,7 +4290,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
4120
4290
|
}
|
|
4121
4291
|
const format = opts.format;
|
|
4122
4292
|
try {
|
|
4123
|
-
const filePath =
|
|
4293
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4124
4294
|
if (!fs.existsSync(filePath)) {
|
|
4125
4295
|
throw new Error(
|
|
4126
4296
|
`Token file not found at ${filePath}.
|
|
@@ -4184,6 +4354,311 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
4184
4354
|
}
|
|
4185
4355
|
);
|
|
4186
4356
|
}
|
|
4357
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4358
|
+
var SEVERITY_EMOJI = {
|
|
4359
|
+
none: "\u25CB",
|
|
4360
|
+
subtle: "\u25D4",
|
|
4361
|
+
moderate: "\u25D1",
|
|
4362
|
+
significant: "\u25CF"
|
|
4363
|
+
};
|
|
4364
|
+
function formatImpactReport(report) {
|
|
4365
|
+
const lines = [];
|
|
4366
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4367
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4368
|
+
if (report.components.length === 0) {
|
|
4369
|
+
lines.push("");
|
|
4370
|
+
lines.push("No components reference this token.");
|
|
4371
|
+
return lines.join("\n");
|
|
4372
|
+
}
|
|
4373
|
+
lines.push("");
|
|
4374
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4375
|
+
const propWidth = Math.max(
|
|
4376
|
+
8,
|
|
4377
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4378
|
+
);
|
|
4379
|
+
for (const comp of report.components) {
|
|
4380
|
+
for (const property of comp.affectedProperties) {
|
|
4381
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4382
|
+
const prop = property.padEnd(propWidth);
|
|
4383
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4384
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
lines.push("");
|
|
4388
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4389
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4390
|
+
lines.push(
|
|
4391
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4392
|
+
);
|
|
4393
|
+
if (report.colorDelta !== void 0) {
|
|
4394
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4395
|
+
}
|
|
4396
|
+
return lines.join("\n");
|
|
4397
|
+
}
|
|
4398
|
+
function formatImpactSummary(report) {
|
|
4399
|
+
if (report.components.length === 0) {
|
|
4400
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4401
|
+
}
|
|
4402
|
+
const parts = report.components.map(
|
|
4403
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4404
|
+
);
|
|
4405
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4406
|
+
}
|
|
4407
|
+
function registerImpact(tokensCmd) {
|
|
4408
|
+
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(
|
|
4409
|
+
(tokenPath, opts) => {
|
|
4410
|
+
try {
|
|
4411
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4412
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4413
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4414
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4415
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4416
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4417
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4418
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4419
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4420
|
+
const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
|
|
4421
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4422
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4423
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4424
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4425
|
+
if (useJson) {
|
|
4426
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4427
|
+
`);
|
|
4428
|
+
} else {
|
|
4429
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4430
|
+
`);
|
|
4431
|
+
if (isTTY()) {
|
|
4432
|
+
process.stdout.write(`
|
|
4433
|
+
${formatImpactSummary(report)}
|
|
4434
|
+
`);
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4439
|
+
`);
|
|
4440
|
+
process.exit(1);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
);
|
|
4444
|
+
}
|
|
4445
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4446
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4447
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4448
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4449
|
+
const htmlHarness = await buildComponentHarness(
|
|
4450
|
+
filePath,
|
|
4451
|
+
componentName,
|
|
4452
|
+
{},
|
|
4453
|
+
// no props
|
|
4454
|
+
vpWidth,
|
|
4455
|
+
cssOverride
|
|
4456
|
+
// injected as <style>
|
|
4457
|
+
);
|
|
4458
|
+
const pool = new render.BrowserPool({
|
|
4459
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4460
|
+
viewportWidth: vpWidth,
|
|
4461
|
+
viewportHeight: vpHeight
|
|
4462
|
+
});
|
|
4463
|
+
await pool.init();
|
|
4464
|
+
const slot = await pool.acquire();
|
|
4465
|
+
const { page } = slot;
|
|
4466
|
+
try {
|
|
4467
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4468
|
+
await page.waitForFunction(
|
|
4469
|
+
() => {
|
|
4470
|
+
const w = window;
|
|
4471
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4472
|
+
},
|
|
4473
|
+
{ timeout: timeoutMs }
|
|
4474
|
+
);
|
|
4475
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
4476
|
+
const bb = await rootLocator.boundingBox();
|
|
4477
|
+
const PAD = 16;
|
|
4478
|
+
const MIN_W = 320;
|
|
4479
|
+
const MIN_H = 120;
|
|
4480
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
4481
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
4482
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
4483
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
4484
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
4485
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
4486
|
+
const screenshot = await page.screenshot({
|
|
4487
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
4488
|
+
type: "png"
|
|
4489
|
+
});
|
|
4490
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
4491
|
+
} finally {
|
|
4492
|
+
pool.release(slot);
|
|
4493
|
+
await pool.close().catch(() => void 0);
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
function registerPreview(tokensCmd) {
|
|
4497
|
+
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(
|
|
4498
|
+
async (tokenPath, opts) => {
|
|
4499
|
+
try {
|
|
4500
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4501
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4502
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4503
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4504
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
4505
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4506
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4507
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4508
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4509
|
+
const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
|
|
4510
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4511
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
4512
|
+
if (impactReport.components.length === 0) {
|
|
4513
|
+
process.stdout.write(
|
|
4514
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
4515
|
+
`
|
|
4516
|
+
);
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
4520
|
+
process.stderr.write(
|
|
4521
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
4522
|
+
`
|
|
4523
|
+
);
|
|
4524
|
+
const manifest = loadManifest(opts.manifest);
|
|
4525
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
4526
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
4527
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
4528
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
4529
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
4530
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
4531
|
+
const renders = [];
|
|
4532
|
+
for (const componentName of affectedNames) {
|
|
4533
|
+
const descriptor = manifest.components[componentName];
|
|
4534
|
+
if (descriptor === void 0) {
|
|
4535
|
+
process.stderr.write(
|
|
4536
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
4537
|
+
`
|
|
4538
|
+
);
|
|
4539
|
+
continue;
|
|
4540
|
+
}
|
|
4541
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
4542
|
+
`);
|
|
4543
|
+
const before = await renderComponentWithCssOverride(
|
|
4544
|
+
descriptor.filePath,
|
|
4545
|
+
componentName,
|
|
4546
|
+
beforeCss,
|
|
4547
|
+
vpWidth,
|
|
4548
|
+
vpHeight,
|
|
4549
|
+
timeout
|
|
4550
|
+
);
|
|
4551
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
4552
|
+
`);
|
|
4553
|
+
const after = await renderComponentWithCssOverride(
|
|
4554
|
+
descriptor.filePath,
|
|
4555
|
+
componentName,
|
|
4556
|
+
afterCss,
|
|
4557
|
+
vpWidth,
|
|
4558
|
+
vpHeight,
|
|
4559
|
+
timeout
|
|
4560
|
+
);
|
|
4561
|
+
renders.push({ name: componentName, before, after });
|
|
4562
|
+
}
|
|
4563
|
+
if (renders.length === 0) {
|
|
4564
|
+
process.stderr.write(
|
|
4565
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
4566
|
+
);
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
4570
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
4571
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
4572
|
+
{
|
|
4573
|
+
props: { version: "before", component: r.name },
|
|
4574
|
+
result: {
|
|
4575
|
+
screenshot: r.before.screenshot,
|
|
4576
|
+
width: cellW,
|
|
4577
|
+
height: cellH,
|
|
4578
|
+
renderTimeMs: 0,
|
|
4579
|
+
computedStyles: {}
|
|
4580
|
+
},
|
|
4581
|
+
index: colIdx * 2,
|
|
4582
|
+
axisIndices: [0, colIdx]
|
|
4583
|
+
},
|
|
4584
|
+
{
|
|
4585
|
+
props: { version: "after", component: r.name },
|
|
4586
|
+
result: {
|
|
4587
|
+
screenshot: r.after.screenshot,
|
|
4588
|
+
width: cellW,
|
|
4589
|
+
height: cellH,
|
|
4590
|
+
renderTimeMs: 0,
|
|
4591
|
+
computedStyles: {}
|
|
4592
|
+
},
|
|
4593
|
+
index: colIdx * 2 + 1,
|
|
4594
|
+
axisIndices: [1, colIdx]
|
|
4595
|
+
}
|
|
4596
|
+
]);
|
|
4597
|
+
const matrixResult = {
|
|
4598
|
+
cells,
|
|
4599
|
+
axes: [
|
|
4600
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
4601
|
+
{ name: "version", values: ["before", "after"] }
|
|
4602
|
+
],
|
|
4603
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
4604
|
+
rows: 2,
|
|
4605
|
+
cols: renders.length,
|
|
4606
|
+
stats: {
|
|
4607
|
+
totalCells: cells.length,
|
|
4608
|
+
totalRenderTimeMs: 0,
|
|
4609
|
+
avgRenderTimeMs: 0,
|
|
4610
|
+
minRenderTimeMs: 0,
|
|
4611
|
+
maxRenderTimeMs: 0,
|
|
4612
|
+
wallClockTimeMs: 0
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
const generator = new render.SpriteSheetGenerator({
|
|
4616
|
+
cellPadding: 8,
|
|
4617
|
+
borderWidth: 1,
|
|
4618
|
+
labelHeight: 32,
|
|
4619
|
+
labelWidth: 120
|
|
4620
|
+
});
|
|
4621
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
4622
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
4623
|
+
const outputPath = opts.output ?? path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
4624
|
+
const outputDir = path.resolve(outputPath, "..");
|
|
4625
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
4626
|
+
fs.writeFileSync(outputPath, spriteResult.png);
|
|
4627
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4628
|
+
if (useJson) {
|
|
4629
|
+
process.stdout.write(
|
|
4630
|
+
`${JSON.stringify(
|
|
4631
|
+
{
|
|
4632
|
+
tokenPath,
|
|
4633
|
+
oldValue: currentValue,
|
|
4634
|
+
newValue: opts.newValue,
|
|
4635
|
+
outputPath,
|
|
4636
|
+
width: spriteResult.width,
|
|
4637
|
+
height: spriteResult.height,
|
|
4638
|
+
components: renders.map((r) => r.name),
|
|
4639
|
+
cells: spriteResult.coordinates.length
|
|
4640
|
+
},
|
|
4641
|
+
null,
|
|
4642
|
+
2
|
|
4643
|
+
)}
|
|
4644
|
+
`
|
|
4645
|
+
);
|
|
4646
|
+
} else {
|
|
4647
|
+
process.stdout.write(
|
|
4648
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
4649
|
+
`
|
|
4650
|
+
);
|
|
4651
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
4652
|
+
`);
|
|
4653
|
+
}
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4656
|
+
`);
|
|
4657
|
+
process.exit(1);
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
);
|
|
4661
|
+
}
|
|
4187
4662
|
|
|
4188
4663
|
// src/tokens/commands.ts
|
|
4189
4664
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
@@ -4205,7 +4680,7 @@ function buildTable2(headers, rows) {
|
|
|
4205
4680
|
);
|
|
4206
4681
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
4207
4682
|
}
|
|
4208
|
-
function
|
|
4683
|
+
function resolveTokenFilePath(fileFlag) {
|
|
4209
4684
|
if (fileFlag !== void 0) {
|
|
4210
4685
|
return path.resolve(process.cwd(), fileFlag);
|
|
4211
4686
|
}
|
|
@@ -4268,7 +4743,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
4268
4743
|
function registerGet2(tokensCmd) {
|
|
4269
4744
|
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) => {
|
|
4270
4745
|
try {
|
|
4271
|
-
const filePath =
|
|
4746
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4272
4747
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4273
4748
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4274
4749
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -4294,7 +4769,7 @@ function registerList2(tokensCmd) {
|
|
|
4294
4769
|
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(
|
|
4295
4770
|
(category, opts) => {
|
|
4296
4771
|
try {
|
|
4297
|
-
const filePath =
|
|
4772
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4298
4773
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4299
4774
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4300
4775
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -4324,7 +4799,7 @@ function registerSearch(tokensCmd) {
|
|
|
4324
4799
|
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(
|
|
4325
4800
|
(value, opts) => {
|
|
4326
4801
|
try {
|
|
4327
|
-
const filePath =
|
|
4802
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4328
4803
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4329
4804
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4330
4805
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -4406,7 +4881,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
4406
4881
|
function registerResolve(tokensCmd) {
|
|
4407
4882
|
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) => {
|
|
4408
4883
|
try {
|
|
4409
|
-
const filePath =
|
|
4884
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4410
4885
|
const absFilePath = filePath;
|
|
4411
4886
|
const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
|
|
4412
4887
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
@@ -4443,7 +4918,7 @@ function registerValidate(tokensCmd) {
|
|
|
4443
4918
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
4444
4919
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4445
4920
|
try {
|
|
4446
|
-
const filePath =
|
|
4921
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4447
4922
|
if (!fs.existsSync(filePath)) {
|
|
4448
4923
|
throw new Error(
|
|
4449
4924
|
`Token file not found at ${filePath}.
|
|
@@ -4527,6 +5002,9 @@ function createTokensCommand() {
|
|
|
4527
5002
|
registerResolve(tokensCmd);
|
|
4528
5003
|
registerValidate(tokensCmd);
|
|
4529
5004
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
5005
|
+
registerCompliance(tokensCmd);
|
|
5006
|
+
registerImpact(tokensCmd);
|
|
5007
|
+
registerPreview(tokensCmd);
|
|
4530
5008
|
return tokensCmd;
|
|
4531
5009
|
}
|
|
4532
5010
|
|
|
@@ -4635,7 +5113,7 @@ exports.createTokensCommand = createTokensCommand;
|
|
|
4635
5113
|
exports.createTokensExportCommand = createTokensExportCommand;
|
|
4636
5114
|
exports.isTTY = isTTY;
|
|
4637
5115
|
exports.matchGlob = matchGlob;
|
|
4638
|
-
exports.resolveTokenFilePath =
|
|
5116
|
+
exports.resolveTokenFilePath = resolveTokenFilePath2;
|
|
4639
5117
|
exports.runInit = runInit;
|
|
4640
5118
|
//# sourceMappingURL=index.cjs.map
|
|
4641
5119
|
//# sourceMappingURL=index.cjs.map
|