@agent-scope/cli 1.13.0 → 1.15.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 +1774 -850
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1602 -701
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -1
- package/dist/index.d.ts +116 -1
- package/dist/index.js +1592 -695
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command9 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -39,27 +39,946 @@ async function browserCapture(options) {
|
|
|
39
39
|
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
40
40
|
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
41
41
|
}
|
|
42
|
-
const report = { ...raw, route: null };
|
|
43
|
-
return { report };
|
|
44
|
-
} finally {
|
|
45
|
-
await browser.close();
|
|
42
|
+
const report = { ...raw, route: null };
|
|
43
|
+
return { report };
|
|
44
|
+
} finally {
|
|
45
|
+
await browser.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
49
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
50
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/ci/commands.ts
|
|
54
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
55
|
+
import { resolve as resolve2 } from "path";
|
|
56
|
+
import { generateManifest } from "@agent-scope/manifest";
|
|
57
|
+
import { BrowserPool, safeRender } from "@agent-scope/render";
|
|
58
|
+
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
59
|
+
import { Command } from "commander";
|
|
60
|
+
|
|
61
|
+
// src/component-bundler.ts
|
|
62
|
+
import { dirname } from "path";
|
|
63
|
+
import * as esbuild from "esbuild";
|
|
64
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
65
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
66
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
67
|
+
}
|
|
68
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
69
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
70
|
+
const wrapperCode = (
|
|
71
|
+
/* ts */
|
|
72
|
+
`
|
|
73
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
74
|
+
import { createRoot } from "react-dom/client";
|
|
75
|
+
import { createElement } from "react";
|
|
76
|
+
|
|
77
|
+
(function scopeRenderHarness() {
|
|
78
|
+
var Component =
|
|
79
|
+
__scopeMod["default"] ||
|
|
80
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
81
|
+
(Object.values(__scopeMod).find(
|
|
82
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
83
|
+
));
|
|
84
|
+
|
|
85
|
+
if (!Component) {
|
|
86
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
87
|
+
"No renderable component found. Checked: default, " +
|
|
88
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
89
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
90
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
var props = ${propsJson};
|
|
96
|
+
var rootEl = document.getElementById("scope-root");
|
|
97
|
+
if (!rootEl) {
|
|
98
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
99
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
103
|
+
// Use requestAnimationFrame to let React flush the render
|
|
104
|
+
requestAnimationFrame(function() {
|
|
105
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
109
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
const result = await esbuild.build({
|
|
115
|
+
stdin: {
|
|
116
|
+
contents: wrapperCode,
|
|
117
|
+
// Resolve relative imports (within the component's dir)
|
|
118
|
+
resolveDir: dirname(filePath),
|
|
119
|
+
loader: "tsx",
|
|
120
|
+
sourcefile: "__scope_harness__.tsx"
|
|
121
|
+
},
|
|
122
|
+
bundle: true,
|
|
123
|
+
format: "iife",
|
|
124
|
+
write: false,
|
|
125
|
+
platform: "browser",
|
|
126
|
+
jsx: "automatic",
|
|
127
|
+
jsxImportSource: "react",
|
|
128
|
+
target: "es2020",
|
|
129
|
+
// Bundle everything — no externals
|
|
130
|
+
external: [],
|
|
131
|
+
define: {
|
|
132
|
+
"process.env.NODE_ENV": '"development"',
|
|
133
|
+
global: "globalThis"
|
|
134
|
+
},
|
|
135
|
+
logLevel: "silent",
|
|
136
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
137
|
+
banner: {
|
|
138
|
+
js: "/* @agent-scope/cli component harness */"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (result.errors.length > 0) {
|
|
142
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
143
|
+
throw new Error(`esbuild failed to bundle component:
|
|
144
|
+
${msg}`);
|
|
145
|
+
}
|
|
146
|
+
const outputFile = result.outputFiles?.[0];
|
|
147
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
148
|
+
throw new Error("esbuild produced no output");
|
|
149
|
+
}
|
|
150
|
+
return outputFile.text;
|
|
151
|
+
}
|
|
152
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
153
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
154
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
155
|
+
</style>` : "";
|
|
156
|
+
return `<!DOCTYPE html>
|
|
157
|
+
<html lang="en">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="UTF-8" />
|
|
160
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
161
|
+
<style>
|
|
162
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
163
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
164
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
165
|
+
</style>
|
|
166
|
+
${projectStyleBlock}
|
|
167
|
+
</head>
|
|
168
|
+
<body>
|
|
169
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
170
|
+
<script>${bundledScript}</script>
|
|
171
|
+
</body>
|
|
172
|
+
</html>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/manifest-formatter.ts
|
|
176
|
+
function isTTY() {
|
|
177
|
+
return process.stdout.isTTY === true;
|
|
178
|
+
}
|
|
179
|
+
function pad(value, width) {
|
|
180
|
+
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
181
|
+
}
|
|
182
|
+
function buildTable(headers, rows) {
|
|
183
|
+
const colWidths = headers.map(
|
|
184
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
185
|
+
);
|
|
186
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
187
|
+
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
188
|
+
const dataRows = rows.map(
|
|
189
|
+
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
190
|
+
);
|
|
191
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
192
|
+
}
|
|
193
|
+
function formatListTable(rows) {
|
|
194
|
+
if (rows.length === 0) return "No components found.";
|
|
195
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
196
|
+
const tableRows = rows.map((r) => [
|
|
197
|
+
r.name,
|
|
198
|
+
r.file,
|
|
199
|
+
r.complexityClass,
|
|
200
|
+
String(r.hookCount),
|
|
201
|
+
String(r.contextCount)
|
|
202
|
+
]);
|
|
203
|
+
return buildTable(headers, tableRows);
|
|
204
|
+
}
|
|
205
|
+
function formatListJson(rows) {
|
|
206
|
+
return JSON.stringify(rows, null, 2);
|
|
207
|
+
}
|
|
208
|
+
function formatSideEffects(se) {
|
|
209
|
+
const parts = [];
|
|
210
|
+
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
211
|
+
if (se.timers) parts.push("timers");
|
|
212
|
+
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
213
|
+
if (se.globalListeners) parts.push("globalListeners");
|
|
214
|
+
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
215
|
+
}
|
|
216
|
+
function formatGetTable(name, descriptor) {
|
|
217
|
+
const propNames = Object.keys(descriptor.props);
|
|
218
|
+
const lines = [
|
|
219
|
+
`Component: ${name}`,
|
|
220
|
+
` File: ${descriptor.filePath}`,
|
|
221
|
+
` Export: ${descriptor.exportType}`,
|
|
222
|
+
` Display Name: ${descriptor.displayName}`,
|
|
223
|
+
` Complexity: ${descriptor.complexityClass}`,
|
|
224
|
+
` Memoized: ${descriptor.memoized}`,
|
|
225
|
+
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
226
|
+
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
227
|
+
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
228
|
+
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
229
|
+
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
230
|
+
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
231
|
+
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
232
|
+
"",
|
|
233
|
+
` Props (${propNames.length}):`
|
|
234
|
+
];
|
|
235
|
+
if (propNames.length === 0) {
|
|
236
|
+
lines.push(" (none)");
|
|
237
|
+
} else {
|
|
238
|
+
for (const propName of propNames) {
|
|
239
|
+
const p = descriptor.props[propName];
|
|
240
|
+
if (p === void 0) continue;
|
|
241
|
+
const req = p.required ? "required" : "optional";
|
|
242
|
+
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
243
|
+
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
244
|
+
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return lines.join("\n");
|
|
248
|
+
}
|
|
249
|
+
function formatGetJson(name, descriptor) {
|
|
250
|
+
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
251
|
+
}
|
|
252
|
+
function formatQueryTable(rows, queryDesc) {
|
|
253
|
+
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
254
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
255
|
+
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
256
|
+
return `Query: ${queryDesc}
|
|
257
|
+
|
|
258
|
+
${buildTable(headers, tableRows)}`;
|
|
259
|
+
}
|
|
260
|
+
function formatQueryJson(rows) {
|
|
261
|
+
return JSON.stringify(rows, null, 2);
|
|
262
|
+
}
|
|
263
|
+
function matchGlob(pattern, value) {
|
|
264
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
265
|
+
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
266
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
267
|
+
return regex.test(value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/render-formatter.ts
|
|
271
|
+
function parseViewport(spec) {
|
|
272
|
+
const lower = spec.toLowerCase();
|
|
273
|
+
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
274
|
+
if (!match) {
|
|
275
|
+
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
276
|
+
}
|
|
277
|
+
const width = parseInt(match[1] ?? "0", 10);
|
|
278
|
+
const height = parseInt(match[2] ?? "0", 10);
|
|
279
|
+
if (width <= 0 || height <= 0) {
|
|
280
|
+
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
281
|
+
}
|
|
282
|
+
return { width, height };
|
|
283
|
+
}
|
|
284
|
+
function formatRenderJson(componentName, props, result) {
|
|
285
|
+
return {
|
|
286
|
+
component: componentName,
|
|
287
|
+
props,
|
|
288
|
+
width: result.width,
|
|
289
|
+
height: result.height,
|
|
290
|
+
renderTimeMs: result.renderTimeMs,
|
|
291
|
+
computedStyles: result.computedStyles,
|
|
292
|
+
screenshot: result.screenshot.toString("base64"),
|
|
293
|
+
dom: result.dom,
|
|
294
|
+
console: result.console,
|
|
295
|
+
accessibility: result.accessibility
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function formatMatrixJson(result) {
|
|
299
|
+
return {
|
|
300
|
+
axes: result.axes.map((axis) => ({
|
|
301
|
+
name: axis.name,
|
|
302
|
+
values: axis.values.map((v) => String(v))
|
|
303
|
+
})),
|
|
304
|
+
stats: { ...result.stats },
|
|
305
|
+
cells: result.cells.map((cell) => ({
|
|
306
|
+
index: cell.index,
|
|
307
|
+
axisIndices: cell.axisIndices,
|
|
308
|
+
props: cell.props,
|
|
309
|
+
renderTimeMs: cell.result.renderTimeMs,
|
|
310
|
+
width: cell.result.width,
|
|
311
|
+
height: cell.result.height,
|
|
312
|
+
screenshot: cell.result.screenshot.toString("base64")
|
|
313
|
+
}))
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function formatMatrixHtml(componentName, result) {
|
|
317
|
+
const cellsHtml = result.cells.map((cell) => {
|
|
318
|
+
const b64 = cell.result.screenshot.toString("base64");
|
|
319
|
+
const propLabel = escapeHtml(
|
|
320
|
+
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
321
|
+
);
|
|
322
|
+
return ` <div class="cell">
|
|
323
|
+
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
324
|
+
<div class="label">${propLabel}</div>
|
|
325
|
+
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
326
|
+
</div>`;
|
|
327
|
+
}).join("\n");
|
|
328
|
+
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
329
|
+
return `<!DOCTYPE html>
|
|
330
|
+
<html lang="en">
|
|
331
|
+
<head>
|
|
332
|
+
<meta charset="UTF-8" />
|
|
333
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
334
|
+
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
335
|
+
<style>
|
|
336
|
+
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
337
|
+
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
338
|
+
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
339
|
+
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
340
|
+
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
341
|
+
.cell img { display: block; }
|
|
342
|
+
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
343
|
+
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
344
|
+
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
345
|
+
</style>
|
|
346
|
+
</head>
|
|
347
|
+
<body>
|
|
348
|
+
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
349
|
+
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
350
|
+
<div class="grid">
|
|
351
|
+
${cellsHtml}
|
|
352
|
+
</div>
|
|
353
|
+
<div class="stats">
|
|
354
|
+
${result.stats.totalCells} cells \xB7
|
|
355
|
+
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
356
|
+
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
357
|
+
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
358
|
+
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
359
|
+
</div>
|
|
360
|
+
</body>
|
|
361
|
+
</html>
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
function formatMatrixCsv(componentName, result) {
|
|
365
|
+
const axisNames = result.axes.map((a) => a.name);
|
|
366
|
+
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
367
|
+
const rows = result.cells.map((cell) => {
|
|
368
|
+
const axisVals = result.axes.map((_, i) => {
|
|
369
|
+
const axisIdx = cell.axisIndices[i];
|
|
370
|
+
const axis = result.axes[i];
|
|
371
|
+
if (axisIdx === void 0 || axis === void 0) return "";
|
|
372
|
+
const val = axis.values[axisIdx];
|
|
373
|
+
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
374
|
+
});
|
|
375
|
+
return [
|
|
376
|
+
csvEscape(componentName),
|
|
377
|
+
...axisVals,
|
|
378
|
+
cell.result.renderTimeMs.toFixed(3),
|
|
379
|
+
String(cell.result.width),
|
|
380
|
+
String(cell.result.height)
|
|
381
|
+
].join(",");
|
|
382
|
+
});
|
|
383
|
+
return `${[headers.join(","), ...rows].join("\n")}
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
387
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
388
|
+
const empty = barWidth - filled;
|
|
389
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
390
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
391
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
392
|
+
}
|
|
393
|
+
function formatSummaryText(results, outputDir) {
|
|
394
|
+
const total = results.length;
|
|
395
|
+
const passed = results.filter((r) => r.success).length;
|
|
396
|
+
const failed = total - passed;
|
|
397
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
398
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
399
|
+
const lines = [
|
|
400
|
+
"\u2500".repeat(60),
|
|
401
|
+
`Render Summary`,
|
|
402
|
+
"\u2500".repeat(60),
|
|
403
|
+
` Total components : ${total}`,
|
|
404
|
+
` Passed : ${passed}`,
|
|
405
|
+
` Failed : ${failed}`,
|
|
406
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
407
|
+
` Output dir : ${outputDir}`
|
|
408
|
+
];
|
|
409
|
+
if (failed > 0) {
|
|
410
|
+
lines.push("", " Failed components:");
|
|
411
|
+
for (const r of results) {
|
|
412
|
+
if (!r.success) {
|
|
413
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
lines.push("\u2500".repeat(60));
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
420
|
+
function escapeHtml(str) {
|
|
421
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
422
|
+
}
|
|
423
|
+
function csvEscape(value) {
|
|
424
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
425
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
426
|
+
}
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/tailwind-css.ts
|
|
431
|
+
import { existsSync, readFileSync } from "fs";
|
|
432
|
+
import { createRequire } from "module";
|
|
433
|
+
import { resolve } from "path";
|
|
434
|
+
var CONFIG_FILENAMES = [
|
|
435
|
+
".reactscope/config.json",
|
|
436
|
+
".reactscope/config.js",
|
|
437
|
+
".reactscope/config.mjs"
|
|
438
|
+
];
|
|
439
|
+
var STYLE_ENTRY_CANDIDATES = [
|
|
440
|
+
"src/index.css",
|
|
441
|
+
"src/globals.css",
|
|
442
|
+
"app/globals.css",
|
|
443
|
+
"app/index.css",
|
|
444
|
+
"styles/index.css",
|
|
445
|
+
"index.css"
|
|
446
|
+
];
|
|
447
|
+
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
448
|
+
var compilerCache = null;
|
|
449
|
+
function getCachedBuild(cwd) {
|
|
450
|
+
if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
|
|
451
|
+
return compilerCache.build;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
function findStylesEntry(cwd) {
|
|
456
|
+
for (const name of CONFIG_FILENAMES) {
|
|
457
|
+
const p = resolve(cwd, name);
|
|
458
|
+
if (!existsSync(p)) continue;
|
|
459
|
+
try {
|
|
460
|
+
if (name.endsWith(".json")) {
|
|
461
|
+
const raw = readFileSync(p, "utf-8");
|
|
462
|
+
const data = JSON.parse(raw);
|
|
463
|
+
const scope = data.scope;
|
|
464
|
+
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
465
|
+
if (typeof entry === "string") {
|
|
466
|
+
const full = resolve(cwd, entry);
|
|
467
|
+
if (existsSync(full)) return full;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
474
|
+
if (existsSync(pkgPath)) {
|
|
475
|
+
try {
|
|
476
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
477
|
+
const pkg = JSON.parse(raw);
|
|
478
|
+
const entry = pkg.scope?.stylesEntry;
|
|
479
|
+
if (typeof entry === "string") {
|
|
480
|
+
const full = resolve(cwd, entry);
|
|
481
|
+
if (existsSync(full)) return full;
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
487
|
+
const full = resolve(cwd, candidate);
|
|
488
|
+
if (existsSync(full)) {
|
|
489
|
+
try {
|
|
490
|
+
const content = readFileSync(full, "utf-8");
|
|
491
|
+
if (TAILWIND_IMPORT.test(content)) return full;
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
async function getTailwindCompiler(cwd) {
|
|
499
|
+
const cached = getCachedBuild(cwd);
|
|
500
|
+
if (cached !== null) return cached;
|
|
501
|
+
const entryPath = findStylesEntry(cwd);
|
|
502
|
+
if (entryPath === null) return null;
|
|
503
|
+
let compile;
|
|
504
|
+
try {
|
|
505
|
+
const require2 = createRequire(resolve(cwd, "package.json"));
|
|
506
|
+
const tailwind = require2("tailwindcss");
|
|
507
|
+
const fn = tailwind.compile;
|
|
508
|
+
if (typeof fn !== "function") return null;
|
|
509
|
+
compile = fn;
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const entryContent = readFileSync(entryPath, "utf-8");
|
|
514
|
+
const loadStylesheet = async (id, base) => {
|
|
515
|
+
if (id === "tailwindcss") {
|
|
516
|
+
const nodeModules = resolve(cwd, "node_modules");
|
|
517
|
+
const tailwindCssPath = resolve(nodeModules, "tailwindcss", "index.css");
|
|
518
|
+
if (!existsSync(tailwindCssPath)) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const content = readFileSync(tailwindCssPath, "utf-8");
|
|
524
|
+
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
525
|
+
}
|
|
526
|
+
const full = resolve(base, id);
|
|
527
|
+
if (existsSync(full)) {
|
|
528
|
+
const content = readFileSync(full, "utf-8");
|
|
529
|
+
return { path: full, base: resolve(full, ".."), content };
|
|
530
|
+
}
|
|
531
|
+
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
532
|
+
};
|
|
533
|
+
const result = await compile(entryContent, {
|
|
534
|
+
base: cwd,
|
|
535
|
+
from: entryPath,
|
|
536
|
+
loadStylesheet
|
|
537
|
+
});
|
|
538
|
+
const build2 = result.build.bind(result);
|
|
539
|
+
compilerCache = { cwd, build: build2 };
|
|
540
|
+
return build2;
|
|
541
|
+
}
|
|
542
|
+
async function getCompiledCssForClasses(cwd, classes) {
|
|
543
|
+
const build2 = await getTailwindCompiler(cwd);
|
|
544
|
+
if (build2 === null) return null;
|
|
545
|
+
const deduped = [...new Set(classes)].filter(Boolean);
|
|
546
|
+
if (deduped.length === 0) return null;
|
|
547
|
+
return build2(deduped);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/ci/commands.ts
|
|
551
|
+
var CI_EXIT = {
|
|
552
|
+
/** All checks passed */
|
|
553
|
+
OK: 0,
|
|
554
|
+
/** Compliance score below threshold */
|
|
555
|
+
COMPLIANCE_BELOW_THRESHOLD: 1,
|
|
556
|
+
/** Accessibility violations found */
|
|
557
|
+
A11Y_VIOLATIONS: 2,
|
|
558
|
+
/** Console errors detected during render */
|
|
559
|
+
CONSOLE_ERRORS: 3,
|
|
560
|
+
/** Visual regression detected against baseline */
|
|
561
|
+
VISUAL_REGRESSION: 4,
|
|
562
|
+
/** One or more components failed to render */
|
|
563
|
+
RENDER_FAILURES: 5
|
|
564
|
+
};
|
|
565
|
+
var ALL_CHECKS = ["compliance", "a11y", "console-errors", "visual-regression"];
|
|
566
|
+
var _pool = null;
|
|
567
|
+
async function getPool(viewportWidth, viewportHeight) {
|
|
568
|
+
if (_pool === null) {
|
|
569
|
+
_pool = new BrowserPool({
|
|
570
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
571
|
+
viewportWidth,
|
|
572
|
+
viewportHeight
|
|
573
|
+
});
|
|
574
|
+
await _pool.init();
|
|
575
|
+
}
|
|
576
|
+
return _pool;
|
|
577
|
+
}
|
|
578
|
+
async function shutdownPool() {
|
|
579
|
+
if (_pool !== null) {
|
|
580
|
+
await _pool.close();
|
|
581
|
+
_pool = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
585
|
+
const pool = await getPool(viewportWidth, viewportHeight);
|
|
586
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
587
|
+
const slot = await pool.acquire();
|
|
588
|
+
const { page } = slot;
|
|
589
|
+
try {
|
|
590
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
591
|
+
await page.waitForFunction(
|
|
592
|
+
() => {
|
|
593
|
+
const w = window;
|
|
594
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
595
|
+
},
|
|
596
|
+
{ timeout: 15e3 }
|
|
597
|
+
);
|
|
598
|
+
const renderError = await page.evaluate(() => {
|
|
599
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
600
|
+
});
|
|
601
|
+
if (renderError !== null) {
|
|
602
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
603
|
+
}
|
|
604
|
+
const rootDir = process.cwd();
|
|
605
|
+
const classes = await page.evaluate(() => {
|
|
606
|
+
const set = /* @__PURE__ */ new Set();
|
|
607
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
608
|
+
for (const c of el.className.split(/\s+/)) {
|
|
609
|
+
if (c) set.add(c);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
return [...set];
|
|
613
|
+
});
|
|
614
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
615
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
616
|
+
await page.addStyleTag({ content: projectCss });
|
|
617
|
+
}
|
|
618
|
+
const startMs = performance.now();
|
|
619
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
620
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
621
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const PAD = 24;
|
|
627
|
+
const MIN_W = 320;
|
|
628
|
+
const MIN_H = 200;
|
|
629
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
630
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
631
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
632
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
633
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
634
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
635
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
636
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
637
|
+
const screenshot = await page.screenshot({
|
|
638
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
639
|
+
type: "png"
|
|
640
|
+
});
|
|
641
|
+
const computedStylesRaw = {};
|
|
642
|
+
const styles = await page.evaluate((sel) => {
|
|
643
|
+
const el = document.querySelector(sel);
|
|
644
|
+
if (el === null) return {};
|
|
645
|
+
const computed = window.getComputedStyle(el);
|
|
646
|
+
const out = {};
|
|
647
|
+
for (const prop of [
|
|
648
|
+
"display",
|
|
649
|
+
"width",
|
|
650
|
+
"height",
|
|
651
|
+
"color",
|
|
652
|
+
"backgroundColor",
|
|
653
|
+
"fontSize",
|
|
654
|
+
"fontFamily",
|
|
655
|
+
"padding",
|
|
656
|
+
"margin"
|
|
657
|
+
]) {
|
|
658
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}, "[data-reactscope-root] > *");
|
|
662
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
663
|
+
const renderTimeMs = performance.now() - startMs;
|
|
664
|
+
return {
|
|
665
|
+
screenshot,
|
|
666
|
+
width: Math.round(safeW),
|
|
667
|
+
height: Math.round(safeH),
|
|
668
|
+
renderTimeMs,
|
|
669
|
+
computedStyles: computedStylesRaw
|
|
670
|
+
};
|
|
671
|
+
} finally {
|
|
672
|
+
pool.release(slot);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
676
|
+
const flat = {};
|
|
677
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
678
|
+
Object.assign(flat, styles);
|
|
679
|
+
}
|
|
680
|
+
const colors = {};
|
|
681
|
+
const spacing = {};
|
|
682
|
+
const typography = {};
|
|
683
|
+
const borders = {};
|
|
684
|
+
const shadows = {};
|
|
685
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
686
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
687
|
+
colors[prop] = value;
|
|
688
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
689
|
+
spacing[prop] = value;
|
|
690
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
691
|
+
typography[prop] = value;
|
|
692
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
693
|
+
borders[prop] = value;
|
|
694
|
+
} else if (prop === "boxShadow") {
|
|
695
|
+
shadows[prop] = value;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return { colors, spacing, typography, borders, shadows };
|
|
699
|
+
}
|
|
700
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
701
|
+
const jsonPath = resolve2(baselineDir, "renders", `${componentName}.json`);
|
|
702
|
+
if (!existsSync2(jsonPath)) return null;
|
|
703
|
+
return JSON.parse(readFileSync2(jsonPath, "utf-8"));
|
|
704
|
+
}
|
|
705
|
+
async function runCi(options = {}) {
|
|
706
|
+
const {
|
|
707
|
+
baselineDir: baselineDirRaw,
|
|
708
|
+
checks = ALL_CHECKS,
|
|
709
|
+
complianceThreshold = 0.9,
|
|
710
|
+
viewportWidth = 375,
|
|
711
|
+
viewportHeight = 812
|
|
712
|
+
} = options;
|
|
713
|
+
const startTime = performance.now();
|
|
714
|
+
const rootDir = process.cwd();
|
|
715
|
+
const checksSet = new Set(checks);
|
|
716
|
+
const baselineDir = baselineDirRaw !== void 0 ? resolve2(rootDir, baselineDirRaw) : void 0;
|
|
717
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
718
|
+
const manifest = await generateManifest({ rootDir });
|
|
719
|
+
const componentNames = Object.keys(manifest.components);
|
|
720
|
+
const total = componentNames.length;
|
|
721
|
+
process.stderr.write(`Found ${total} components.
|
|
722
|
+
`);
|
|
723
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
724
|
+
`);
|
|
725
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
726
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
727
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
728
|
+
let completed = 0;
|
|
729
|
+
const CONCURRENCY = 4;
|
|
730
|
+
let nextIdx = 0;
|
|
731
|
+
const renderOne = async (name) => {
|
|
732
|
+
const descriptor = manifest.components[name];
|
|
733
|
+
if (descriptor === void 0) return;
|
|
734
|
+
const filePath = resolve2(rootDir, descriptor.filePath);
|
|
735
|
+
const outcome = await safeRender(
|
|
736
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
737
|
+
{
|
|
738
|
+
props: {},
|
|
739
|
+
sourceLocation: {
|
|
740
|
+
file: descriptor.filePath,
|
|
741
|
+
line: descriptor.loc.start,
|
|
742
|
+
column: 0
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
completed++;
|
|
747
|
+
const pct = Math.round(completed / total * 100);
|
|
748
|
+
if (isTTY()) {
|
|
749
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
750
|
+
}
|
|
751
|
+
if (outcome.crashed) {
|
|
752
|
+
renderFailures.add(name);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const result = outcome.result;
|
|
756
|
+
currentRenderMeta.set(name, { width: result.width, height: result.height });
|
|
757
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
758
|
+
};
|
|
759
|
+
if (total > 0) {
|
|
760
|
+
const worker = async () => {
|
|
761
|
+
while (nextIdx < componentNames.length) {
|
|
762
|
+
const i = nextIdx++;
|
|
763
|
+
const name = componentNames[i];
|
|
764
|
+
if (name !== void 0) {
|
|
765
|
+
await renderOne(name);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const workers = [];
|
|
770
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
771
|
+
workers.push(worker());
|
|
772
|
+
}
|
|
773
|
+
await Promise.all(workers);
|
|
774
|
+
}
|
|
775
|
+
await shutdownPool();
|
|
776
|
+
if (isTTY() && total > 0) {
|
|
777
|
+
process.stderr.write("\n");
|
|
778
|
+
}
|
|
779
|
+
const resolver = new TokenResolver([]);
|
|
780
|
+
const engine = new ComplianceEngine(resolver);
|
|
781
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
782
|
+
const complianceScore = batchReport.aggregateCompliance;
|
|
783
|
+
const checkResults = [];
|
|
784
|
+
const renderFailureCount = renderFailures.size;
|
|
785
|
+
const rendersPassed = renderFailureCount === 0;
|
|
786
|
+
if (checksSet.has("compliance")) {
|
|
787
|
+
const compliancePassed = complianceScore >= complianceThreshold;
|
|
788
|
+
checkResults.push({
|
|
789
|
+
check: "compliance",
|
|
790
|
+
passed: compliancePassed,
|
|
791
|
+
message: compliancePassed ? `Compliance ${(complianceScore * 100).toFixed(1)}% >= threshold ${(complianceThreshold * 100).toFixed(1)}%` : `Compliance ${(complianceScore * 100).toFixed(1)}% < threshold ${(complianceThreshold * 100).toFixed(1)}%`,
|
|
792
|
+
value: complianceScore,
|
|
793
|
+
threshold: complianceThreshold
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (checksSet.has("a11y")) {
|
|
797
|
+
checkResults.push({
|
|
798
|
+
check: "a11y",
|
|
799
|
+
passed: true,
|
|
800
|
+
message: "Accessibility audit not yet implemented \u2014 skipped"
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (checksSet.has("console-errors")) {
|
|
804
|
+
const consoleErrorsPassed = rendersPassed;
|
|
805
|
+
checkResults.push({
|
|
806
|
+
check: "console-errors",
|
|
807
|
+
passed: consoleErrorsPassed,
|
|
808
|
+
message: consoleErrorsPassed ? "No console errors detected" : `Console errors likely \u2014 ${renderFailureCount} component(s) failed to render`
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
let hasVisualRegression = false;
|
|
812
|
+
if (checksSet.has("visual-regression") && baselineDir !== void 0) {
|
|
813
|
+
if (!existsSync2(baselineDir)) {
|
|
814
|
+
process.stderr.write(
|
|
815
|
+
`Warning: baseline directory not found at "${baselineDir}" \u2014 skipping visual regression check.
|
|
816
|
+
`
|
|
817
|
+
);
|
|
818
|
+
checkResults.push({
|
|
819
|
+
check: "visual-regression",
|
|
820
|
+
passed: true,
|
|
821
|
+
message: `Baseline not found at ${baselineDir} \u2014 skipped`
|
|
822
|
+
});
|
|
823
|
+
} else {
|
|
824
|
+
const regressions = [];
|
|
825
|
+
for (const name of componentNames) {
|
|
826
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
827
|
+
const currentMeta = currentRenderMeta.get(name);
|
|
828
|
+
if (baselineMeta !== null && currentMeta !== void 0) {
|
|
829
|
+
const dw = Math.abs(currentMeta.width - baselineMeta.width);
|
|
830
|
+
const dh = Math.abs(currentMeta.height - baselineMeta.height);
|
|
831
|
+
if (dw > 10 || dh > 10) {
|
|
832
|
+
regressions.push(name);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
hasVisualRegression = regressions.length > 0;
|
|
837
|
+
checkResults.push({
|
|
838
|
+
check: "visual-regression",
|
|
839
|
+
passed: !hasVisualRegression,
|
|
840
|
+
message: hasVisualRegression ? `Visual regression detected in ${regressions.length} component(s): ${regressions.slice(0, 5).join(", ")}${regressions.length > 5 ? "..." : ""}` : "No visual regressions detected"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
let exitCode = CI_EXIT.OK;
|
|
845
|
+
const complianceResult = checkResults.find((r) => r.check === "compliance");
|
|
846
|
+
const a11yResult = checkResults.find((r) => r.check === "a11y");
|
|
847
|
+
const consoleResult = checkResults.find((r) => r.check === "console-errors");
|
|
848
|
+
const visualResult = checkResults.find((r) => r.check === "visual-regression");
|
|
849
|
+
if (complianceResult !== void 0 && !complianceResult.passed) {
|
|
850
|
+
exitCode = CI_EXIT.COMPLIANCE_BELOW_THRESHOLD;
|
|
851
|
+
} else if (a11yResult !== void 0 && !a11yResult.passed) {
|
|
852
|
+
exitCode = CI_EXIT.A11Y_VIOLATIONS;
|
|
853
|
+
} else if (consoleResult !== void 0 && !consoleResult.passed) {
|
|
854
|
+
exitCode = CI_EXIT.CONSOLE_ERRORS;
|
|
855
|
+
} else if (visualResult !== void 0 && !visualResult.passed) {
|
|
856
|
+
exitCode = CI_EXIT.VISUAL_REGRESSION;
|
|
857
|
+
} else if (!rendersPassed) {
|
|
858
|
+
exitCode = CI_EXIT.RENDER_FAILURES;
|
|
859
|
+
}
|
|
860
|
+
const passed = exitCode === CI_EXIT.OK;
|
|
861
|
+
const wallClockMs = performance.now() - startTime;
|
|
862
|
+
return {
|
|
863
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
864
|
+
passed,
|
|
865
|
+
exitCode,
|
|
866
|
+
checks: checkResults,
|
|
867
|
+
components: {
|
|
868
|
+
total,
|
|
869
|
+
rendered: total - renderFailureCount,
|
|
870
|
+
failed: renderFailureCount
|
|
871
|
+
},
|
|
872
|
+
complianceScore,
|
|
873
|
+
complianceThreshold,
|
|
874
|
+
baselineCompared: baselineDir !== void 0 && existsSync2(baselineDir),
|
|
875
|
+
wallClockMs
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function formatCiReport(result) {
|
|
879
|
+
const lines = [];
|
|
880
|
+
const title = "Scope CI Report";
|
|
881
|
+
const rule2 = "=".repeat(Math.max(title.length, 40));
|
|
882
|
+
lines.push(title, rule2);
|
|
883
|
+
const { total, rendered, failed } = result.components;
|
|
884
|
+
lines.push(
|
|
885
|
+
`Components: ${total} total ${rendered} rendered${failed > 0 ? ` ${failed} failed` : ""}`
|
|
886
|
+
);
|
|
887
|
+
lines.push("");
|
|
888
|
+
for (const check of result.checks) {
|
|
889
|
+
const icon = check.passed ? "pass" : "FAIL";
|
|
890
|
+
lines.push(` [${icon}] ${check.message}`);
|
|
891
|
+
}
|
|
892
|
+
if (result.components.failed > 0) {
|
|
893
|
+
lines.push(` [FAIL] ${result.components.failed} component(s) failed to render`);
|
|
894
|
+
}
|
|
895
|
+
lines.push("");
|
|
896
|
+
lines.push(rule2);
|
|
897
|
+
if (result.passed) {
|
|
898
|
+
lines.push(
|
|
899
|
+
`CI passed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
900
|
+
);
|
|
901
|
+
} else {
|
|
902
|
+
lines.push(
|
|
903
|
+
`CI failed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
return lines.join("\n");
|
|
907
|
+
}
|
|
908
|
+
function parseChecks(raw) {
|
|
909
|
+
if (raw === void 0) return void 0;
|
|
910
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
911
|
+
if (parts.length === 0) return void 0;
|
|
912
|
+
const valid = new Set(ALL_CHECKS);
|
|
913
|
+
const parsed = [];
|
|
914
|
+
for (const part of parts) {
|
|
915
|
+
if (!valid.has(part)) {
|
|
916
|
+
process.stderr.write(
|
|
917
|
+
`Warning: unknown check "${part}" \u2014 valid checks are: ${ALL_CHECKS.join(", ")}
|
|
918
|
+
`
|
|
919
|
+
);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
parsed.push(part);
|
|
46
923
|
}
|
|
924
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
47
925
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
926
|
+
function createCiCommand() {
|
|
927
|
+
return new Command("ci").description(
|
|
928
|
+
"Run a non-interactive CI pipeline (manifest -> render -> compliance -> regression) with exit codes"
|
|
929
|
+
).option(
|
|
930
|
+
"-b, --baseline <dir>",
|
|
931
|
+
"Baseline directory for visual regression comparison (omit to skip)"
|
|
932
|
+
).option(
|
|
933
|
+
"--checks <list>",
|
|
934
|
+
`Comma-separated checks to run (default: all). Valid: ${ALL_CHECKS.join(", ")}`
|
|
935
|
+
).option("--threshold <n>", "Compliance pass threshold (0-1, default: 0.90)", "0.90").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").option("--json", "Emit structured JSON to stdout in addition to the summary", false).option("-o, --output <path>", "Write the CI result JSON to a file").action(
|
|
936
|
+
async (opts) => {
|
|
937
|
+
try {
|
|
938
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
939
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
940
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
941
|
+
const complianceThreshold = Number.parseFloat(opts.threshold);
|
|
942
|
+
const checks = parseChecks(opts.checks);
|
|
943
|
+
const result = await runCi({
|
|
944
|
+
baselineDir: opts.baseline,
|
|
945
|
+
checks,
|
|
946
|
+
complianceThreshold,
|
|
947
|
+
viewportWidth,
|
|
948
|
+
viewportHeight
|
|
949
|
+
});
|
|
950
|
+
if (opts.output !== void 0) {
|
|
951
|
+
const outPath = resolve2(process.cwd(), opts.output);
|
|
952
|
+
writeFileSync2(outPath, JSON.stringify(result, null, 2), "utf-8");
|
|
953
|
+
process.stderr.write(`CI result written to ${opts.output}
|
|
954
|
+
`);
|
|
955
|
+
}
|
|
956
|
+
process.stdout.write(`${formatCiReport(result)}
|
|
957
|
+
`);
|
|
958
|
+
if (opts.json) {
|
|
959
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
960
|
+
`);
|
|
961
|
+
}
|
|
962
|
+
process.exit(result.exitCode);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
965
|
+
`);
|
|
966
|
+
process.exit(CI_EXIT.RENDER_FAILURES);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
);
|
|
51
970
|
}
|
|
52
971
|
|
|
53
972
|
// src/init/index.ts
|
|
54
|
-
import { appendFileSync, existsSync as
|
|
973
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
55
974
|
import { join as join2 } from "path";
|
|
56
975
|
import * as readline from "readline";
|
|
57
976
|
|
|
58
977
|
// src/init/detect.ts
|
|
59
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
978
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
60
979
|
import { join } from "path";
|
|
61
980
|
function hasConfigFile(dir, stem) {
|
|
62
|
-
if (!
|
|
981
|
+
if (!existsSync3(dir)) return false;
|
|
63
982
|
try {
|
|
64
983
|
const entries = readdirSync(dir);
|
|
65
984
|
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
@@ -69,7 +988,7 @@ function hasConfigFile(dir, stem) {
|
|
|
69
988
|
}
|
|
70
989
|
function readSafe(path) {
|
|
71
990
|
try {
|
|
72
|
-
return
|
|
991
|
+
return readFileSync3(path, "utf-8");
|
|
73
992
|
} catch {
|
|
74
993
|
return null;
|
|
75
994
|
}
|
|
@@ -82,15 +1001,15 @@ function detectFramework(rootDir, packageDeps) {
|
|
|
82
1001
|
return "unknown";
|
|
83
1002
|
}
|
|
84
1003
|
function detectPackageManager(rootDir) {
|
|
85
|
-
if (
|
|
86
|
-
if (
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
1004
|
+
if (existsSync3(join(rootDir, "bun.lock"))) return "bun";
|
|
1005
|
+
if (existsSync3(join(rootDir, "yarn.lock"))) return "yarn";
|
|
1006
|
+
if (existsSync3(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1007
|
+
if (existsSync3(join(rootDir, "package-lock.json"))) return "npm";
|
|
89
1008
|
return "npm";
|
|
90
1009
|
}
|
|
91
1010
|
function detectTypeScript(rootDir) {
|
|
92
1011
|
const candidate = join(rootDir, "tsconfig.json");
|
|
93
|
-
if (
|
|
1012
|
+
if (existsSync3(candidate)) {
|
|
94
1013
|
return { typescript: true, tsconfigPath: candidate };
|
|
95
1014
|
}
|
|
96
1015
|
return { typescript: false, tsconfigPath: null };
|
|
@@ -103,7 +1022,7 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
103
1022
|
const altExt = typescript ? "jsx" : "jsx";
|
|
104
1023
|
for (const dir of COMPONENT_DIRS) {
|
|
105
1024
|
const absDir = join(rootDir, dir);
|
|
106
|
-
if (!
|
|
1025
|
+
if (!existsSync3(absDir)) continue;
|
|
107
1026
|
let hasComponents = false;
|
|
108
1027
|
try {
|
|
109
1028
|
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
@@ -158,7 +1077,7 @@ function detectTokenSources(rootDir) {
|
|
|
158
1077
|
}
|
|
159
1078
|
}
|
|
160
1079
|
const srcDir = join(rootDir, "src");
|
|
161
|
-
const dirsToScan =
|
|
1080
|
+
const dirsToScan = existsSync3(srcDir) ? [srcDir] : [];
|
|
162
1081
|
for (const scanDir of dirsToScan) {
|
|
163
1082
|
try {
|
|
164
1083
|
const entries = readdirSync(scanDir, { withFileTypes: true });
|
|
@@ -174,7 +1093,7 @@ function detectTokenSources(rootDir) {
|
|
|
174
1093
|
} catch {
|
|
175
1094
|
}
|
|
176
1095
|
}
|
|
177
|
-
if (
|
|
1096
|
+
if (existsSync3(srcDir)) {
|
|
178
1097
|
try {
|
|
179
1098
|
const entries = readdirSync(srcDir);
|
|
180
1099
|
for (const entry of entries) {
|
|
@@ -217,7 +1136,7 @@ function detectProject(rootDir) {
|
|
|
217
1136
|
}
|
|
218
1137
|
|
|
219
1138
|
// src/init/index.ts
|
|
220
|
-
import { Command } from "commander";
|
|
1139
|
+
import { Command as Command2 } from "commander";
|
|
221
1140
|
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
222
1141
|
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
223
1142
|
return {
|
|
@@ -255,9 +1174,9 @@ function createRL() {
|
|
|
255
1174
|
});
|
|
256
1175
|
}
|
|
257
1176
|
async function ask(rl, question) {
|
|
258
|
-
return new Promise((
|
|
1177
|
+
return new Promise((resolve15) => {
|
|
259
1178
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
1179
|
+
resolve15(answer.trim());
|
|
261
1180
|
});
|
|
262
1181
|
});
|
|
263
1182
|
}
|
|
@@ -267,8 +1186,8 @@ async function askWithDefault(rl, label, defaultValue) {
|
|
|
267
1186
|
}
|
|
268
1187
|
function ensureGitignoreEntry(rootDir, entry) {
|
|
269
1188
|
const gitignorePath = join2(rootDir, ".gitignore");
|
|
270
|
-
if (
|
|
271
|
-
const content =
|
|
1189
|
+
if (existsSync4(gitignorePath)) {
|
|
1190
|
+
const content = readFileSync4(gitignorePath, "utf-8");
|
|
272
1191
|
const normalised = entry.replace(/\/$/, "");
|
|
273
1192
|
const lines = content.split("\n").map((l) => l.trim());
|
|
274
1193
|
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
@@ -278,24 +1197,24 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
278
1197
|
appendFileSync(gitignorePath, `${suffix}${entry}
|
|
279
1198
|
`);
|
|
280
1199
|
} else {
|
|
281
|
-
|
|
1200
|
+
writeFileSync3(gitignorePath, `${entry}
|
|
282
1201
|
`);
|
|
283
1202
|
}
|
|
284
1203
|
}
|
|
285
1204
|
function scaffoldConfig(rootDir, config) {
|
|
286
1205
|
const path = join2(rootDir, "reactscope.config.json");
|
|
287
|
-
|
|
1206
|
+
writeFileSync3(path, `${JSON.stringify(config, null, 2)}
|
|
288
1207
|
`);
|
|
289
1208
|
return path;
|
|
290
1209
|
}
|
|
291
1210
|
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
292
1211
|
const path = join2(rootDir, tokenFile);
|
|
293
|
-
if (!
|
|
1212
|
+
if (!existsSync4(path)) {
|
|
294
1213
|
const stub = {
|
|
295
1214
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
296
1215
|
tokens: {}
|
|
297
1216
|
};
|
|
298
|
-
|
|
1217
|
+
writeFileSync3(path, `${JSON.stringify(stub, null, 2)}
|
|
299
1218
|
`);
|
|
300
1219
|
}
|
|
301
1220
|
return path;
|
|
@@ -304,8 +1223,8 @@ function scaffoldOutputDir(rootDir, outputDir) {
|
|
|
304
1223
|
const dirPath = join2(rootDir, outputDir);
|
|
305
1224
|
mkdirSync(dirPath, { recursive: true });
|
|
306
1225
|
const keepPath = join2(dirPath, ".gitkeep");
|
|
307
|
-
if (!
|
|
308
|
-
|
|
1226
|
+
if (!existsSync4(keepPath)) {
|
|
1227
|
+
writeFileSync3(keepPath, "");
|
|
309
1228
|
}
|
|
310
1229
|
return dirPath;
|
|
311
1230
|
}
|
|
@@ -313,7 +1232,7 @@ async function runInit(options) {
|
|
|
313
1232
|
const rootDir = options.cwd ?? process.cwd();
|
|
314
1233
|
const configPath = join2(rootDir, "reactscope.config.json");
|
|
315
1234
|
const created = [];
|
|
316
|
-
if (
|
|
1235
|
+
if (existsSync4(configPath) && !options.force) {
|
|
317
1236
|
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
318
1237
|
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
319
1238
|
`);
|
|
@@ -383,257 +1302,46 @@ async function runInit(options) {
|
|
|
383
1302
|
`);
|
|
384
1303
|
}
|
|
385
1304
|
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
386
|
-
return {
|
|
387
|
-
success: true,
|
|
388
|
-
message: "Project initialised successfully.",
|
|
389
|
-
created,
|
|
390
|
-
skipped: false
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
function createInitCommand() {
|
|
394
|
-
return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
395
|
-
try {
|
|
396
|
-
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
397
|
-
if (!result.success && !result.skipped) {
|
|
398
|
-
process.exit(1);
|
|
399
|
-
}
|
|
400
|
-
} catch (err) {
|
|
401
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
402
|
-
`);
|
|
403
|
-
process.exit(1);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// src/instrument/renders.ts
|
|
409
|
-
import { resolve as resolve5 } from "path";
|
|
410
|
-
import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
|
|
411
|
-
import { Command as Command4 } from "commander";
|
|
412
|
-
|
|
413
|
-
// src/component-bundler.ts
|
|
414
|
-
import { dirname } from "path";
|
|
415
|
-
import * as esbuild from "esbuild";
|
|
416
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
417
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
418
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
419
|
-
}
|
|
420
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
421
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
422
|
-
const wrapperCode = (
|
|
423
|
-
/* ts */
|
|
424
|
-
`
|
|
425
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
426
|
-
import { createRoot } from "react-dom/client";
|
|
427
|
-
import { createElement } from "react";
|
|
428
|
-
|
|
429
|
-
(function scopeRenderHarness() {
|
|
430
|
-
var Component =
|
|
431
|
-
__scopeMod["default"] ||
|
|
432
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
433
|
-
(Object.values(__scopeMod).find(
|
|
434
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
435
|
-
));
|
|
436
|
-
|
|
437
|
-
if (!Component) {
|
|
438
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
439
|
-
"No renderable component found. Checked: default, " +
|
|
440
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
441
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
442
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
var props = ${propsJson};
|
|
448
|
-
var rootEl = document.getElementById("scope-root");
|
|
449
|
-
if (!rootEl) {
|
|
450
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
451
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
455
|
-
// Use requestAnimationFrame to let React flush the render
|
|
456
|
-
requestAnimationFrame(function() {
|
|
457
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
458
|
-
});
|
|
459
|
-
} catch (err) {
|
|
460
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
461
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
462
|
-
}
|
|
463
|
-
})();
|
|
464
|
-
`
|
|
465
|
-
);
|
|
466
|
-
const result = await esbuild.build({
|
|
467
|
-
stdin: {
|
|
468
|
-
contents: wrapperCode,
|
|
469
|
-
// Resolve relative imports (within the component's dir)
|
|
470
|
-
resolveDir: dirname(filePath),
|
|
471
|
-
loader: "tsx",
|
|
472
|
-
sourcefile: "__scope_harness__.tsx"
|
|
473
|
-
},
|
|
474
|
-
bundle: true,
|
|
475
|
-
format: "iife",
|
|
476
|
-
write: false,
|
|
477
|
-
platform: "browser",
|
|
478
|
-
jsx: "automatic",
|
|
479
|
-
jsxImportSource: "react",
|
|
480
|
-
target: "es2020",
|
|
481
|
-
// Bundle everything — no externals
|
|
482
|
-
external: [],
|
|
483
|
-
define: {
|
|
484
|
-
"process.env.NODE_ENV": '"development"',
|
|
485
|
-
global: "globalThis"
|
|
486
|
-
},
|
|
487
|
-
logLevel: "silent",
|
|
488
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
489
|
-
banner: {
|
|
490
|
-
js: "/* @agent-scope/cli component harness */"
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
if (result.errors.length > 0) {
|
|
494
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
495
|
-
throw new Error(`esbuild failed to bundle component:
|
|
496
|
-
${msg}`);
|
|
497
|
-
}
|
|
498
|
-
const outputFile = result.outputFiles?.[0];
|
|
499
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
500
|
-
throw new Error("esbuild produced no output");
|
|
501
|
-
}
|
|
502
|
-
return outputFile.text;
|
|
503
|
-
}
|
|
504
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
505
|
-
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
506
|
-
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
507
|
-
</style>` : "";
|
|
508
|
-
return `<!DOCTYPE html>
|
|
509
|
-
<html lang="en">
|
|
510
|
-
<head>
|
|
511
|
-
<meta charset="UTF-8" />
|
|
512
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
513
|
-
<style>
|
|
514
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
515
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
516
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
517
|
-
</style>
|
|
518
|
-
${projectStyleBlock}
|
|
519
|
-
</head>
|
|
520
|
-
<body>
|
|
521
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
522
|
-
<script>${bundledScript}</script>
|
|
523
|
-
</body>
|
|
524
|
-
</html>`;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// src/manifest-commands.ts
|
|
528
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
529
|
-
import { resolve } from "path";
|
|
530
|
-
import { generateManifest } from "@agent-scope/manifest";
|
|
531
|
-
import { Command as Command2 } from "commander";
|
|
532
|
-
|
|
533
|
-
// src/manifest-formatter.ts
|
|
534
|
-
function isTTY() {
|
|
535
|
-
return process.stdout.isTTY === true;
|
|
536
|
-
}
|
|
537
|
-
function pad(value, width) {
|
|
538
|
-
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
539
|
-
}
|
|
540
|
-
function buildTable(headers, rows) {
|
|
541
|
-
const colWidths = headers.map(
|
|
542
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
543
|
-
);
|
|
544
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
545
|
-
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
546
|
-
const dataRows = rows.map(
|
|
547
|
-
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
548
|
-
);
|
|
549
|
-
return [headerRow, divider, ...dataRows].join("\n");
|
|
550
|
-
}
|
|
551
|
-
function formatListTable(rows) {
|
|
552
|
-
if (rows.length === 0) return "No components found.";
|
|
553
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
554
|
-
const tableRows = rows.map((r) => [
|
|
555
|
-
r.name,
|
|
556
|
-
r.file,
|
|
557
|
-
r.complexityClass,
|
|
558
|
-
String(r.hookCount),
|
|
559
|
-
String(r.contextCount)
|
|
560
|
-
]);
|
|
561
|
-
return buildTable(headers, tableRows);
|
|
562
|
-
}
|
|
563
|
-
function formatListJson(rows) {
|
|
564
|
-
return JSON.stringify(rows, null, 2);
|
|
565
|
-
}
|
|
566
|
-
function formatSideEffects(se) {
|
|
567
|
-
const parts = [];
|
|
568
|
-
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
569
|
-
if (se.timers) parts.push("timers");
|
|
570
|
-
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
571
|
-
if (se.globalListeners) parts.push("globalListeners");
|
|
572
|
-
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
573
|
-
}
|
|
574
|
-
function formatGetTable(name, descriptor) {
|
|
575
|
-
const propNames = Object.keys(descriptor.props);
|
|
576
|
-
const lines = [
|
|
577
|
-
`Component: ${name}`,
|
|
578
|
-
` File: ${descriptor.filePath}`,
|
|
579
|
-
` Export: ${descriptor.exportType}`,
|
|
580
|
-
` Display Name: ${descriptor.displayName}`,
|
|
581
|
-
` Complexity: ${descriptor.complexityClass}`,
|
|
582
|
-
` Memoized: ${descriptor.memoized}`,
|
|
583
|
-
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
584
|
-
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
585
|
-
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
586
|
-
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
587
|
-
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
588
|
-
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
589
|
-
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
590
|
-
"",
|
|
591
|
-
` Props (${propNames.length}):`
|
|
592
|
-
];
|
|
593
|
-
if (propNames.length === 0) {
|
|
594
|
-
lines.push(" (none)");
|
|
595
|
-
} else {
|
|
596
|
-
for (const propName of propNames) {
|
|
597
|
-
const p = descriptor.props[propName];
|
|
598
|
-
if (p === void 0) continue;
|
|
599
|
-
const req = p.required ? "required" : "optional";
|
|
600
|
-
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
601
|
-
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
602
|
-
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return lines.join("\n");
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
message: "Project initialised successfully.",
|
|
1308
|
+
created,
|
|
1309
|
+
skipped: false
|
|
1310
|
+
};
|
|
606
1311
|
}
|
|
607
|
-
function
|
|
608
|
-
return
|
|
1312
|
+
function createInitCommand() {
|
|
1313
|
+
return new Command2("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
1314
|
+
try {
|
|
1315
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1316
|
+
if (!result.success && !result.skipped) {
|
|
1317
|
+
process.exit(1);
|
|
1318
|
+
}
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1321
|
+
`);
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
609
1325
|
}
|
|
610
|
-
function formatQueryTable(rows, queryDesc) {
|
|
611
|
-
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
612
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
613
|
-
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
614
|
-
return `Query: ${queryDesc}
|
|
615
1326
|
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
function matchGlob(pattern, value) {
|
|
622
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
623
|
-
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
624
|
-
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
625
|
-
return regex.test(value);
|
|
626
|
-
}
|
|
1327
|
+
// src/instrument/renders.ts
|
|
1328
|
+
import { resolve as resolve7 } from "path";
|
|
1329
|
+
import { BrowserPool as BrowserPool3 } from "@agent-scope/render";
|
|
1330
|
+
import { Command as Command5 } from "commander";
|
|
627
1331
|
|
|
628
1332
|
// src/manifest-commands.ts
|
|
1333
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1334
|
+
import { resolve as resolve3 } from "path";
|
|
1335
|
+
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
1336
|
+
import { Command as Command3 } from "commander";
|
|
629
1337
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
630
1338
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
631
|
-
const absPath =
|
|
632
|
-
if (!
|
|
1339
|
+
const absPath = resolve3(process.cwd(), manifestPath);
|
|
1340
|
+
if (!existsSync5(absPath)) {
|
|
633
1341
|
throw new Error(`Manifest not found at ${absPath}.
|
|
634
1342
|
Run \`scope manifest generate\` first.`);
|
|
635
1343
|
}
|
|
636
|
-
const raw =
|
|
1344
|
+
const raw = readFileSync5(absPath, "utf-8");
|
|
637
1345
|
return JSON.parse(raw);
|
|
638
1346
|
}
|
|
639
1347
|
function resolveFormat(formatFlag) {
|
|
@@ -721,244 +1429,84 @@ function registerQuery(manifestCmd) {
|
|
|
721
1429
|
entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
|
|
722
1430
|
}
|
|
723
1431
|
if (opts.complexity !== void 0) {
|
|
724
|
-
const cls = opts.complexity;
|
|
725
|
-
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
726
|
-
}
|
|
727
|
-
if (opts.sideEffects) {
|
|
728
|
-
entries = entries.filter(([, d]) => {
|
|
729
|
-
const se = d.sideEffects;
|
|
730
|
-
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
if (opts.hasFetch) {
|
|
734
|
-
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
735
|
-
}
|
|
736
|
-
const rows = entries.map(([name, d]) => ({
|
|
737
|
-
name,
|
|
738
|
-
file: d.filePath,
|
|
739
|
-
complexityClass: d.complexityClass,
|
|
740
|
-
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
741
|
-
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
742
|
-
}));
|
|
743
|
-
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
744
|
-
process.stdout.write(`${output}
|
|
745
|
-
`);
|
|
746
|
-
} catch (err) {
|
|
747
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
748
|
-
`);
|
|
749
|
-
process.exit(1);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
function registerGenerate(manifestCmd) {
|
|
755
|
-
manifestCmd.command("generate").description(
|
|
756
|
-
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
757
|
-
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
758
|
-
try {
|
|
759
|
-
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
760
|
-
const outputPath = resolve(process.cwd(), opts.output);
|
|
761
|
-
const include = opts.include?.split(",").map((s) => s.trim());
|
|
762
|
-
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
763
|
-
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
764
|
-
`);
|
|
765
|
-
const manifest = await generateManifest({
|
|
766
|
-
rootDir,
|
|
767
|
-
...include !== void 0 && { include },
|
|
768
|
-
...exclude !== void 0 && { exclude }
|
|
769
|
-
});
|
|
770
|
-
const componentCount = Object.keys(manifest.components).length;
|
|
771
|
-
process.stderr.write(`Found ${componentCount} components.
|
|
772
|
-
`);
|
|
773
|
-
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
774
|
-
if (!existsSync3(outputDir)) {
|
|
775
|
-
mkdirSync2(outputDir, { recursive: true });
|
|
776
|
-
}
|
|
777
|
-
writeFileSync3(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
778
|
-
process.stderr.write(`Manifest written to ${outputPath}
|
|
779
|
-
`);
|
|
780
|
-
process.stdout.write(`${outputPath}
|
|
781
|
-
`);
|
|
782
|
-
} catch (err) {
|
|
783
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
784
|
-
`);
|
|
785
|
-
process.exit(1);
|
|
786
|
-
}
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
function createManifestCommand() {
|
|
790
|
-
const manifestCmd = new Command2("manifest").description(
|
|
791
|
-
"Query and explore the component manifest"
|
|
792
|
-
);
|
|
793
|
-
registerList(manifestCmd);
|
|
794
|
-
registerGet(manifestCmd);
|
|
795
|
-
registerQuery(manifestCmd);
|
|
796
|
-
registerGenerate(manifestCmd);
|
|
797
|
-
return manifestCmd;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// src/render-formatter.ts
|
|
801
|
-
function parseViewport(spec) {
|
|
802
|
-
const lower = spec.toLowerCase();
|
|
803
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
804
|
-
if (!match) {
|
|
805
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
806
|
-
}
|
|
807
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
808
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
809
|
-
if (width <= 0 || height <= 0) {
|
|
810
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
811
|
-
}
|
|
812
|
-
return { width, height };
|
|
813
|
-
}
|
|
814
|
-
function formatRenderJson(componentName, props, result) {
|
|
815
|
-
return {
|
|
816
|
-
component: componentName,
|
|
817
|
-
props,
|
|
818
|
-
width: result.width,
|
|
819
|
-
height: result.height,
|
|
820
|
-
renderTimeMs: result.renderTimeMs,
|
|
821
|
-
computedStyles: result.computedStyles,
|
|
822
|
-
screenshot: result.screenshot.toString("base64"),
|
|
823
|
-
dom: result.dom,
|
|
824
|
-
console: result.console,
|
|
825
|
-
accessibility: result.accessibility
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
function formatMatrixJson(result) {
|
|
829
|
-
return {
|
|
830
|
-
axes: result.axes.map((axis) => ({
|
|
831
|
-
name: axis.name,
|
|
832
|
-
values: axis.values.map((v) => String(v))
|
|
833
|
-
})),
|
|
834
|
-
stats: { ...result.stats },
|
|
835
|
-
cells: result.cells.map((cell) => ({
|
|
836
|
-
index: cell.index,
|
|
837
|
-
axisIndices: cell.axisIndices,
|
|
838
|
-
props: cell.props,
|
|
839
|
-
renderTimeMs: cell.result.renderTimeMs,
|
|
840
|
-
width: cell.result.width,
|
|
841
|
-
height: cell.result.height,
|
|
842
|
-
screenshot: cell.result.screenshot.toString("base64")
|
|
843
|
-
}))
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
function formatMatrixHtml(componentName, result) {
|
|
847
|
-
const cellsHtml = result.cells.map((cell) => {
|
|
848
|
-
const b64 = cell.result.screenshot.toString("base64");
|
|
849
|
-
const propLabel = escapeHtml(
|
|
850
|
-
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
851
|
-
);
|
|
852
|
-
return ` <div class="cell">
|
|
853
|
-
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
854
|
-
<div class="label">${propLabel}</div>
|
|
855
|
-
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
856
|
-
</div>`;
|
|
857
|
-
}).join("\n");
|
|
858
|
-
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
859
|
-
return `<!DOCTYPE html>
|
|
860
|
-
<html lang="en">
|
|
861
|
-
<head>
|
|
862
|
-
<meta charset="UTF-8" />
|
|
863
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
864
|
-
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
865
|
-
<style>
|
|
866
|
-
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
867
|
-
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
868
|
-
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
869
|
-
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
870
|
-
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
871
|
-
.cell img { display: block; }
|
|
872
|
-
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
873
|
-
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
874
|
-
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
875
|
-
</style>
|
|
876
|
-
</head>
|
|
877
|
-
<body>
|
|
878
|
-
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
879
|
-
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
880
|
-
<div class="grid">
|
|
881
|
-
${cellsHtml}
|
|
882
|
-
</div>
|
|
883
|
-
<div class="stats">
|
|
884
|
-
${result.stats.totalCells} cells \xB7
|
|
885
|
-
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
886
|
-
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
887
|
-
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
888
|
-
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
889
|
-
</div>
|
|
890
|
-
</body>
|
|
891
|
-
</html>
|
|
892
|
-
`;
|
|
893
|
-
}
|
|
894
|
-
function formatMatrixCsv(componentName, result) {
|
|
895
|
-
const axisNames = result.axes.map((a) => a.name);
|
|
896
|
-
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
897
|
-
const rows = result.cells.map((cell) => {
|
|
898
|
-
const axisVals = result.axes.map((_, i) => {
|
|
899
|
-
const axisIdx = cell.axisIndices[i];
|
|
900
|
-
const axis = result.axes[i];
|
|
901
|
-
if (axisIdx === void 0 || axis === void 0) return "";
|
|
902
|
-
const val = axis.values[axisIdx];
|
|
903
|
-
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
904
|
-
});
|
|
905
|
-
return [
|
|
906
|
-
csvEscape(componentName),
|
|
907
|
-
...axisVals,
|
|
908
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
909
|
-
String(cell.result.width),
|
|
910
|
-
String(cell.result.height)
|
|
911
|
-
].join(",");
|
|
912
|
-
});
|
|
913
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
914
|
-
`;
|
|
915
|
-
}
|
|
916
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
917
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
918
|
-
const empty = barWidth - filled;
|
|
919
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
920
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
921
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
922
|
-
}
|
|
923
|
-
function formatSummaryText(results, outputDir) {
|
|
924
|
-
const total = results.length;
|
|
925
|
-
const passed = results.filter((r) => r.success).length;
|
|
926
|
-
const failed = total - passed;
|
|
927
|
-
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
928
|
-
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
929
|
-
const lines = [
|
|
930
|
-
"\u2500".repeat(60),
|
|
931
|
-
`Render Summary`,
|
|
932
|
-
"\u2500".repeat(60),
|
|
933
|
-
` Total components : ${total}`,
|
|
934
|
-
` Passed : ${passed}`,
|
|
935
|
-
` Failed : ${failed}`,
|
|
936
|
-
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
937
|
-
` Output dir : ${outputDir}`
|
|
938
|
-
];
|
|
939
|
-
if (failed > 0) {
|
|
940
|
-
lines.push("", " Failed components:");
|
|
941
|
-
for (const r of results) {
|
|
942
|
-
if (!r.success) {
|
|
943
|
-
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
1432
|
+
const cls = opts.complexity;
|
|
1433
|
+
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
1434
|
+
}
|
|
1435
|
+
if (opts.sideEffects) {
|
|
1436
|
+
entries = entries.filter(([, d]) => {
|
|
1437
|
+
const se = d.sideEffects;
|
|
1438
|
+
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
if (opts.hasFetch) {
|
|
1442
|
+
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1443
|
+
}
|
|
1444
|
+
const rows = entries.map(([name, d]) => ({
|
|
1445
|
+
name,
|
|
1446
|
+
file: d.filePath,
|
|
1447
|
+
complexityClass: d.complexityClass,
|
|
1448
|
+
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
1449
|
+
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
1450
|
+
}));
|
|
1451
|
+
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
1452
|
+
process.stdout.write(`${output}
|
|
1453
|
+
`);
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1456
|
+
`);
|
|
1457
|
+
process.exit(1);
|
|
944
1458
|
}
|
|
945
1459
|
}
|
|
946
|
-
|
|
947
|
-
lines.push("\u2500".repeat(60));
|
|
948
|
-
return lines.join("\n");
|
|
1460
|
+
);
|
|
949
1461
|
}
|
|
950
|
-
function
|
|
951
|
-
|
|
1462
|
+
function registerGenerate(manifestCmd) {
|
|
1463
|
+
manifestCmd.command("generate").description(
|
|
1464
|
+
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
1465
|
+
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
1466
|
+
try {
|
|
1467
|
+
const rootDir = resolve3(process.cwd(), opts.root ?? ".");
|
|
1468
|
+
const outputPath = resolve3(process.cwd(), opts.output);
|
|
1469
|
+
const include = opts.include?.split(",").map((s) => s.trim());
|
|
1470
|
+
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
1471
|
+
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
1472
|
+
`);
|
|
1473
|
+
const manifest = await generateManifest2({
|
|
1474
|
+
rootDir,
|
|
1475
|
+
...include !== void 0 && { include },
|
|
1476
|
+
...exclude !== void 0 && { exclude }
|
|
1477
|
+
});
|
|
1478
|
+
const componentCount = Object.keys(manifest.components).length;
|
|
1479
|
+
process.stderr.write(`Found ${componentCount} components.
|
|
1480
|
+
`);
|
|
1481
|
+
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
1482
|
+
if (!existsSync5(outputDir)) {
|
|
1483
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
1484
|
+
}
|
|
1485
|
+
writeFileSync4(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
1486
|
+
process.stderr.write(`Manifest written to ${outputPath}
|
|
1487
|
+
`);
|
|
1488
|
+
process.stdout.write(`${outputPath}
|
|
1489
|
+
`);
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1492
|
+
`);
|
|
1493
|
+
process.exit(1);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
952
1496
|
}
|
|
953
|
-
function
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1497
|
+
function createManifestCommand() {
|
|
1498
|
+
const manifestCmd = new Command3("manifest").description(
|
|
1499
|
+
"Query and explore the component manifest"
|
|
1500
|
+
);
|
|
1501
|
+
registerList(manifestCmd);
|
|
1502
|
+
registerGet(manifestCmd);
|
|
1503
|
+
registerQuery(manifestCmd);
|
|
1504
|
+
registerGenerate(manifestCmd);
|
|
1505
|
+
return manifestCmd;
|
|
958
1506
|
}
|
|
959
1507
|
|
|
960
1508
|
// src/instrument/hooks.ts
|
|
961
|
-
import { resolve as
|
|
1509
|
+
import { resolve as resolve4 } from "path";
|
|
962
1510
|
import { Command as Cmd } from "commander";
|
|
963
1511
|
import { chromium as chromium2 } from "playwright";
|
|
964
1512
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
@@ -1281,7 +1829,7 @@ Available: ${available}`
|
|
|
1281
1829
|
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1282
1830
|
}
|
|
1283
1831
|
const rootDir = process.cwd();
|
|
1284
|
-
const filePath =
|
|
1832
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
1285
1833
|
process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
|
|
1286
1834
|
`);
|
|
1287
1835
|
const result = await runHooksProfiling(componentName, filePath, props);
|
|
@@ -1309,7 +1857,7 @@ Available: ${available}`
|
|
|
1309
1857
|
}
|
|
1310
1858
|
|
|
1311
1859
|
// src/instrument/profile.ts
|
|
1312
|
-
import { resolve as
|
|
1860
|
+
import { resolve as resolve5 } from "path";
|
|
1313
1861
|
import { Command as Cmd2 } from "commander";
|
|
1314
1862
|
import { chromium as chromium3 } from "playwright";
|
|
1315
1863
|
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
@@ -1575,7 +2123,7 @@ Available: ${available}`
|
|
|
1575
2123
|
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
1576
2124
|
}
|
|
1577
2125
|
const rootDir = process.cwd();
|
|
1578
|
-
const filePath =
|
|
2126
|
+
const filePath = resolve5(rootDir, descriptor.filePath);
|
|
1579
2127
|
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
1580
2128
|
`);
|
|
1581
2129
|
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
@@ -1603,29 +2151,29 @@ Available: ${available}`
|
|
|
1603
2151
|
}
|
|
1604
2152
|
|
|
1605
2153
|
// src/instrument/tree.ts
|
|
1606
|
-
import { resolve as
|
|
2154
|
+
import { resolve as resolve6 } from "path";
|
|
1607
2155
|
import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
|
|
1608
|
-
import { BrowserPool } from "@agent-scope/render";
|
|
1609
|
-
import { Command as
|
|
2156
|
+
import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
|
|
2157
|
+
import { Command as Command4 } from "commander";
|
|
1610
2158
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1611
2159
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1612
2160
|
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1613
|
-
var
|
|
1614
|
-
async function
|
|
1615
|
-
if (
|
|
1616
|
-
|
|
2161
|
+
var _pool2 = null;
|
|
2162
|
+
async function getPool2() {
|
|
2163
|
+
if (_pool2 === null) {
|
|
2164
|
+
_pool2 = new BrowserPool2({
|
|
1617
2165
|
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1618
2166
|
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1619
2167
|
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1620
2168
|
});
|
|
1621
|
-
await
|
|
2169
|
+
await _pool2.init();
|
|
1622
2170
|
}
|
|
1623
|
-
return
|
|
2171
|
+
return _pool2;
|
|
1624
2172
|
}
|
|
1625
|
-
async function
|
|
1626
|
-
if (
|
|
1627
|
-
await
|
|
1628
|
-
|
|
2173
|
+
async function shutdownPool2() {
|
|
2174
|
+
if (_pool2 !== null) {
|
|
2175
|
+
await _pool2.close();
|
|
2176
|
+
_pool2 = null;
|
|
1629
2177
|
}
|
|
1630
2178
|
}
|
|
1631
2179
|
function mapNodeType(node) {
|
|
@@ -1839,7 +2387,7 @@ function formatInstrumentTree(root, showProviderDepth = false) {
|
|
|
1839
2387
|
}
|
|
1840
2388
|
async function runInstrumentTree(options) {
|
|
1841
2389
|
const { componentName, filePath } = options;
|
|
1842
|
-
const pool = await
|
|
2390
|
+
const pool = await getPool2();
|
|
1843
2391
|
const slot = await pool.acquire();
|
|
1844
2392
|
const { page } = slot;
|
|
1845
2393
|
try {
|
|
@@ -1900,7 +2448,7 @@ async function runInstrumentTree(options) {
|
|
|
1900
2448
|
}
|
|
1901
2449
|
}
|
|
1902
2450
|
function createInstrumentTreeCommand() {
|
|
1903
|
-
return new
|
|
2451
|
+
return new Command4("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
1904
2452
|
"--wasted-renders",
|
|
1905
2453
|
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1906
2454
|
false
|
|
@@ -1924,7 +2472,7 @@ Available: ${available}`
|
|
|
1924
2472
|
}
|
|
1925
2473
|
}
|
|
1926
2474
|
const rootDir = process.cwd();
|
|
1927
|
-
const filePath =
|
|
2475
|
+
const filePath = resolve6(rootDir, descriptor.filePath);
|
|
1928
2476
|
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1929
2477
|
`);
|
|
1930
2478
|
const instrumentRoot = await runInstrumentTree({
|
|
@@ -1936,7 +2484,7 @@ Available: ${available}`
|
|
|
1936
2484
|
providerDepth: opts.providerDepth,
|
|
1937
2485
|
wastedRenders: opts.wastedRenders
|
|
1938
2486
|
});
|
|
1939
|
-
await
|
|
2487
|
+
await shutdownPool2();
|
|
1940
2488
|
const fmt = resolveFormat2(opts.format);
|
|
1941
2489
|
if (fmt === "json") {
|
|
1942
2490
|
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
@@ -1947,7 +2495,7 @@ Available: ${available}`
|
|
|
1947
2495
|
`);
|
|
1948
2496
|
}
|
|
1949
2497
|
} catch (err) {
|
|
1950
|
-
await
|
|
2498
|
+
await shutdownPool2();
|
|
1951
2499
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1952
2500
|
`);
|
|
1953
2501
|
process.exit(1);
|
|
@@ -2267,22 +2815,22 @@ async function replayInteraction2(page, steps) {
|
|
|
2267
2815
|
}
|
|
2268
2816
|
}
|
|
2269
2817
|
}
|
|
2270
|
-
var
|
|
2271
|
-
async function
|
|
2272
|
-
if (
|
|
2273
|
-
|
|
2818
|
+
var _pool3 = null;
|
|
2819
|
+
async function getPool3() {
|
|
2820
|
+
if (_pool3 === null) {
|
|
2821
|
+
_pool3 = new BrowserPool3({
|
|
2274
2822
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
2275
2823
|
viewportWidth: 1280,
|
|
2276
2824
|
viewportHeight: 800
|
|
2277
2825
|
});
|
|
2278
|
-
await
|
|
2826
|
+
await _pool3.init();
|
|
2279
2827
|
}
|
|
2280
|
-
return
|
|
2828
|
+
return _pool3;
|
|
2281
2829
|
}
|
|
2282
|
-
async function
|
|
2283
|
-
if (
|
|
2284
|
-
await
|
|
2285
|
-
|
|
2830
|
+
async function shutdownPool3() {
|
|
2831
|
+
if (_pool3 !== null) {
|
|
2832
|
+
await _pool3.close();
|
|
2833
|
+
_pool3 = null;
|
|
2286
2834
|
}
|
|
2287
2835
|
}
|
|
2288
2836
|
async function analyzeRenders(options) {
|
|
@@ -2297,9 +2845,9 @@ Available: ${available}`
|
|
|
2297
2845
|
);
|
|
2298
2846
|
}
|
|
2299
2847
|
const rootDir = process.cwd();
|
|
2300
|
-
const filePath =
|
|
2848
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2301
2849
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
2302
|
-
const pool = await
|
|
2850
|
+
const pool = await getPool3();
|
|
2303
2851
|
const slot = await pool.acquire();
|
|
2304
2852
|
const { page } = slot;
|
|
2305
2853
|
const startMs = performance.now();
|
|
@@ -2380,7 +2928,7 @@ function formatRendersTable(result) {
|
|
|
2380
2928
|
return lines.join("\n");
|
|
2381
2929
|
}
|
|
2382
2930
|
function createInstrumentRendersCommand() {
|
|
2383
|
-
return new
|
|
2931
|
+
return new Command5("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
2384
2932
|
"--interaction <json>",
|
|
2385
2933
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
2386
2934
|
"[]"
|
|
@@ -2407,7 +2955,7 @@ function createInstrumentRendersCommand() {
|
|
|
2407
2955
|
interaction,
|
|
2408
2956
|
manifestPath: opts.manifest
|
|
2409
2957
|
});
|
|
2410
|
-
await
|
|
2958
|
+
await shutdownPool3();
|
|
2411
2959
|
if (opts.json || !isTTY()) {
|
|
2412
2960
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2413
2961
|
`);
|
|
@@ -2416,7 +2964,7 @@ function createInstrumentRendersCommand() {
|
|
|
2416
2964
|
`);
|
|
2417
2965
|
}
|
|
2418
2966
|
} catch (err) {
|
|
2419
|
-
await
|
|
2967
|
+
await shutdownPool3();
|
|
2420
2968
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2421
2969
|
`);
|
|
2422
2970
|
process.exit(1);
|
|
@@ -2425,7 +2973,7 @@ function createInstrumentRendersCommand() {
|
|
|
2425
2973
|
);
|
|
2426
2974
|
}
|
|
2427
2975
|
function createInstrumentCommand() {
|
|
2428
|
-
const instrumentCmd = new
|
|
2976
|
+
const instrumentCmd = new Command5("instrument").description(
|
|
2429
2977
|
"Structured instrumentation commands for React component analysis"
|
|
2430
2978
|
);
|
|
2431
2979
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
@@ -2436,159 +2984,37 @@ function createInstrumentCommand() {
|
|
|
2436
2984
|
}
|
|
2437
2985
|
|
|
2438
2986
|
// src/render-commands.ts
|
|
2439
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
2440
|
-
import { resolve as
|
|
2987
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2988
|
+
import { resolve as resolve8 } from "path";
|
|
2441
2989
|
import {
|
|
2442
2990
|
ALL_CONTEXT_IDS,
|
|
2443
2991
|
ALL_STRESS_IDS,
|
|
2444
|
-
BrowserPool as
|
|
2992
|
+
BrowserPool as BrowserPool4,
|
|
2445
2993
|
contextAxis,
|
|
2446
2994
|
RenderMatrix,
|
|
2447
2995
|
SatoriRenderer,
|
|
2448
|
-
safeRender,
|
|
2996
|
+
safeRender as safeRender2,
|
|
2449
2997
|
stressAxis
|
|
2450
2998
|
} from "@agent-scope/render";
|
|
2451
|
-
import { Command as
|
|
2452
|
-
|
|
2453
|
-
// src/tailwind-css.ts
|
|
2454
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
2455
|
-
import { createRequire } from "module";
|
|
2456
|
-
import { resolve as resolve6 } from "path";
|
|
2457
|
-
var CONFIG_FILENAMES = [
|
|
2458
|
-
".reactscope/config.json",
|
|
2459
|
-
".reactscope/config.js",
|
|
2460
|
-
".reactscope/config.mjs"
|
|
2461
|
-
];
|
|
2462
|
-
var STYLE_ENTRY_CANDIDATES = [
|
|
2463
|
-
"src/index.css",
|
|
2464
|
-
"src/globals.css",
|
|
2465
|
-
"app/globals.css",
|
|
2466
|
-
"app/index.css",
|
|
2467
|
-
"styles/index.css",
|
|
2468
|
-
"index.css"
|
|
2469
|
-
];
|
|
2470
|
-
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2471
|
-
var compilerCache = null;
|
|
2472
|
-
function getCachedBuild(cwd) {
|
|
2473
|
-
if (compilerCache !== null && resolve6(compilerCache.cwd) === resolve6(cwd)) {
|
|
2474
|
-
return compilerCache.build;
|
|
2475
|
-
}
|
|
2476
|
-
return null;
|
|
2477
|
-
}
|
|
2478
|
-
function findStylesEntry(cwd) {
|
|
2479
|
-
for (const name of CONFIG_FILENAMES) {
|
|
2480
|
-
const p = resolve6(cwd, name);
|
|
2481
|
-
if (!existsSync4(p)) continue;
|
|
2482
|
-
try {
|
|
2483
|
-
if (name.endsWith(".json")) {
|
|
2484
|
-
const raw = readFileSync4(p, "utf-8");
|
|
2485
|
-
const data = JSON.parse(raw);
|
|
2486
|
-
const scope = data.scope;
|
|
2487
|
-
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2488
|
-
if (typeof entry === "string") {
|
|
2489
|
-
const full = resolve6(cwd, entry);
|
|
2490
|
-
if (existsSync4(full)) return full;
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
} catch {
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
const pkgPath = resolve6(cwd, "package.json");
|
|
2497
|
-
if (existsSync4(pkgPath)) {
|
|
2498
|
-
try {
|
|
2499
|
-
const raw = readFileSync4(pkgPath, "utf-8");
|
|
2500
|
-
const pkg = JSON.parse(raw);
|
|
2501
|
-
const entry = pkg.scope?.stylesEntry;
|
|
2502
|
-
if (typeof entry === "string") {
|
|
2503
|
-
const full = resolve6(cwd, entry);
|
|
2504
|
-
if (existsSync4(full)) return full;
|
|
2505
|
-
}
|
|
2506
|
-
} catch {
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2510
|
-
const full = resolve6(cwd, candidate);
|
|
2511
|
-
if (existsSync4(full)) {
|
|
2512
|
-
try {
|
|
2513
|
-
const content = readFileSync4(full, "utf-8");
|
|
2514
|
-
if (TAILWIND_IMPORT.test(content)) return full;
|
|
2515
|
-
} catch {
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
return null;
|
|
2520
|
-
}
|
|
2521
|
-
async function getTailwindCompiler(cwd) {
|
|
2522
|
-
const cached = getCachedBuild(cwd);
|
|
2523
|
-
if (cached !== null) return cached;
|
|
2524
|
-
const entryPath = findStylesEntry(cwd);
|
|
2525
|
-
if (entryPath === null) return null;
|
|
2526
|
-
let compile;
|
|
2527
|
-
try {
|
|
2528
|
-
const require2 = createRequire(resolve6(cwd, "package.json"));
|
|
2529
|
-
const tailwind = require2("tailwindcss");
|
|
2530
|
-
const fn = tailwind.compile;
|
|
2531
|
-
if (typeof fn !== "function") return null;
|
|
2532
|
-
compile = fn;
|
|
2533
|
-
} catch {
|
|
2534
|
-
return null;
|
|
2535
|
-
}
|
|
2536
|
-
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
2537
|
-
const loadStylesheet = async (id, base) => {
|
|
2538
|
-
if (id === "tailwindcss") {
|
|
2539
|
-
const nodeModules = resolve6(cwd, "node_modules");
|
|
2540
|
-
const tailwindCssPath = resolve6(nodeModules, "tailwindcss", "index.css");
|
|
2541
|
-
if (!existsSync4(tailwindCssPath)) {
|
|
2542
|
-
throw new Error(
|
|
2543
|
-
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
2544
|
-
);
|
|
2545
|
-
}
|
|
2546
|
-
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
2547
|
-
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2548
|
-
}
|
|
2549
|
-
const full = resolve6(base, id);
|
|
2550
|
-
if (existsSync4(full)) {
|
|
2551
|
-
const content = readFileSync4(full, "utf-8");
|
|
2552
|
-
return { path: full, base: resolve6(full, ".."), content };
|
|
2553
|
-
}
|
|
2554
|
-
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2555
|
-
};
|
|
2556
|
-
const result = await compile(entryContent, {
|
|
2557
|
-
base: cwd,
|
|
2558
|
-
from: entryPath,
|
|
2559
|
-
loadStylesheet
|
|
2560
|
-
});
|
|
2561
|
-
const build2 = result.build.bind(result);
|
|
2562
|
-
compilerCache = { cwd, build: build2 };
|
|
2563
|
-
return build2;
|
|
2564
|
-
}
|
|
2565
|
-
async function getCompiledCssForClasses(cwd, classes) {
|
|
2566
|
-
const build2 = await getTailwindCompiler(cwd);
|
|
2567
|
-
if (build2 === null) return null;
|
|
2568
|
-
const deduped = [...new Set(classes)].filter(Boolean);
|
|
2569
|
-
if (deduped.length === 0) return null;
|
|
2570
|
-
return build2(deduped);
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
// src/render-commands.ts
|
|
2999
|
+
import { Command as Command6 } from "commander";
|
|
2574
3000
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2575
3001
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2576
|
-
var
|
|
2577
|
-
async function
|
|
2578
|
-
if (
|
|
2579
|
-
|
|
3002
|
+
var _pool4 = null;
|
|
3003
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
3004
|
+
if (_pool4 === null) {
|
|
3005
|
+
_pool4 = new BrowserPool4({
|
|
2580
3006
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2581
3007
|
viewportWidth,
|
|
2582
3008
|
viewportHeight
|
|
2583
3009
|
});
|
|
2584
|
-
await
|
|
3010
|
+
await _pool4.init();
|
|
2585
3011
|
}
|
|
2586
|
-
return
|
|
3012
|
+
return _pool4;
|
|
2587
3013
|
}
|
|
2588
|
-
async function
|
|
2589
|
-
if (
|
|
2590
|
-
await
|
|
2591
|
-
|
|
3014
|
+
async function shutdownPool4() {
|
|
3015
|
+
if (_pool4 !== null) {
|
|
3016
|
+
await _pool4.close();
|
|
3017
|
+
_pool4 = null;
|
|
2592
3018
|
}
|
|
2593
3019
|
}
|
|
2594
3020
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2599,7 +3025,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2599
3025
|
_satori: satori,
|
|
2600
3026
|
async renderCell(props, _complexityClass) {
|
|
2601
3027
|
const startMs = performance.now();
|
|
2602
|
-
const pool = await
|
|
3028
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2603
3029
|
const htmlHarness = await buildComponentHarness(
|
|
2604
3030
|
filePath,
|
|
2605
3031
|
componentName,
|
|
@@ -2718,13 +3144,13 @@ Available: ${available}`
|
|
|
2718
3144
|
}
|
|
2719
3145
|
const { width, height } = parseViewport(opts.viewport);
|
|
2720
3146
|
const rootDir = process.cwd();
|
|
2721
|
-
const filePath =
|
|
3147
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2722
3148
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
2723
3149
|
process.stderr.write(
|
|
2724
3150
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
2725
3151
|
`
|
|
2726
3152
|
);
|
|
2727
|
-
const outcome = await
|
|
3153
|
+
const outcome = await safeRender2(
|
|
2728
3154
|
() => renderer.renderCell(props, descriptor.complexityClass),
|
|
2729
3155
|
{
|
|
2730
3156
|
props,
|
|
@@ -2735,7 +3161,7 @@ Available: ${available}`
|
|
|
2735
3161
|
}
|
|
2736
3162
|
}
|
|
2737
3163
|
);
|
|
2738
|
-
await
|
|
3164
|
+
await shutdownPool4();
|
|
2739
3165
|
if (outcome.crashed) {
|
|
2740
3166
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2741
3167
|
`);
|
|
@@ -2748,8 +3174,8 @@ Available: ${available}`
|
|
|
2748
3174
|
}
|
|
2749
3175
|
const result = outcome.result;
|
|
2750
3176
|
if (opts.output !== void 0) {
|
|
2751
|
-
const outPath =
|
|
2752
|
-
|
|
3177
|
+
const outPath = resolve8(process.cwd(), opts.output);
|
|
3178
|
+
writeFileSync5(outPath, result.screenshot);
|
|
2753
3179
|
process.stdout.write(
|
|
2754
3180
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
2755
3181
|
`
|
|
@@ -2762,20 +3188,20 @@ Available: ${available}`
|
|
|
2762
3188
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
2763
3189
|
`);
|
|
2764
3190
|
} else if (fmt === "file") {
|
|
2765
|
-
const dir =
|
|
3191
|
+
const dir = resolve8(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2766
3192
|
mkdirSync3(dir, { recursive: true });
|
|
2767
|
-
const outPath =
|
|
2768
|
-
|
|
3193
|
+
const outPath = resolve8(dir, `${componentName}.png`);
|
|
3194
|
+
writeFileSync5(outPath, result.screenshot);
|
|
2769
3195
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
2770
3196
|
process.stdout.write(
|
|
2771
3197
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
2772
3198
|
`
|
|
2773
3199
|
);
|
|
2774
3200
|
} else {
|
|
2775
|
-
const dir =
|
|
3201
|
+
const dir = resolve8(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2776
3202
|
mkdirSync3(dir, { recursive: true });
|
|
2777
|
-
const outPath =
|
|
2778
|
-
|
|
3203
|
+
const outPath = resolve8(dir, `${componentName}.png`);
|
|
3204
|
+
writeFileSync5(outPath, result.screenshot);
|
|
2779
3205
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
2780
3206
|
process.stdout.write(
|
|
2781
3207
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -2783,7 +3209,7 @@ Available: ${available}`
|
|
|
2783
3209
|
);
|
|
2784
3210
|
}
|
|
2785
3211
|
} catch (err) {
|
|
2786
|
-
await
|
|
3212
|
+
await shutdownPool4();
|
|
2787
3213
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2788
3214
|
`);
|
|
2789
3215
|
process.exit(1);
|
|
@@ -2810,7 +3236,7 @@ Available: ${available}`
|
|
|
2810
3236
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
2811
3237
|
const { width, height } = { width: 375, height: 812 };
|
|
2812
3238
|
const rootDir = process.cwd();
|
|
2813
|
-
const filePath =
|
|
3239
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2814
3240
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
2815
3241
|
const axes = [];
|
|
2816
3242
|
if (opts.axes !== void 0) {
|
|
@@ -2868,29 +3294,29 @@ Available: ${available}`
|
|
|
2868
3294
|
concurrency
|
|
2869
3295
|
});
|
|
2870
3296
|
const result = await matrix.render();
|
|
2871
|
-
await
|
|
3297
|
+
await shutdownPool4();
|
|
2872
3298
|
process.stderr.write(
|
|
2873
3299
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2874
3300
|
`
|
|
2875
3301
|
);
|
|
2876
3302
|
if (opts.sprite !== void 0) {
|
|
2877
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2878
|
-
const gen = new
|
|
3303
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3304
|
+
const gen = new SpriteSheetGenerator2();
|
|
2879
3305
|
const sheet = await gen.generate(result);
|
|
2880
|
-
const spritePath =
|
|
2881
|
-
|
|
3306
|
+
const spritePath = resolve8(process.cwd(), opts.sprite);
|
|
3307
|
+
writeFileSync5(spritePath, sheet.png);
|
|
2882
3308
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
2883
3309
|
`);
|
|
2884
3310
|
}
|
|
2885
3311
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2886
3312
|
if (fmt === "file") {
|
|
2887
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2888
|
-
const gen = new
|
|
3313
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3314
|
+
const gen = new SpriteSheetGenerator2();
|
|
2889
3315
|
const sheet = await gen.generate(result);
|
|
2890
|
-
const dir =
|
|
3316
|
+
const dir = resolve8(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2891
3317
|
mkdirSync3(dir, { recursive: true });
|
|
2892
|
-
const outPath =
|
|
2893
|
-
|
|
3318
|
+
const outPath = resolve8(dir, `${componentName}-matrix.png`);
|
|
3319
|
+
writeFileSync5(outPath, sheet.png);
|
|
2894
3320
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
2895
3321
|
process.stdout.write(
|
|
2896
3322
|
`\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
|
|
@@ -2902,8 +3328,8 @@ Available: ${available}`
|
|
|
2902
3328
|
} else if (fmt === "png") {
|
|
2903
3329
|
if (opts.sprite !== void 0) {
|
|
2904
3330
|
} else {
|
|
2905
|
-
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2906
|
-
const gen = new
|
|
3331
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3332
|
+
const gen = new SpriteSheetGenerator2();
|
|
2907
3333
|
const sheet = await gen.generate(result);
|
|
2908
3334
|
process.stdout.write(sheet.png);
|
|
2909
3335
|
}
|
|
@@ -2913,7 +3339,7 @@ Available: ${available}`
|
|
|
2913
3339
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2914
3340
|
}
|
|
2915
3341
|
} catch (err) {
|
|
2916
|
-
await
|
|
3342
|
+
await shutdownPool4();
|
|
2917
3343
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2918
3344
|
`);
|
|
2919
3345
|
process.exit(1);
|
|
@@ -2933,7 +3359,7 @@ function registerRenderAll(renderCmd) {
|
|
|
2933
3359
|
return;
|
|
2934
3360
|
}
|
|
2935
3361
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
2936
|
-
const outputDir =
|
|
3362
|
+
const outputDir = resolve8(process.cwd(), opts.outputDir);
|
|
2937
3363
|
mkdirSync3(outputDir, { recursive: true });
|
|
2938
3364
|
const rootDir = process.cwd();
|
|
2939
3365
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
@@ -2943,9 +3369,9 @@ function registerRenderAll(renderCmd) {
|
|
|
2943
3369
|
const renderOne = async (name) => {
|
|
2944
3370
|
const descriptor = manifest.components[name];
|
|
2945
3371
|
if (descriptor === void 0) return;
|
|
2946
|
-
const filePath =
|
|
3372
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2947
3373
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
2948
|
-
const outcome = await
|
|
3374
|
+
const outcome = await safeRender2(
|
|
2949
3375
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
2950
3376
|
{
|
|
2951
3377
|
props: {},
|
|
@@ -2966,8 +3392,8 @@ function registerRenderAll(renderCmd) {
|
|
|
2966
3392
|
success: false,
|
|
2967
3393
|
errorMessage: outcome.error.message
|
|
2968
3394
|
});
|
|
2969
|
-
const errPath =
|
|
2970
|
-
|
|
3395
|
+
const errPath = resolve8(outputDir, `${name}.error.json`);
|
|
3396
|
+
writeFileSync5(
|
|
2971
3397
|
errPath,
|
|
2972
3398
|
JSON.stringify(
|
|
2973
3399
|
{
|
|
@@ -2984,10 +3410,10 @@ function registerRenderAll(renderCmd) {
|
|
|
2984
3410
|
}
|
|
2985
3411
|
const result = outcome.result;
|
|
2986
3412
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
2987
|
-
const pngPath =
|
|
2988
|
-
|
|
2989
|
-
const jsonPath =
|
|
2990
|
-
|
|
3413
|
+
const pngPath = resolve8(outputDir, `${name}.png`);
|
|
3414
|
+
writeFileSync5(pngPath, result.screenshot);
|
|
3415
|
+
const jsonPath = resolve8(outputDir, `${name}.json`);
|
|
3416
|
+
writeFileSync5(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
2991
3417
|
if (isTTY()) {
|
|
2992
3418
|
process.stdout.write(
|
|
2993
3419
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -3010,13 +3436,13 @@ function registerRenderAll(renderCmd) {
|
|
|
3010
3436
|
workers.push(worker());
|
|
3011
3437
|
}
|
|
3012
3438
|
await Promise.all(workers);
|
|
3013
|
-
await
|
|
3439
|
+
await shutdownPool4();
|
|
3014
3440
|
process.stderr.write("\n");
|
|
3015
3441
|
const summary = formatSummaryText(results, outputDir);
|
|
3016
3442
|
process.stderr.write(`${summary}
|
|
3017
3443
|
`);
|
|
3018
3444
|
} catch (err) {
|
|
3019
|
-
await
|
|
3445
|
+
await shutdownPool4();
|
|
3020
3446
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3021
3447
|
`);
|
|
3022
3448
|
process.exit(1);
|
|
@@ -3049,7 +3475,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
3049
3475
|
return "json";
|
|
3050
3476
|
}
|
|
3051
3477
|
function createRenderCommand() {
|
|
3052
|
-
const renderCmd = new
|
|
3478
|
+
const renderCmd = new Command6("render").description(
|
|
3053
3479
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
3054
3480
|
);
|
|
3055
3481
|
registerRenderSingle(renderCmd);
|
|
@@ -3059,32 +3485,32 @@ function createRenderCommand() {
|
|
|
3059
3485
|
}
|
|
3060
3486
|
|
|
3061
3487
|
// src/report/baseline.ts
|
|
3062
|
-
import { existsSync as
|
|
3063
|
-
import { resolve as
|
|
3064
|
-
import { generateManifest as
|
|
3065
|
-
import { BrowserPool as
|
|
3066
|
-
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
3488
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync6 } from "fs";
|
|
3489
|
+
import { resolve as resolve9 } from "path";
|
|
3490
|
+
import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
|
|
3491
|
+
import { BrowserPool as BrowserPool5, safeRender as safeRender3 } from "@agent-scope/render";
|
|
3492
|
+
import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
|
|
3067
3493
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
3068
|
-
var
|
|
3069
|
-
async function
|
|
3070
|
-
if (
|
|
3071
|
-
|
|
3494
|
+
var _pool5 = null;
|
|
3495
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3496
|
+
if (_pool5 === null) {
|
|
3497
|
+
_pool5 = new BrowserPool5({
|
|
3072
3498
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3073
3499
|
viewportWidth,
|
|
3074
3500
|
viewportHeight
|
|
3075
3501
|
});
|
|
3076
|
-
await
|
|
3502
|
+
await _pool5.init();
|
|
3077
3503
|
}
|
|
3078
|
-
return
|
|
3504
|
+
return _pool5;
|
|
3079
3505
|
}
|
|
3080
|
-
async function
|
|
3081
|
-
if (
|
|
3082
|
-
await
|
|
3083
|
-
|
|
3506
|
+
async function shutdownPool5() {
|
|
3507
|
+
if (_pool5 !== null) {
|
|
3508
|
+
await _pool5.close();
|
|
3509
|
+
_pool5 = null;
|
|
3084
3510
|
}
|
|
3085
3511
|
}
|
|
3086
|
-
async function
|
|
3087
|
-
const pool = await
|
|
3512
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3513
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3088
3514
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3089
3515
|
const slot = await pool.acquire();
|
|
3090
3516
|
const { page } = slot;
|
|
@@ -3174,7 +3600,7 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
|
|
|
3174
3600
|
pool.release(slot);
|
|
3175
3601
|
}
|
|
3176
3602
|
}
|
|
3177
|
-
function
|
|
3603
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3178
3604
|
const flat = {};
|
|
3179
3605
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3180
3606
|
Object.assign(flat, styles);
|
|
@@ -3209,30 +3635,30 @@ async function runBaseline(options = {}) {
|
|
|
3209
3635
|
} = options;
|
|
3210
3636
|
const startTime = performance.now();
|
|
3211
3637
|
const rootDir = process.cwd();
|
|
3212
|
-
const baselineDir =
|
|
3213
|
-
const rendersDir =
|
|
3214
|
-
if (
|
|
3638
|
+
const baselineDir = resolve9(rootDir, outputDir);
|
|
3639
|
+
const rendersDir = resolve9(baselineDir, "renders");
|
|
3640
|
+
if (existsSync6(baselineDir)) {
|
|
3215
3641
|
rmSync(baselineDir, { recursive: true, force: true });
|
|
3216
3642
|
}
|
|
3217
3643
|
mkdirSync4(rendersDir, { recursive: true });
|
|
3218
3644
|
let manifest;
|
|
3219
3645
|
if (manifestPath !== void 0) {
|
|
3220
|
-
const { readFileSync:
|
|
3221
|
-
const absPath =
|
|
3222
|
-
if (!
|
|
3646
|
+
const { readFileSync: readFileSync11 } = await import("fs");
|
|
3647
|
+
const absPath = resolve9(rootDir, manifestPath);
|
|
3648
|
+
if (!existsSync6(absPath)) {
|
|
3223
3649
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3224
3650
|
}
|
|
3225
|
-
manifest = JSON.parse(
|
|
3651
|
+
manifest = JSON.parse(readFileSync11(absPath, "utf-8"));
|
|
3226
3652
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3227
3653
|
`);
|
|
3228
3654
|
} else {
|
|
3229
3655
|
process.stderr.write("Scanning for React components\u2026\n");
|
|
3230
|
-
manifest = await
|
|
3656
|
+
manifest = await generateManifest3({ rootDir });
|
|
3231
3657
|
const count = Object.keys(manifest.components).length;
|
|
3232
3658
|
process.stderr.write(`Found ${count} components.
|
|
3233
3659
|
`);
|
|
3234
3660
|
}
|
|
3235
|
-
|
|
3661
|
+
writeFileSync6(resolve9(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
3236
3662
|
let componentNames = Object.keys(manifest.components);
|
|
3237
3663
|
if (componentsGlob !== void 0) {
|
|
3238
3664
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -3252,8 +3678,8 @@ async function runBaseline(options = {}) {
|
|
|
3252
3678
|
aggregateCompliance: 1,
|
|
3253
3679
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3254
3680
|
};
|
|
3255
|
-
|
|
3256
|
-
|
|
3681
|
+
writeFileSync6(
|
|
3682
|
+
resolve9(baselineDir, "compliance.json"),
|
|
3257
3683
|
JSON.stringify(emptyReport, null, 2),
|
|
3258
3684
|
"utf-8"
|
|
3259
3685
|
);
|
|
@@ -3274,9 +3700,9 @@ async function runBaseline(options = {}) {
|
|
|
3274
3700
|
const renderOne = async (name) => {
|
|
3275
3701
|
const descriptor = manifest.components[name];
|
|
3276
3702
|
if (descriptor === void 0) return;
|
|
3277
|
-
const filePath =
|
|
3278
|
-
const outcome = await
|
|
3279
|
-
() =>
|
|
3703
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3704
|
+
const outcome = await safeRender3(
|
|
3705
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3280
3706
|
{
|
|
3281
3707
|
props: {},
|
|
3282
3708
|
sourceLocation: {
|
|
@@ -3293,8 +3719,8 @@ async function runBaseline(options = {}) {
|
|
|
3293
3719
|
}
|
|
3294
3720
|
if (outcome.crashed) {
|
|
3295
3721
|
failureCount++;
|
|
3296
|
-
const errPath =
|
|
3297
|
-
|
|
3722
|
+
const errPath = resolve9(rendersDir, `${name}.error.json`);
|
|
3723
|
+
writeFileSync6(
|
|
3298
3724
|
errPath,
|
|
3299
3725
|
JSON.stringify(
|
|
3300
3726
|
{
|
|
@@ -3311,14 +3737,14 @@ async function runBaseline(options = {}) {
|
|
|
3311
3737
|
return;
|
|
3312
3738
|
}
|
|
3313
3739
|
const result = outcome.result;
|
|
3314
|
-
|
|
3740
|
+
writeFileSync6(resolve9(rendersDir, `${name}.png`), result.screenshot);
|
|
3315
3741
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
3316
|
-
|
|
3317
|
-
|
|
3742
|
+
writeFileSync6(
|
|
3743
|
+
resolve9(rendersDir, `${name}.json`),
|
|
3318
3744
|
JSON.stringify(jsonOutput, null, 2),
|
|
3319
3745
|
"utf-8"
|
|
3320
3746
|
);
|
|
3321
|
-
computedStylesMap.set(name,
|
|
3747
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3322
3748
|
};
|
|
3323
3749
|
const worker = async () => {
|
|
3324
3750
|
while (nextIdx < componentNames.length) {
|
|
@@ -3334,15 +3760,15 @@ async function runBaseline(options = {}) {
|
|
|
3334
3760
|
workers.push(worker());
|
|
3335
3761
|
}
|
|
3336
3762
|
await Promise.all(workers);
|
|
3337
|
-
await
|
|
3763
|
+
await shutdownPool5();
|
|
3338
3764
|
if (isTTY()) {
|
|
3339
3765
|
process.stderr.write("\n");
|
|
3340
3766
|
}
|
|
3341
|
-
const resolver = new
|
|
3342
|
-
const engine = new
|
|
3767
|
+
const resolver = new TokenResolver2([]);
|
|
3768
|
+
const engine = new ComplianceEngine2(resolver);
|
|
3343
3769
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
3344
|
-
|
|
3345
|
-
|
|
3770
|
+
writeFileSync6(
|
|
3771
|
+
resolve9(baselineDir, "compliance.json"),
|
|
3346
3772
|
JSON.stringify(batchReport, null, 2),
|
|
3347
3773
|
"utf-8"
|
|
3348
3774
|
);
|
|
@@ -3385,43 +3811,43 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3385
3811
|
}
|
|
3386
3812
|
|
|
3387
3813
|
// src/report/diff.ts
|
|
3388
|
-
import { existsSync as
|
|
3389
|
-
import { resolve as
|
|
3390
|
-
import { generateManifest as
|
|
3391
|
-
import { BrowserPool as
|
|
3392
|
-
import { ComplianceEngine as
|
|
3814
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
3815
|
+
import { resolve as resolve10 } from "path";
|
|
3816
|
+
import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
|
|
3817
|
+
import { BrowserPool as BrowserPool6, safeRender as safeRender4 } from "@agent-scope/render";
|
|
3818
|
+
import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3 } from "@agent-scope/tokens";
|
|
3393
3819
|
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3394
3820
|
function loadBaselineCompliance(baselineDir) {
|
|
3395
|
-
const compliancePath =
|
|
3396
|
-
if (!
|
|
3397
|
-
const raw = JSON.parse(
|
|
3821
|
+
const compliancePath = resolve10(baselineDir, "compliance.json");
|
|
3822
|
+
if (!existsSync7(compliancePath)) return null;
|
|
3823
|
+
const raw = JSON.parse(readFileSync6(compliancePath, "utf-8"));
|
|
3398
3824
|
return raw;
|
|
3399
3825
|
}
|
|
3400
|
-
function
|
|
3401
|
-
const jsonPath =
|
|
3402
|
-
if (!
|
|
3403
|
-
return JSON.parse(
|
|
3826
|
+
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3827
|
+
const jsonPath = resolve10(baselineDir, "renders", `${componentName}.json`);
|
|
3828
|
+
if (!existsSync7(jsonPath)) return null;
|
|
3829
|
+
return JSON.parse(readFileSync6(jsonPath, "utf-8"));
|
|
3404
3830
|
}
|
|
3405
|
-
var
|
|
3406
|
-
async function
|
|
3407
|
-
if (
|
|
3408
|
-
|
|
3831
|
+
var _pool6 = null;
|
|
3832
|
+
async function getPool6(viewportWidth, viewportHeight) {
|
|
3833
|
+
if (_pool6 === null) {
|
|
3834
|
+
_pool6 = new BrowserPool6({
|
|
3409
3835
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3410
3836
|
viewportWidth,
|
|
3411
3837
|
viewportHeight
|
|
3412
3838
|
});
|
|
3413
|
-
await
|
|
3839
|
+
await _pool6.init();
|
|
3414
3840
|
}
|
|
3415
|
-
return
|
|
3841
|
+
return _pool6;
|
|
3416
3842
|
}
|
|
3417
|
-
async function
|
|
3418
|
-
if (
|
|
3419
|
-
await
|
|
3420
|
-
|
|
3843
|
+
async function shutdownPool6() {
|
|
3844
|
+
if (_pool6 !== null) {
|
|
3845
|
+
await _pool6.close();
|
|
3846
|
+
_pool6 = null;
|
|
3421
3847
|
}
|
|
3422
3848
|
}
|
|
3423
|
-
async function
|
|
3424
|
-
const pool = await
|
|
3849
|
+
async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3850
|
+
const pool = await getPool6(viewportWidth, viewportHeight);
|
|
3425
3851
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3426
3852
|
const slot = await pool.acquire();
|
|
3427
3853
|
const { page } = slot;
|
|
@@ -3511,7 +3937,7 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
|
|
|
3511
3937
|
pool.release(slot);
|
|
3512
3938
|
}
|
|
3513
3939
|
}
|
|
3514
|
-
function
|
|
3940
|
+
function extractComputedStyles3(computedStylesRaw) {
|
|
3515
3941
|
const flat = {};
|
|
3516
3942
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3517
3943
|
Object.assign(flat, styles);
|
|
@@ -3567,19 +3993,19 @@ async function runDiff(options = {}) {
|
|
|
3567
3993
|
} = options;
|
|
3568
3994
|
const startTime = performance.now();
|
|
3569
3995
|
const rootDir = process.cwd();
|
|
3570
|
-
const baselineDir =
|
|
3571
|
-
if (!
|
|
3996
|
+
const baselineDir = resolve10(rootDir, baselineDirRaw);
|
|
3997
|
+
if (!existsSync7(baselineDir)) {
|
|
3572
3998
|
throw new Error(
|
|
3573
3999
|
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3574
4000
|
);
|
|
3575
4001
|
}
|
|
3576
|
-
const baselineManifestPath =
|
|
3577
|
-
if (!
|
|
4002
|
+
const baselineManifestPath = resolve10(baselineDir, "manifest.json");
|
|
4003
|
+
if (!existsSync7(baselineManifestPath)) {
|
|
3578
4004
|
throw new Error(
|
|
3579
4005
|
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3580
4006
|
);
|
|
3581
4007
|
}
|
|
3582
|
-
const baselineManifest = JSON.parse(
|
|
4008
|
+
const baselineManifest = JSON.parse(readFileSync6(baselineManifestPath, "utf-8"));
|
|
3583
4009
|
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3584
4010
|
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3585
4011
|
process.stderr.write(
|
|
@@ -3588,16 +4014,16 @@ async function runDiff(options = {}) {
|
|
|
3588
4014
|
);
|
|
3589
4015
|
let currentManifest;
|
|
3590
4016
|
if (manifestPath !== void 0) {
|
|
3591
|
-
const absPath =
|
|
3592
|
-
if (!
|
|
4017
|
+
const absPath = resolve10(rootDir, manifestPath);
|
|
4018
|
+
if (!existsSync7(absPath)) {
|
|
3593
4019
|
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3594
4020
|
}
|
|
3595
|
-
currentManifest = JSON.parse(
|
|
4021
|
+
currentManifest = JSON.parse(readFileSync6(absPath, "utf-8"));
|
|
3596
4022
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3597
4023
|
`);
|
|
3598
4024
|
} else {
|
|
3599
4025
|
process.stderr.write("Scanning for React components\u2026\n");
|
|
3600
|
-
currentManifest = await
|
|
4026
|
+
currentManifest = await generateManifest4({ rootDir });
|
|
3601
4027
|
const count = Object.keys(currentManifest.components).length;
|
|
3602
4028
|
process.stderr.write(`Found ${count} components.
|
|
3603
4029
|
`);
|
|
@@ -3625,9 +4051,9 @@ async function runDiff(options = {}) {
|
|
|
3625
4051
|
const renderOne = async (name) => {
|
|
3626
4052
|
const descriptor = currentManifest.components[name];
|
|
3627
4053
|
if (descriptor === void 0) return;
|
|
3628
|
-
const filePath =
|
|
3629
|
-
const outcome = await
|
|
3630
|
-
() =>
|
|
4054
|
+
const filePath = resolve10(rootDir, descriptor.filePath);
|
|
4055
|
+
const outcome = await safeRender4(
|
|
4056
|
+
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3631
4057
|
{
|
|
3632
4058
|
props: {},
|
|
3633
4059
|
sourceLocation: {
|
|
@@ -3652,7 +4078,7 @@ async function runDiff(options = {}) {
|
|
|
3652
4078
|
height: result.height,
|
|
3653
4079
|
renderTimeMs: result.renderTimeMs
|
|
3654
4080
|
});
|
|
3655
|
-
computedStylesMap.set(name,
|
|
4081
|
+
computedStylesMap.set(name, extractComputedStyles3(result.computedStyles));
|
|
3656
4082
|
};
|
|
3657
4083
|
if (total > 0) {
|
|
3658
4084
|
const worker = async () => {
|
|
@@ -3670,18 +4096,18 @@ async function runDiff(options = {}) {
|
|
|
3670
4096
|
}
|
|
3671
4097
|
await Promise.all(workers);
|
|
3672
4098
|
}
|
|
3673
|
-
await
|
|
4099
|
+
await shutdownPool6();
|
|
3674
4100
|
if (isTTY() && total > 0) {
|
|
3675
4101
|
process.stderr.write("\n");
|
|
3676
4102
|
}
|
|
3677
|
-
const resolver = new
|
|
3678
|
-
const engine = new
|
|
4103
|
+
const resolver = new TokenResolver3([]);
|
|
4104
|
+
const engine = new ComplianceEngine3(resolver);
|
|
3679
4105
|
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3680
4106
|
const entries = [];
|
|
3681
4107
|
for (const name of componentNames) {
|
|
3682
4108
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3683
4109
|
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3684
|
-
const baselineMeta =
|
|
4110
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3685
4111
|
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3686
4112
|
const failed = renderFailures.has(name);
|
|
3687
4113
|
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
@@ -3701,7 +4127,7 @@ async function runDiff(options = {}) {
|
|
|
3701
4127
|
}
|
|
3702
4128
|
for (const name of removedNames) {
|
|
3703
4129
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3704
|
-
const baselineMeta =
|
|
4130
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3705
4131
|
entries.push({
|
|
3706
4132
|
name,
|
|
3707
4133
|
status: "removed",
|
|
@@ -3843,7 +4269,7 @@ function registerDiffSubCommand(reportCmd) {
|
|
|
3843
4269
|
regressionThreshold
|
|
3844
4270
|
});
|
|
3845
4271
|
if (opts.output !== void 0) {
|
|
3846
|
-
|
|
4272
|
+
writeFileSync7(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3847
4273
|
process.stderr.write(`Diff written to ${opts.output}
|
|
3848
4274
|
`);
|
|
3849
4275
|
}
|
|
@@ -4144,50 +4570,226 @@ function buildStructuredReport(report) {
|
|
|
4144
4570
|
}
|
|
4145
4571
|
|
|
4146
4572
|
// src/tokens/commands.ts
|
|
4147
|
-
import { existsSync as
|
|
4148
|
-
import { resolve as
|
|
4573
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
4574
|
+
import { resolve as resolve14 } from "path";
|
|
4149
4575
|
import {
|
|
4150
4576
|
parseTokenFileSync as parseTokenFileSync2,
|
|
4151
4577
|
TokenParseError,
|
|
4152
|
-
TokenResolver as
|
|
4578
|
+
TokenResolver as TokenResolver8,
|
|
4153
4579
|
TokenValidationError,
|
|
4154
4580
|
validateTokenFile
|
|
4155
4581
|
} from "@agent-scope/tokens";
|
|
4156
|
-
import { Command as
|
|
4582
|
+
import { Command as Command8 } from "commander";
|
|
4583
|
+
|
|
4584
|
+
// src/tokens/compliance.ts
|
|
4585
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4586
|
+
import { resolve as resolve11 } from "path";
|
|
4587
|
+
import {
|
|
4588
|
+
ComplianceEngine as ComplianceEngine4,
|
|
4589
|
+
TokenResolver as TokenResolver4
|
|
4590
|
+
} from "@agent-scope/tokens";
|
|
4591
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4592
|
+
function loadStylesFile(stylesPath) {
|
|
4593
|
+
const absPath = resolve11(process.cwd(), stylesPath);
|
|
4594
|
+
if (!existsSync8(absPath)) {
|
|
4595
|
+
throw new Error(
|
|
4596
|
+
`Compliance styles file not found at ${absPath}.
|
|
4597
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4598
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4601
|
+
const raw = readFileSync7(absPath, "utf-8");
|
|
4602
|
+
let parsed;
|
|
4603
|
+
try {
|
|
4604
|
+
parsed = JSON.parse(raw);
|
|
4605
|
+
} catch (err) {
|
|
4606
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4607
|
+
}
|
|
4608
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4609
|
+
throw new Error(
|
|
4610
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
return parsed;
|
|
4614
|
+
}
|
|
4615
|
+
function categoryForProperty(property) {
|
|
4616
|
+
const lower = property.toLowerCase();
|
|
4617
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4618
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4619
|
+
return "color";
|
|
4620
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4621
|
+
return "spacing";
|
|
4622
|
+
if (lower.includes("border")) return "border";
|
|
4623
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4624
|
+
return "typography";
|
|
4625
|
+
return "spacing";
|
|
4626
|
+
}
|
|
4627
|
+
function buildCategorySummary(batch) {
|
|
4628
|
+
const cats = {
|
|
4629
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4630
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4631
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4632
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4633
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4634
|
+
};
|
|
4635
|
+
for (const report of Object.values(batch.components)) {
|
|
4636
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4637
|
+
const cat = categoryForProperty(property);
|
|
4638
|
+
const summary = cats[cat];
|
|
4639
|
+
if (summary === void 0) continue;
|
|
4640
|
+
summary.total++;
|
|
4641
|
+
if (result.status === "on_system") {
|
|
4642
|
+
summary.onSystem++;
|
|
4643
|
+
} else {
|
|
4644
|
+
summary.offSystem++;
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
for (const summary of Object.values(cats)) {
|
|
4649
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4650
|
+
}
|
|
4651
|
+
return cats;
|
|
4652
|
+
}
|
|
4653
|
+
function collectOffenders(batch, limit = 10) {
|
|
4654
|
+
const offenders = [];
|
|
4655
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4656
|
+
name,
|
|
4657
|
+
report,
|
|
4658
|
+
offSystemCount: report.offSystem
|
|
4659
|
+
}));
|
|
4660
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4661
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4662
|
+
if (offSystemCount === 0) continue;
|
|
4663
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4664
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4665
|
+
offenders.push({
|
|
4666
|
+
component: name,
|
|
4667
|
+
property,
|
|
4668
|
+
value: result.value,
|
|
4669
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4670
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4671
|
+
offSystemCount
|
|
4672
|
+
});
|
|
4673
|
+
if (offenders.length >= limit) break;
|
|
4674
|
+
}
|
|
4675
|
+
if (offenders.length >= limit) break;
|
|
4676
|
+
}
|
|
4677
|
+
return offenders;
|
|
4678
|
+
}
|
|
4679
|
+
function formatPct(n) {
|
|
4680
|
+
return `${Math.round(n * 100)}%`;
|
|
4681
|
+
}
|
|
4682
|
+
function truncate(s, max) {
|
|
4683
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4684
|
+
}
|
|
4685
|
+
function formatComplianceReport(batch, threshold) {
|
|
4686
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4687
|
+
const lines = [];
|
|
4688
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4689
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4690
|
+
lines.push("");
|
|
4691
|
+
const cats = buildCategorySummary(batch);
|
|
4692
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4693
|
+
if (catEntries.length > 0) {
|
|
4694
|
+
lines.push("By category:");
|
|
4695
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4696
|
+
for (const [cat, summary] of catEntries) {
|
|
4697
|
+
const label = cat.padEnd(catWidth);
|
|
4698
|
+
lines.push(
|
|
4699
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
lines.push("");
|
|
4703
|
+
}
|
|
4704
|
+
const offenders = collectOffenders(batch);
|
|
4705
|
+
if (offenders.length > 0) {
|
|
4706
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4707
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4708
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4709
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4710
|
+
for (const offender of offenders) {
|
|
4711
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4712
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4713
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4714
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4715
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4716
|
+
}
|
|
4717
|
+
} else {
|
|
4718
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4719
|
+
}
|
|
4720
|
+
return lines.join("\n");
|
|
4721
|
+
}
|
|
4722
|
+
function registerCompliance(tokensCmd) {
|
|
4723
|
+
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) => {
|
|
4724
|
+
try {
|
|
4725
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4726
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4727
|
+
const resolver = new TokenResolver4(tokens);
|
|
4728
|
+
const engine = new ComplianceEngine4(resolver);
|
|
4729
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4730
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4731
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4732
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4733
|
+
componentMap.set(name, styles);
|
|
4734
|
+
}
|
|
4735
|
+
if (componentMap.size === 0) {
|
|
4736
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4737
|
+
`);
|
|
4738
|
+
}
|
|
4739
|
+
const batch = engine.auditBatch(componentMap);
|
|
4740
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4741
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4742
|
+
if (useJson) {
|
|
4743
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4744
|
+
`);
|
|
4745
|
+
} else {
|
|
4746
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4747
|
+
`);
|
|
4748
|
+
}
|
|
4749
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4750
|
+
process.exit(1);
|
|
4751
|
+
}
|
|
4752
|
+
} catch (err) {
|
|
4753
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4754
|
+
`);
|
|
4755
|
+
process.exit(1);
|
|
4756
|
+
}
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4157
4759
|
|
|
4158
4760
|
// src/tokens/export.ts
|
|
4159
|
-
import { existsSync as
|
|
4160
|
-
import { resolve as
|
|
4761
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4762
|
+
import { resolve as resolve12 } from "path";
|
|
4161
4763
|
import {
|
|
4162
4764
|
exportTokens,
|
|
4163
4765
|
parseTokenFileSync,
|
|
4164
4766
|
ThemeResolver,
|
|
4165
|
-
TokenResolver as
|
|
4767
|
+
TokenResolver as TokenResolver5
|
|
4166
4768
|
} from "@agent-scope/tokens";
|
|
4167
|
-
import { Command as
|
|
4769
|
+
import { Command as Command7 } from "commander";
|
|
4168
4770
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
4169
4771
|
var CONFIG_FILE = "reactscope.config.json";
|
|
4170
4772
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
4171
|
-
function
|
|
4773
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
4172
4774
|
if (fileFlag !== void 0) {
|
|
4173
|
-
return
|
|
4775
|
+
return resolve12(process.cwd(), fileFlag);
|
|
4174
4776
|
}
|
|
4175
|
-
const configPath =
|
|
4176
|
-
if (
|
|
4777
|
+
const configPath = resolve12(process.cwd(), CONFIG_FILE);
|
|
4778
|
+
if (existsSync9(configPath)) {
|
|
4177
4779
|
try {
|
|
4178
|
-
const raw =
|
|
4780
|
+
const raw = readFileSync8(configPath, "utf-8");
|
|
4179
4781
|
const config = JSON.parse(raw);
|
|
4180
4782
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
4181
4783
|
const file = config.tokens.file;
|
|
4182
|
-
return
|
|
4784
|
+
return resolve12(process.cwd(), file);
|
|
4183
4785
|
}
|
|
4184
4786
|
} catch {
|
|
4185
4787
|
}
|
|
4186
4788
|
}
|
|
4187
|
-
return
|
|
4789
|
+
return resolve12(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
4188
4790
|
}
|
|
4189
4791
|
function createTokensExportCommand() {
|
|
4190
|
-
return new
|
|
4792
|
+
return new Command7("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
4191
4793
|
"--theme <name>",
|
|
4192
4794
|
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
4193
4795
|
).action(
|
|
@@ -4202,14 +4804,14 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
4202
4804
|
}
|
|
4203
4805
|
const format = opts.format;
|
|
4204
4806
|
try {
|
|
4205
|
-
const filePath =
|
|
4206
|
-
if (!
|
|
4807
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4808
|
+
if (!existsSync9(filePath)) {
|
|
4207
4809
|
throw new Error(
|
|
4208
4810
|
`Token file not found at ${filePath}.
|
|
4209
4811
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
4210
4812
|
);
|
|
4211
4813
|
}
|
|
4212
|
-
const raw =
|
|
4814
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
4213
4815
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
4214
4816
|
let themesMap;
|
|
4215
4817
|
if (opts.theme !== void 0) {
|
|
@@ -4220,7 +4822,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
4220
4822
|
Available themes: ${available}`
|
|
4221
4823
|
);
|
|
4222
4824
|
}
|
|
4223
|
-
const baseResolver = new
|
|
4825
|
+
const baseResolver = new TokenResolver5(tokens);
|
|
4224
4826
|
const themeResolver = ThemeResolver.fromTokenFile(
|
|
4225
4827
|
baseResolver,
|
|
4226
4828
|
rawFile
|
|
@@ -4248,8 +4850,8 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
4248
4850
|
themes: themesMap
|
|
4249
4851
|
});
|
|
4250
4852
|
if (opts.out !== void 0) {
|
|
4251
|
-
const outPath =
|
|
4252
|
-
|
|
4853
|
+
const outPath = resolve12(process.cwd(), opts.out);
|
|
4854
|
+
writeFileSync8(outPath, output, "utf-8");
|
|
4253
4855
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
4254
4856
|
`);
|
|
4255
4857
|
} else {
|
|
@@ -4267,6 +4869,324 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
4267
4869
|
);
|
|
4268
4870
|
}
|
|
4269
4871
|
|
|
4872
|
+
// src/tokens/impact.ts
|
|
4873
|
+
import {
|
|
4874
|
+
ComplianceEngine as ComplianceEngine5,
|
|
4875
|
+
ImpactAnalyzer,
|
|
4876
|
+
TokenResolver as TokenResolver6
|
|
4877
|
+
} from "@agent-scope/tokens";
|
|
4878
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4879
|
+
var SEVERITY_EMOJI = {
|
|
4880
|
+
none: "\u25CB",
|
|
4881
|
+
subtle: "\u25D4",
|
|
4882
|
+
moderate: "\u25D1",
|
|
4883
|
+
significant: "\u25CF"
|
|
4884
|
+
};
|
|
4885
|
+
function formatImpactReport(report) {
|
|
4886
|
+
const lines = [];
|
|
4887
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4888
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4889
|
+
if (report.components.length === 0) {
|
|
4890
|
+
lines.push("");
|
|
4891
|
+
lines.push("No components reference this token.");
|
|
4892
|
+
return lines.join("\n");
|
|
4893
|
+
}
|
|
4894
|
+
lines.push("");
|
|
4895
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4896
|
+
const propWidth = Math.max(
|
|
4897
|
+
8,
|
|
4898
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4899
|
+
);
|
|
4900
|
+
for (const comp of report.components) {
|
|
4901
|
+
for (const property of comp.affectedProperties) {
|
|
4902
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4903
|
+
const prop = property.padEnd(propWidth);
|
|
4904
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4905
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
lines.push("");
|
|
4909
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4910
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4911
|
+
lines.push(
|
|
4912
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4913
|
+
);
|
|
4914
|
+
if (report.colorDelta !== void 0) {
|
|
4915
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4916
|
+
}
|
|
4917
|
+
return lines.join("\n");
|
|
4918
|
+
}
|
|
4919
|
+
function formatImpactSummary(report) {
|
|
4920
|
+
if (report.components.length === 0) {
|
|
4921
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4922
|
+
}
|
|
4923
|
+
const parts = report.components.map(
|
|
4924
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4925
|
+
);
|
|
4926
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4927
|
+
}
|
|
4928
|
+
function registerImpact(tokensCmd) {
|
|
4929
|
+
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(
|
|
4930
|
+
(tokenPath, opts) => {
|
|
4931
|
+
try {
|
|
4932
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4933
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4934
|
+
const resolver = new TokenResolver6(tokens);
|
|
4935
|
+
const engine = new ComplianceEngine5(resolver);
|
|
4936
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4937
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4938
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4939
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4940
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4941
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4942
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4943
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4944
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4945
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4946
|
+
if (useJson) {
|
|
4947
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4948
|
+
`);
|
|
4949
|
+
} else {
|
|
4950
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4951
|
+
`);
|
|
4952
|
+
if (isTTY()) {
|
|
4953
|
+
process.stdout.write(`
|
|
4954
|
+
${formatImpactSummary(report)}
|
|
4955
|
+
`);
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
} catch (err) {
|
|
4959
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4960
|
+
`);
|
|
4961
|
+
process.exit(1);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
);
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
// src/tokens/preview.ts
|
|
4968
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
4969
|
+
import { resolve as resolve13 } from "path";
|
|
4970
|
+
import { BrowserPool as BrowserPool7, SpriteSheetGenerator } from "@agent-scope/render";
|
|
4971
|
+
import { ComplianceEngine as ComplianceEngine6, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver7 } from "@agent-scope/tokens";
|
|
4972
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4973
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4974
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4975
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4976
|
+
const htmlHarness = await buildComponentHarness(
|
|
4977
|
+
filePath,
|
|
4978
|
+
componentName,
|
|
4979
|
+
{},
|
|
4980
|
+
// no props
|
|
4981
|
+
vpWidth,
|
|
4982
|
+
cssOverride
|
|
4983
|
+
// injected as <style>
|
|
4984
|
+
);
|
|
4985
|
+
const pool = new BrowserPool7({
|
|
4986
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4987
|
+
viewportWidth: vpWidth,
|
|
4988
|
+
viewportHeight: vpHeight
|
|
4989
|
+
});
|
|
4990
|
+
await pool.init();
|
|
4991
|
+
const slot = await pool.acquire();
|
|
4992
|
+
const { page } = slot;
|
|
4993
|
+
try {
|
|
4994
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4995
|
+
await page.waitForFunction(
|
|
4996
|
+
() => {
|
|
4997
|
+
const w = window;
|
|
4998
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4999
|
+
},
|
|
5000
|
+
{ timeout: timeoutMs }
|
|
5001
|
+
);
|
|
5002
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
5003
|
+
const bb = await rootLocator.boundingBox();
|
|
5004
|
+
const PAD = 16;
|
|
5005
|
+
const MIN_W = 320;
|
|
5006
|
+
const MIN_H = 120;
|
|
5007
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
5008
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
5009
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
5010
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
5011
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
5012
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
5013
|
+
const screenshot = await page.screenshot({
|
|
5014
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
5015
|
+
type: "png"
|
|
5016
|
+
});
|
|
5017
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
5018
|
+
} finally {
|
|
5019
|
+
pool.release(slot);
|
|
5020
|
+
await pool.close().catch(() => void 0);
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
function registerPreview(tokensCmd) {
|
|
5024
|
+
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(
|
|
5025
|
+
async (tokenPath, opts) => {
|
|
5026
|
+
try {
|
|
5027
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
5028
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
5029
|
+
const resolver = new TokenResolver7(tokens);
|
|
5030
|
+
const engine = new ComplianceEngine6(resolver);
|
|
5031
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
5032
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
5033
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
5034
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
5035
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
5036
|
+
const analyzer = new ImpactAnalyzer2(resolver, complianceReports);
|
|
5037
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
5038
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
5039
|
+
if (impactReport.components.length === 0) {
|
|
5040
|
+
process.stdout.write(
|
|
5041
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
5042
|
+
`
|
|
5043
|
+
);
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
5047
|
+
process.stderr.write(
|
|
5048
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
5049
|
+
`
|
|
5050
|
+
);
|
|
5051
|
+
const manifest = loadManifest(opts.manifest);
|
|
5052
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
5053
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
5054
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
5055
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
5056
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
5057
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
5058
|
+
const renders = [];
|
|
5059
|
+
for (const componentName of affectedNames) {
|
|
5060
|
+
const descriptor = manifest.components[componentName];
|
|
5061
|
+
if (descriptor === void 0) {
|
|
5062
|
+
process.stderr.write(
|
|
5063
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
5064
|
+
`
|
|
5065
|
+
);
|
|
5066
|
+
continue;
|
|
5067
|
+
}
|
|
5068
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
5069
|
+
`);
|
|
5070
|
+
const before = await renderComponentWithCssOverride(
|
|
5071
|
+
descriptor.filePath,
|
|
5072
|
+
componentName,
|
|
5073
|
+
beforeCss,
|
|
5074
|
+
vpWidth,
|
|
5075
|
+
vpHeight,
|
|
5076
|
+
timeout
|
|
5077
|
+
);
|
|
5078
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
5079
|
+
`);
|
|
5080
|
+
const after = await renderComponentWithCssOverride(
|
|
5081
|
+
descriptor.filePath,
|
|
5082
|
+
componentName,
|
|
5083
|
+
afterCss,
|
|
5084
|
+
vpWidth,
|
|
5085
|
+
vpHeight,
|
|
5086
|
+
timeout
|
|
5087
|
+
);
|
|
5088
|
+
renders.push({ name: componentName, before, after });
|
|
5089
|
+
}
|
|
5090
|
+
if (renders.length === 0) {
|
|
5091
|
+
process.stderr.write(
|
|
5092
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
5093
|
+
);
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
5096
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
5097
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
5098
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
5099
|
+
{
|
|
5100
|
+
props: { version: "before", component: r.name },
|
|
5101
|
+
result: {
|
|
5102
|
+
screenshot: r.before.screenshot,
|
|
5103
|
+
width: cellW,
|
|
5104
|
+
height: cellH,
|
|
5105
|
+
renderTimeMs: 0,
|
|
5106
|
+
computedStyles: {}
|
|
5107
|
+
},
|
|
5108
|
+
index: colIdx * 2,
|
|
5109
|
+
axisIndices: [0, colIdx]
|
|
5110
|
+
},
|
|
5111
|
+
{
|
|
5112
|
+
props: { version: "after", component: r.name },
|
|
5113
|
+
result: {
|
|
5114
|
+
screenshot: r.after.screenshot,
|
|
5115
|
+
width: cellW,
|
|
5116
|
+
height: cellH,
|
|
5117
|
+
renderTimeMs: 0,
|
|
5118
|
+
computedStyles: {}
|
|
5119
|
+
},
|
|
5120
|
+
index: colIdx * 2 + 1,
|
|
5121
|
+
axisIndices: [1, colIdx]
|
|
5122
|
+
}
|
|
5123
|
+
]);
|
|
5124
|
+
const matrixResult = {
|
|
5125
|
+
cells,
|
|
5126
|
+
axes: [
|
|
5127
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
5128
|
+
{ name: "version", values: ["before", "after"] }
|
|
5129
|
+
],
|
|
5130
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
5131
|
+
rows: 2,
|
|
5132
|
+
cols: renders.length,
|
|
5133
|
+
stats: {
|
|
5134
|
+
totalCells: cells.length,
|
|
5135
|
+
totalRenderTimeMs: 0,
|
|
5136
|
+
avgRenderTimeMs: 0,
|
|
5137
|
+
minRenderTimeMs: 0,
|
|
5138
|
+
maxRenderTimeMs: 0,
|
|
5139
|
+
wallClockTimeMs: 0
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
const generator = new SpriteSheetGenerator({
|
|
5143
|
+
cellPadding: 8,
|
|
5144
|
+
borderWidth: 1,
|
|
5145
|
+
labelHeight: 32,
|
|
5146
|
+
labelWidth: 120
|
|
5147
|
+
});
|
|
5148
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
5149
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
5150
|
+
const outputPath = opts.output ?? resolve13(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
5151
|
+
const outputDir = resolve13(outputPath, "..");
|
|
5152
|
+
mkdirSync5(outputDir, { recursive: true });
|
|
5153
|
+
writeFileSync9(outputPath, spriteResult.png);
|
|
5154
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
5155
|
+
if (useJson) {
|
|
5156
|
+
process.stdout.write(
|
|
5157
|
+
`${JSON.stringify(
|
|
5158
|
+
{
|
|
5159
|
+
tokenPath,
|
|
5160
|
+
oldValue: currentValue,
|
|
5161
|
+
newValue: opts.newValue,
|
|
5162
|
+
outputPath,
|
|
5163
|
+
width: spriteResult.width,
|
|
5164
|
+
height: spriteResult.height,
|
|
5165
|
+
components: renders.map((r) => r.name),
|
|
5166
|
+
cells: spriteResult.coordinates.length
|
|
5167
|
+
},
|
|
5168
|
+
null,
|
|
5169
|
+
2
|
|
5170
|
+
)}
|
|
5171
|
+
`
|
|
5172
|
+
);
|
|
5173
|
+
} else {
|
|
5174
|
+
process.stdout.write(
|
|
5175
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
5176
|
+
`
|
|
5177
|
+
);
|
|
5178
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
5179
|
+
`);
|
|
5180
|
+
}
|
|
5181
|
+
} catch (err) {
|
|
5182
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
5183
|
+
`);
|
|
5184
|
+
process.exit(1);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
5188
|
+
}
|
|
5189
|
+
|
|
4270
5190
|
// src/tokens/commands.ts
|
|
4271
5191
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
4272
5192
|
var CONFIG_FILE2 = "reactscope.config.json";
|
|
@@ -4287,32 +5207,32 @@ function buildTable2(headers, rows) {
|
|
|
4287
5207
|
);
|
|
4288
5208
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
4289
5209
|
}
|
|
4290
|
-
function
|
|
5210
|
+
function resolveTokenFilePath(fileFlag) {
|
|
4291
5211
|
if (fileFlag !== void 0) {
|
|
4292
|
-
return
|
|
5212
|
+
return resolve14(process.cwd(), fileFlag);
|
|
4293
5213
|
}
|
|
4294
|
-
const configPath =
|
|
4295
|
-
if (
|
|
5214
|
+
const configPath = resolve14(process.cwd(), CONFIG_FILE2);
|
|
5215
|
+
if (existsSync10(configPath)) {
|
|
4296
5216
|
try {
|
|
4297
|
-
const raw =
|
|
5217
|
+
const raw = readFileSync9(configPath, "utf-8");
|
|
4298
5218
|
const config = JSON.parse(raw);
|
|
4299
5219
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
4300
5220
|
const file = config.tokens.file;
|
|
4301
|
-
return
|
|
5221
|
+
return resolve14(process.cwd(), file);
|
|
4302
5222
|
}
|
|
4303
5223
|
} catch {
|
|
4304
5224
|
}
|
|
4305
5225
|
}
|
|
4306
|
-
return
|
|
5226
|
+
return resolve14(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
4307
5227
|
}
|
|
4308
5228
|
function loadTokens(absPath) {
|
|
4309
|
-
if (!
|
|
5229
|
+
if (!existsSync10(absPath)) {
|
|
4310
5230
|
throw new Error(
|
|
4311
5231
|
`Token file not found at ${absPath}.
|
|
4312
5232
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
4313
5233
|
);
|
|
4314
5234
|
}
|
|
4315
|
-
const raw =
|
|
5235
|
+
const raw = readFileSync9(absPath, "utf-8");
|
|
4316
5236
|
return parseTokenFileSync2(raw);
|
|
4317
5237
|
}
|
|
4318
5238
|
function getRawValue(node, segments) {
|
|
@@ -4350,9 +5270,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
4350
5270
|
function registerGet2(tokensCmd) {
|
|
4351
5271
|
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) => {
|
|
4352
5272
|
try {
|
|
4353
|
-
const filePath =
|
|
5273
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4354
5274
|
const { tokens } = loadTokens(filePath);
|
|
4355
|
-
const resolver = new
|
|
5275
|
+
const resolver = new TokenResolver8(tokens);
|
|
4356
5276
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
4357
5277
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
4358
5278
|
if (useJson) {
|
|
@@ -4376,9 +5296,9 @@ function registerList2(tokensCmd) {
|
|
|
4376
5296
|
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(
|
|
4377
5297
|
(category, opts) => {
|
|
4378
5298
|
try {
|
|
4379
|
-
const filePath =
|
|
5299
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4380
5300
|
const { tokens } = loadTokens(filePath);
|
|
4381
|
-
const resolver = new
|
|
5301
|
+
const resolver = new TokenResolver8(tokens);
|
|
4382
5302
|
const filtered = resolver.list(opts.type, category);
|
|
4383
5303
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
4384
5304
|
if (useJson) {
|
|
@@ -4406,9 +5326,9 @@ function registerSearch(tokensCmd) {
|
|
|
4406
5326
|
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(
|
|
4407
5327
|
(value, opts) => {
|
|
4408
5328
|
try {
|
|
4409
|
-
const filePath =
|
|
5329
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4410
5330
|
const { tokens } = loadTokens(filePath);
|
|
4411
|
-
const resolver = new
|
|
5331
|
+
const resolver = new TokenResolver8(tokens);
|
|
4412
5332
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
4413
5333
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
4414
5334
|
"color",
|
|
@@ -4488,10 +5408,10 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
4488
5408
|
function registerResolve(tokensCmd) {
|
|
4489
5409
|
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) => {
|
|
4490
5410
|
try {
|
|
4491
|
-
const filePath =
|
|
5411
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4492
5412
|
const absFilePath = filePath;
|
|
4493
5413
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
4494
|
-
const resolver = new
|
|
5414
|
+
const resolver = new TokenResolver8(tokens);
|
|
4495
5415
|
resolver.resolve(tokenPath);
|
|
4496
5416
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
4497
5417
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -4525,14 +5445,14 @@ function registerValidate(tokensCmd) {
|
|
|
4525
5445
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
4526
5446
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4527
5447
|
try {
|
|
4528
|
-
const filePath =
|
|
4529
|
-
if (!
|
|
5448
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
5449
|
+
if (!existsSync10(filePath)) {
|
|
4530
5450
|
throw new Error(
|
|
4531
5451
|
`Token file not found at ${filePath}.
|
|
4532
5452
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
4533
5453
|
);
|
|
4534
5454
|
}
|
|
4535
|
-
const raw =
|
|
5455
|
+
const raw = readFileSync9(filePath, "utf-8");
|
|
4536
5456
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
4537
5457
|
const errors = [];
|
|
4538
5458
|
let parsed;
|
|
@@ -4600,7 +5520,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
4600
5520
|
}
|
|
4601
5521
|
}
|
|
4602
5522
|
function createTokensCommand() {
|
|
4603
|
-
const tokensCmd = new
|
|
5523
|
+
const tokensCmd = new Command8("tokens").description(
|
|
4604
5524
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
4605
5525
|
);
|
|
4606
5526
|
registerGet2(tokensCmd);
|
|
@@ -4609,12 +5529,15 @@ function createTokensCommand() {
|
|
|
4609
5529
|
registerResolve(tokensCmd);
|
|
4610
5530
|
registerValidate(tokensCmd);
|
|
4611
5531
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
5532
|
+
registerCompliance(tokensCmd);
|
|
5533
|
+
registerImpact(tokensCmd);
|
|
5534
|
+
registerPreview(tokensCmd);
|
|
4612
5535
|
return tokensCmd;
|
|
4613
5536
|
}
|
|
4614
5537
|
|
|
4615
5538
|
// src/program.ts
|
|
4616
5539
|
function createProgram(options = {}) {
|
|
4617
|
-
const program2 = new
|
|
5540
|
+
const program2 = new Command9("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
4618
5541
|
program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
4619
5542
|
async (url, opts) => {
|
|
4620
5543
|
try {
|
|
@@ -4687,7 +5610,7 @@ function createProgram(options = {}) {
|
|
|
4687
5610
|
}
|
|
4688
5611
|
);
|
|
4689
5612
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
4690
|
-
const raw =
|
|
5613
|
+
const raw = readFileSync10(tracePath, "utf-8");
|
|
4691
5614
|
const trace = loadTrace(raw);
|
|
4692
5615
|
const source = generateTest(trace, {
|
|
4693
5616
|
description: opts.description,
|
|
@@ -4701,6 +5624,7 @@ function createProgram(options = {}) {
|
|
|
4701
5624
|
program2.addCommand(createTokensCommand());
|
|
4702
5625
|
program2.addCommand(createInstrumentCommand());
|
|
4703
5626
|
program2.addCommand(createInitCommand());
|
|
5627
|
+
program2.addCommand(createCiCommand());
|
|
4704
5628
|
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
4705
5629
|
if (existingReportCmd !== void 0) {
|
|
4706
5630
|
registerBaselineSubCommand(existingReportCmd);
|