@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/index.js
CHANGED
|
@@ -1,16 +1,918 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, appendFileSync, readdirSync, rmSync } from 'fs';
|
|
2
|
+
import { resolve, join, dirname } from 'path';
|
|
3
|
+
import { generateManifest } from '@agent-scope/manifest';
|
|
4
|
+
import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
5
|
+
import { TokenResolver, ComplianceEngine, parseTokenFileSync, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
4
6
|
import { Command } from 'commander';
|
|
5
|
-
import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, BrowserPool, SatoriRenderer } from '@agent-scope/render';
|
|
6
7
|
import * as esbuild from 'esbuild';
|
|
7
|
-
import {
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import * as readline from 'readline';
|
|
8
10
|
import { chromium } from 'playwright';
|
|
9
11
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
|
-
import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine } from '@agent-scope/tokens';
|
|
12
12
|
|
|
13
|
-
// src/
|
|
13
|
+
// src/ci/commands.ts
|
|
14
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
15
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
16
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
17
|
+
}
|
|
18
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
19
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
20
|
+
const wrapperCode = (
|
|
21
|
+
/* ts */
|
|
22
|
+
`
|
|
23
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
24
|
+
import { createRoot } from "react-dom/client";
|
|
25
|
+
import { createElement } from "react";
|
|
26
|
+
|
|
27
|
+
(function scopeRenderHarness() {
|
|
28
|
+
var Component =
|
|
29
|
+
__scopeMod["default"] ||
|
|
30
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
31
|
+
(Object.values(__scopeMod).find(
|
|
32
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
if (!Component) {
|
|
36
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
37
|
+
"No renderable component found. Checked: default, " +
|
|
38
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
39
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
40
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
var props = ${propsJson};
|
|
46
|
+
var rootEl = document.getElementById("scope-root");
|
|
47
|
+
if (!rootEl) {
|
|
48
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
49
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
53
|
+
// Use requestAnimationFrame to let React flush the render
|
|
54
|
+
requestAnimationFrame(function() {
|
|
55
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
59
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
const result = await esbuild.build({
|
|
65
|
+
stdin: {
|
|
66
|
+
contents: wrapperCode,
|
|
67
|
+
// Resolve relative imports (within the component's dir)
|
|
68
|
+
resolveDir: dirname(filePath),
|
|
69
|
+
loader: "tsx",
|
|
70
|
+
sourcefile: "__scope_harness__.tsx"
|
|
71
|
+
},
|
|
72
|
+
bundle: true,
|
|
73
|
+
format: "iife",
|
|
74
|
+
write: false,
|
|
75
|
+
platform: "browser",
|
|
76
|
+
jsx: "automatic",
|
|
77
|
+
jsxImportSource: "react",
|
|
78
|
+
target: "es2020",
|
|
79
|
+
// Bundle everything — no externals
|
|
80
|
+
external: [],
|
|
81
|
+
define: {
|
|
82
|
+
"process.env.NODE_ENV": '"development"',
|
|
83
|
+
global: "globalThis"
|
|
84
|
+
},
|
|
85
|
+
logLevel: "silent",
|
|
86
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
87
|
+
banner: {
|
|
88
|
+
js: "/* @agent-scope/cli component harness */"
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (result.errors.length > 0) {
|
|
92
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
93
|
+
throw new Error(`esbuild failed to bundle component:
|
|
94
|
+
${msg}`);
|
|
95
|
+
}
|
|
96
|
+
const outputFile = result.outputFiles?.[0];
|
|
97
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
98
|
+
throw new Error("esbuild produced no output");
|
|
99
|
+
}
|
|
100
|
+
return outputFile.text;
|
|
101
|
+
}
|
|
102
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
103
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
104
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
105
|
+
</style>` : "";
|
|
106
|
+
return `<!DOCTYPE html>
|
|
107
|
+
<html lang="en">
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="UTF-8" />
|
|
110
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
111
|
+
<style>
|
|
112
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
113
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
114
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
115
|
+
</style>
|
|
116
|
+
${projectStyleBlock}
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
120
|
+
<script>${bundledScript}</script>
|
|
121
|
+
</body>
|
|
122
|
+
</html>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/manifest-formatter.ts
|
|
126
|
+
function isTTY() {
|
|
127
|
+
return process.stdout.isTTY === true;
|
|
128
|
+
}
|
|
129
|
+
function pad(value, width) {
|
|
130
|
+
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
131
|
+
}
|
|
132
|
+
function buildTable(headers, rows) {
|
|
133
|
+
const colWidths = headers.map(
|
|
134
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
135
|
+
);
|
|
136
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
137
|
+
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
138
|
+
const dataRows = rows.map(
|
|
139
|
+
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
140
|
+
);
|
|
141
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
142
|
+
}
|
|
143
|
+
function formatListTable(rows) {
|
|
144
|
+
if (rows.length === 0) return "No components found.";
|
|
145
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
146
|
+
const tableRows = rows.map((r) => [
|
|
147
|
+
r.name,
|
|
148
|
+
r.file,
|
|
149
|
+
r.complexityClass,
|
|
150
|
+
String(r.hookCount),
|
|
151
|
+
String(r.contextCount)
|
|
152
|
+
]);
|
|
153
|
+
return buildTable(headers, tableRows);
|
|
154
|
+
}
|
|
155
|
+
function formatListJson(rows) {
|
|
156
|
+
return JSON.stringify(rows, null, 2);
|
|
157
|
+
}
|
|
158
|
+
function formatSideEffects(se) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
161
|
+
if (se.timers) parts.push("timers");
|
|
162
|
+
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
163
|
+
if (se.globalListeners) parts.push("globalListeners");
|
|
164
|
+
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
165
|
+
}
|
|
166
|
+
function formatGetTable(name, descriptor) {
|
|
167
|
+
const propNames = Object.keys(descriptor.props);
|
|
168
|
+
const lines = [
|
|
169
|
+
`Component: ${name}`,
|
|
170
|
+
` File: ${descriptor.filePath}`,
|
|
171
|
+
` Export: ${descriptor.exportType}`,
|
|
172
|
+
` Display Name: ${descriptor.displayName}`,
|
|
173
|
+
` Complexity: ${descriptor.complexityClass}`,
|
|
174
|
+
` Memoized: ${descriptor.memoized}`,
|
|
175
|
+
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
176
|
+
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
177
|
+
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
178
|
+
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
179
|
+
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
180
|
+
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
181
|
+
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
182
|
+
"",
|
|
183
|
+
` Props (${propNames.length}):`
|
|
184
|
+
];
|
|
185
|
+
if (propNames.length === 0) {
|
|
186
|
+
lines.push(" (none)");
|
|
187
|
+
} else {
|
|
188
|
+
for (const propName of propNames) {
|
|
189
|
+
const p = descriptor.props[propName];
|
|
190
|
+
if (p === void 0) continue;
|
|
191
|
+
const req = p.required ? "required" : "optional";
|
|
192
|
+
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
193
|
+
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
194
|
+
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
function formatGetJson(name, descriptor) {
|
|
200
|
+
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
201
|
+
}
|
|
202
|
+
function formatQueryTable(rows, queryDesc) {
|
|
203
|
+
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
204
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
205
|
+
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
206
|
+
return `Query: ${queryDesc}
|
|
207
|
+
|
|
208
|
+
${buildTable(headers, tableRows)}`;
|
|
209
|
+
}
|
|
210
|
+
function formatQueryJson(rows) {
|
|
211
|
+
return JSON.stringify(rows, null, 2);
|
|
212
|
+
}
|
|
213
|
+
function matchGlob(pattern, value) {
|
|
214
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
216
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
217
|
+
return regex.test(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/render-formatter.ts
|
|
221
|
+
function parseViewport(spec) {
|
|
222
|
+
const lower = spec.toLowerCase();
|
|
223
|
+
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
224
|
+
if (!match) {
|
|
225
|
+
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
226
|
+
}
|
|
227
|
+
const width = parseInt(match[1] ?? "0", 10);
|
|
228
|
+
const height = parseInt(match[2] ?? "0", 10);
|
|
229
|
+
if (width <= 0 || height <= 0) {
|
|
230
|
+
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
231
|
+
}
|
|
232
|
+
return { width, height };
|
|
233
|
+
}
|
|
234
|
+
function formatRenderJson(componentName, props, result) {
|
|
235
|
+
return {
|
|
236
|
+
component: componentName,
|
|
237
|
+
props,
|
|
238
|
+
width: result.width,
|
|
239
|
+
height: result.height,
|
|
240
|
+
renderTimeMs: result.renderTimeMs,
|
|
241
|
+
computedStyles: result.computedStyles,
|
|
242
|
+
screenshot: result.screenshot.toString("base64"),
|
|
243
|
+
dom: result.dom,
|
|
244
|
+
console: result.console,
|
|
245
|
+
accessibility: result.accessibility
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function formatMatrixJson(result) {
|
|
249
|
+
return {
|
|
250
|
+
axes: result.axes.map((axis) => ({
|
|
251
|
+
name: axis.name,
|
|
252
|
+
values: axis.values.map((v) => String(v))
|
|
253
|
+
})),
|
|
254
|
+
stats: { ...result.stats },
|
|
255
|
+
cells: result.cells.map((cell) => ({
|
|
256
|
+
index: cell.index,
|
|
257
|
+
axisIndices: cell.axisIndices,
|
|
258
|
+
props: cell.props,
|
|
259
|
+
renderTimeMs: cell.result.renderTimeMs,
|
|
260
|
+
width: cell.result.width,
|
|
261
|
+
height: cell.result.height,
|
|
262
|
+
screenshot: cell.result.screenshot.toString("base64")
|
|
263
|
+
}))
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function formatMatrixHtml(componentName, result) {
|
|
267
|
+
const cellsHtml = result.cells.map((cell) => {
|
|
268
|
+
const b64 = cell.result.screenshot.toString("base64");
|
|
269
|
+
const propLabel = escapeHtml(
|
|
270
|
+
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
271
|
+
);
|
|
272
|
+
return ` <div class="cell">
|
|
273
|
+
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
274
|
+
<div class="label">${propLabel}</div>
|
|
275
|
+
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
276
|
+
</div>`;
|
|
277
|
+
}).join("\n");
|
|
278
|
+
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
279
|
+
return `<!DOCTYPE html>
|
|
280
|
+
<html lang="en">
|
|
281
|
+
<head>
|
|
282
|
+
<meta charset="UTF-8" />
|
|
283
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
284
|
+
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
285
|
+
<style>
|
|
286
|
+
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
287
|
+
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
288
|
+
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
289
|
+
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
290
|
+
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
291
|
+
.cell img { display: block; }
|
|
292
|
+
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
293
|
+
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
294
|
+
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
295
|
+
</style>
|
|
296
|
+
</head>
|
|
297
|
+
<body>
|
|
298
|
+
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
299
|
+
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
300
|
+
<div class="grid">
|
|
301
|
+
${cellsHtml}
|
|
302
|
+
</div>
|
|
303
|
+
<div class="stats">
|
|
304
|
+
${result.stats.totalCells} cells \xB7
|
|
305
|
+
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
306
|
+
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
307
|
+
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
308
|
+
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
309
|
+
</div>
|
|
310
|
+
</body>
|
|
311
|
+
</html>
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function formatMatrixCsv(componentName, result) {
|
|
315
|
+
const axisNames = result.axes.map((a) => a.name);
|
|
316
|
+
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
317
|
+
const rows = result.cells.map((cell) => {
|
|
318
|
+
const axisVals = result.axes.map((_, i) => {
|
|
319
|
+
const axisIdx = cell.axisIndices[i];
|
|
320
|
+
const axis = result.axes[i];
|
|
321
|
+
if (axisIdx === void 0 || axis === void 0) return "";
|
|
322
|
+
const val = axis.values[axisIdx];
|
|
323
|
+
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
324
|
+
});
|
|
325
|
+
return [
|
|
326
|
+
csvEscape(componentName),
|
|
327
|
+
...axisVals,
|
|
328
|
+
cell.result.renderTimeMs.toFixed(3),
|
|
329
|
+
String(cell.result.width),
|
|
330
|
+
String(cell.result.height)
|
|
331
|
+
].join(",");
|
|
332
|
+
});
|
|
333
|
+
return `${[headers.join(","), ...rows].join("\n")}
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
337
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
338
|
+
const empty = barWidth - filled;
|
|
339
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
340
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
341
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
342
|
+
}
|
|
343
|
+
function formatSummaryText(results, outputDir) {
|
|
344
|
+
const total = results.length;
|
|
345
|
+
const passed = results.filter((r) => r.success).length;
|
|
346
|
+
const failed = total - passed;
|
|
347
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
348
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
349
|
+
const lines = [
|
|
350
|
+
"\u2500".repeat(60),
|
|
351
|
+
`Render Summary`,
|
|
352
|
+
"\u2500".repeat(60),
|
|
353
|
+
` Total components : ${total}`,
|
|
354
|
+
` Passed : ${passed}`,
|
|
355
|
+
` Failed : ${failed}`,
|
|
356
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
357
|
+
` Output dir : ${outputDir}`
|
|
358
|
+
];
|
|
359
|
+
if (failed > 0) {
|
|
360
|
+
lines.push("", " Failed components:");
|
|
361
|
+
for (const r of results) {
|
|
362
|
+
if (!r.success) {
|
|
363
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
lines.push("\u2500".repeat(60));
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
370
|
+
function escapeHtml(str) {
|
|
371
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
372
|
+
}
|
|
373
|
+
function csvEscape(value) {
|
|
374
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
375
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
var CONFIG_FILENAMES = [
|
|
380
|
+
".reactscope/config.json",
|
|
381
|
+
".reactscope/config.js",
|
|
382
|
+
".reactscope/config.mjs"
|
|
383
|
+
];
|
|
384
|
+
var STYLE_ENTRY_CANDIDATES = [
|
|
385
|
+
"src/index.css",
|
|
386
|
+
"src/globals.css",
|
|
387
|
+
"app/globals.css",
|
|
388
|
+
"app/index.css",
|
|
389
|
+
"styles/index.css",
|
|
390
|
+
"index.css"
|
|
391
|
+
];
|
|
392
|
+
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
393
|
+
var compilerCache = null;
|
|
394
|
+
function getCachedBuild(cwd) {
|
|
395
|
+
if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
|
|
396
|
+
return compilerCache.build;
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function findStylesEntry(cwd) {
|
|
401
|
+
for (const name of CONFIG_FILENAMES) {
|
|
402
|
+
const p = resolve(cwd, name);
|
|
403
|
+
if (!existsSync(p)) continue;
|
|
404
|
+
try {
|
|
405
|
+
if (name.endsWith(".json")) {
|
|
406
|
+
const raw = readFileSync(p, "utf-8");
|
|
407
|
+
const data = JSON.parse(raw);
|
|
408
|
+
const scope = data.scope;
|
|
409
|
+
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
410
|
+
if (typeof entry === "string") {
|
|
411
|
+
const full = resolve(cwd, entry);
|
|
412
|
+
if (existsSync(full)) return full;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
419
|
+
if (existsSync(pkgPath)) {
|
|
420
|
+
try {
|
|
421
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
422
|
+
const pkg = JSON.parse(raw);
|
|
423
|
+
const entry = pkg.scope?.stylesEntry;
|
|
424
|
+
if (typeof entry === "string") {
|
|
425
|
+
const full = resolve(cwd, entry);
|
|
426
|
+
if (existsSync(full)) return full;
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
432
|
+
const full = resolve(cwd, candidate);
|
|
433
|
+
if (existsSync(full)) {
|
|
434
|
+
try {
|
|
435
|
+
const content = readFileSync(full, "utf-8");
|
|
436
|
+
if (TAILWIND_IMPORT.test(content)) return full;
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
async function getTailwindCompiler(cwd) {
|
|
444
|
+
const cached = getCachedBuild(cwd);
|
|
445
|
+
if (cached !== null) return cached;
|
|
446
|
+
const entryPath = findStylesEntry(cwd);
|
|
447
|
+
if (entryPath === null) return null;
|
|
448
|
+
let compile;
|
|
449
|
+
try {
|
|
450
|
+
const require2 = createRequire(resolve(cwd, "package.json"));
|
|
451
|
+
const tailwind = require2("tailwindcss");
|
|
452
|
+
const fn = tailwind.compile;
|
|
453
|
+
if (typeof fn !== "function") return null;
|
|
454
|
+
compile = fn;
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const entryContent = readFileSync(entryPath, "utf-8");
|
|
459
|
+
const loadStylesheet = async (id, base) => {
|
|
460
|
+
if (id === "tailwindcss") {
|
|
461
|
+
const nodeModules = resolve(cwd, "node_modules");
|
|
462
|
+
const tailwindCssPath = resolve(nodeModules, "tailwindcss", "index.css");
|
|
463
|
+
if (!existsSync(tailwindCssPath)) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const content = readFileSync(tailwindCssPath, "utf-8");
|
|
469
|
+
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
470
|
+
}
|
|
471
|
+
const full = resolve(base, id);
|
|
472
|
+
if (existsSync(full)) {
|
|
473
|
+
const content = readFileSync(full, "utf-8");
|
|
474
|
+
return { path: full, base: resolve(full, ".."), content };
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
477
|
+
};
|
|
478
|
+
const result = await compile(entryContent, {
|
|
479
|
+
base: cwd,
|
|
480
|
+
from: entryPath,
|
|
481
|
+
loadStylesheet
|
|
482
|
+
});
|
|
483
|
+
const build2 = result.build.bind(result);
|
|
484
|
+
compilerCache = { cwd, build: build2 };
|
|
485
|
+
return build2;
|
|
486
|
+
}
|
|
487
|
+
async function getCompiledCssForClasses(cwd, classes) {
|
|
488
|
+
const build2 = await getTailwindCompiler(cwd);
|
|
489
|
+
if (build2 === null) return null;
|
|
490
|
+
const deduped = [...new Set(classes)].filter(Boolean);
|
|
491
|
+
if (deduped.length === 0) return null;
|
|
492
|
+
return build2(deduped);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/ci/commands.ts
|
|
496
|
+
var CI_EXIT = {
|
|
497
|
+
/** All checks passed */
|
|
498
|
+
OK: 0,
|
|
499
|
+
/** Compliance score below threshold */
|
|
500
|
+
COMPLIANCE_BELOW_THRESHOLD: 1,
|
|
501
|
+
/** Accessibility violations found */
|
|
502
|
+
A11Y_VIOLATIONS: 2,
|
|
503
|
+
/** Console errors detected during render */
|
|
504
|
+
CONSOLE_ERRORS: 3,
|
|
505
|
+
/** Visual regression detected against baseline */
|
|
506
|
+
VISUAL_REGRESSION: 4,
|
|
507
|
+
/** One or more components failed to render */
|
|
508
|
+
RENDER_FAILURES: 5
|
|
509
|
+
};
|
|
510
|
+
var ALL_CHECKS = ["compliance", "a11y", "console-errors", "visual-regression"];
|
|
511
|
+
var _pool = null;
|
|
512
|
+
async function getPool(viewportWidth, viewportHeight) {
|
|
513
|
+
if (_pool === null) {
|
|
514
|
+
_pool = new BrowserPool({
|
|
515
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
516
|
+
viewportWidth,
|
|
517
|
+
viewportHeight
|
|
518
|
+
});
|
|
519
|
+
await _pool.init();
|
|
520
|
+
}
|
|
521
|
+
return _pool;
|
|
522
|
+
}
|
|
523
|
+
async function shutdownPool() {
|
|
524
|
+
if (_pool !== null) {
|
|
525
|
+
await _pool.close();
|
|
526
|
+
_pool = null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
530
|
+
const pool = await getPool(viewportWidth, viewportHeight);
|
|
531
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
532
|
+
const slot = await pool.acquire();
|
|
533
|
+
const { page } = slot;
|
|
534
|
+
try {
|
|
535
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
536
|
+
await page.waitForFunction(
|
|
537
|
+
() => {
|
|
538
|
+
const w = window;
|
|
539
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
540
|
+
},
|
|
541
|
+
{ timeout: 15e3 }
|
|
542
|
+
);
|
|
543
|
+
const renderError = await page.evaluate(() => {
|
|
544
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
545
|
+
});
|
|
546
|
+
if (renderError !== null) {
|
|
547
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
548
|
+
}
|
|
549
|
+
const rootDir = process.cwd();
|
|
550
|
+
const classes = await page.evaluate(() => {
|
|
551
|
+
const set = /* @__PURE__ */ new Set();
|
|
552
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
553
|
+
for (const c of el.className.split(/\s+/)) {
|
|
554
|
+
if (c) set.add(c);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return [...set];
|
|
558
|
+
});
|
|
559
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
560
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
561
|
+
await page.addStyleTag({ content: projectCss });
|
|
562
|
+
}
|
|
563
|
+
const startMs = performance.now();
|
|
564
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
565
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
566
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const PAD = 24;
|
|
572
|
+
const MIN_W = 320;
|
|
573
|
+
const MIN_H = 200;
|
|
574
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
575
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
576
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
577
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
578
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
579
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
580
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
581
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
582
|
+
const screenshot = await page.screenshot({
|
|
583
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
584
|
+
type: "png"
|
|
585
|
+
});
|
|
586
|
+
const computedStylesRaw = {};
|
|
587
|
+
const styles = await page.evaluate((sel) => {
|
|
588
|
+
const el = document.querySelector(sel);
|
|
589
|
+
if (el === null) return {};
|
|
590
|
+
const computed = window.getComputedStyle(el);
|
|
591
|
+
const out = {};
|
|
592
|
+
for (const prop of [
|
|
593
|
+
"display",
|
|
594
|
+
"width",
|
|
595
|
+
"height",
|
|
596
|
+
"color",
|
|
597
|
+
"backgroundColor",
|
|
598
|
+
"fontSize",
|
|
599
|
+
"fontFamily",
|
|
600
|
+
"padding",
|
|
601
|
+
"margin"
|
|
602
|
+
]) {
|
|
603
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}, "[data-reactscope-root] > *");
|
|
607
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
608
|
+
const renderTimeMs = performance.now() - startMs;
|
|
609
|
+
return {
|
|
610
|
+
screenshot,
|
|
611
|
+
width: Math.round(safeW),
|
|
612
|
+
height: Math.round(safeH),
|
|
613
|
+
renderTimeMs,
|
|
614
|
+
computedStyles: computedStylesRaw
|
|
615
|
+
};
|
|
616
|
+
} finally {
|
|
617
|
+
pool.release(slot);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
621
|
+
const flat = {};
|
|
622
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
623
|
+
Object.assign(flat, styles);
|
|
624
|
+
}
|
|
625
|
+
const colors = {};
|
|
626
|
+
const spacing = {};
|
|
627
|
+
const typography = {};
|
|
628
|
+
const borders = {};
|
|
629
|
+
const shadows = {};
|
|
630
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
631
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
632
|
+
colors[prop] = value;
|
|
633
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
634
|
+
spacing[prop] = value;
|
|
635
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
636
|
+
typography[prop] = value;
|
|
637
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
638
|
+
borders[prop] = value;
|
|
639
|
+
} else if (prop === "boxShadow") {
|
|
640
|
+
shadows[prop] = value;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { colors, spacing, typography, borders, shadows };
|
|
644
|
+
}
|
|
645
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
646
|
+
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
647
|
+
if (!existsSync(jsonPath)) return null;
|
|
648
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
649
|
+
}
|
|
650
|
+
async function runCi(options = {}) {
|
|
651
|
+
const {
|
|
652
|
+
baselineDir: baselineDirRaw,
|
|
653
|
+
checks = ALL_CHECKS,
|
|
654
|
+
complianceThreshold = 0.9,
|
|
655
|
+
viewportWidth = 375,
|
|
656
|
+
viewportHeight = 812
|
|
657
|
+
} = options;
|
|
658
|
+
const startTime = performance.now();
|
|
659
|
+
const rootDir = process.cwd();
|
|
660
|
+
const checksSet = new Set(checks);
|
|
661
|
+
const baselineDir = baselineDirRaw !== void 0 ? resolve(rootDir, baselineDirRaw) : void 0;
|
|
662
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
663
|
+
const manifest = await generateManifest({ rootDir });
|
|
664
|
+
const componentNames = Object.keys(manifest.components);
|
|
665
|
+
const total = componentNames.length;
|
|
666
|
+
process.stderr.write(`Found ${total} components.
|
|
667
|
+
`);
|
|
668
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
669
|
+
`);
|
|
670
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
671
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
672
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
673
|
+
let completed = 0;
|
|
674
|
+
const CONCURRENCY = 4;
|
|
675
|
+
let nextIdx = 0;
|
|
676
|
+
const renderOne = async (name) => {
|
|
677
|
+
const descriptor = manifest.components[name];
|
|
678
|
+
if (descriptor === void 0) return;
|
|
679
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
680
|
+
const outcome = await safeRender(
|
|
681
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
682
|
+
{
|
|
683
|
+
props: {},
|
|
684
|
+
sourceLocation: {
|
|
685
|
+
file: descriptor.filePath,
|
|
686
|
+
line: descriptor.loc.start,
|
|
687
|
+
column: 0
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
completed++;
|
|
692
|
+
const pct = Math.round(completed / total * 100);
|
|
693
|
+
if (isTTY()) {
|
|
694
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
695
|
+
}
|
|
696
|
+
if (outcome.crashed) {
|
|
697
|
+
renderFailures.add(name);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const result = outcome.result;
|
|
701
|
+
currentRenderMeta.set(name, { width: result.width, height: result.height });
|
|
702
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
703
|
+
};
|
|
704
|
+
if (total > 0) {
|
|
705
|
+
const worker = async () => {
|
|
706
|
+
while (nextIdx < componentNames.length) {
|
|
707
|
+
const i = nextIdx++;
|
|
708
|
+
const name = componentNames[i];
|
|
709
|
+
if (name !== void 0) {
|
|
710
|
+
await renderOne(name);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const workers = [];
|
|
715
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
716
|
+
workers.push(worker());
|
|
717
|
+
}
|
|
718
|
+
await Promise.all(workers);
|
|
719
|
+
}
|
|
720
|
+
await shutdownPool();
|
|
721
|
+
if (isTTY() && total > 0) {
|
|
722
|
+
process.stderr.write("\n");
|
|
723
|
+
}
|
|
724
|
+
const resolver = new TokenResolver([]);
|
|
725
|
+
const engine = new ComplianceEngine(resolver);
|
|
726
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
727
|
+
const complianceScore = batchReport.aggregateCompliance;
|
|
728
|
+
const checkResults = [];
|
|
729
|
+
const renderFailureCount = renderFailures.size;
|
|
730
|
+
const rendersPassed = renderFailureCount === 0;
|
|
731
|
+
if (checksSet.has("compliance")) {
|
|
732
|
+
const compliancePassed = complianceScore >= complianceThreshold;
|
|
733
|
+
checkResults.push({
|
|
734
|
+
check: "compliance",
|
|
735
|
+
passed: compliancePassed,
|
|
736
|
+
message: compliancePassed ? `Compliance ${(complianceScore * 100).toFixed(1)}% >= threshold ${(complianceThreshold * 100).toFixed(1)}%` : `Compliance ${(complianceScore * 100).toFixed(1)}% < threshold ${(complianceThreshold * 100).toFixed(1)}%`,
|
|
737
|
+
value: complianceScore,
|
|
738
|
+
threshold: complianceThreshold
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
if (checksSet.has("a11y")) {
|
|
742
|
+
checkResults.push({
|
|
743
|
+
check: "a11y",
|
|
744
|
+
passed: true,
|
|
745
|
+
message: "Accessibility audit not yet implemented \u2014 skipped"
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (checksSet.has("console-errors")) {
|
|
749
|
+
const consoleErrorsPassed = rendersPassed;
|
|
750
|
+
checkResults.push({
|
|
751
|
+
check: "console-errors",
|
|
752
|
+
passed: consoleErrorsPassed,
|
|
753
|
+
message: consoleErrorsPassed ? "No console errors detected" : `Console errors likely \u2014 ${renderFailureCount} component(s) failed to render`
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
let hasVisualRegression = false;
|
|
757
|
+
if (checksSet.has("visual-regression") && baselineDir !== void 0) {
|
|
758
|
+
if (!existsSync(baselineDir)) {
|
|
759
|
+
process.stderr.write(
|
|
760
|
+
`Warning: baseline directory not found at "${baselineDir}" \u2014 skipping visual regression check.
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
checkResults.push({
|
|
764
|
+
check: "visual-regression",
|
|
765
|
+
passed: true,
|
|
766
|
+
message: `Baseline not found at ${baselineDir} \u2014 skipped`
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
const regressions = [];
|
|
770
|
+
for (const name of componentNames) {
|
|
771
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
772
|
+
const currentMeta = currentRenderMeta.get(name);
|
|
773
|
+
if (baselineMeta !== null && currentMeta !== void 0) {
|
|
774
|
+
const dw = Math.abs(currentMeta.width - baselineMeta.width);
|
|
775
|
+
const dh = Math.abs(currentMeta.height - baselineMeta.height);
|
|
776
|
+
if (dw > 10 || dh > 10) {
|
|
777
|
+
regressions.push(name);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
hasVisualRegression = regressions.length > 0;
|
|
782
|
+
checkResults.push({
|
|
783
|
+
check: "visual-regression",
|
|
784
|
+
passed: !hasVisualRegression,
|
|
785
|
+
message: hasVisualRegression ? `Visual regression detected in ${regressions.length} component(s): ${regressions.slice(0, 5).join(", ")}${regressions.length > 5 ? "..." : ""}` : "No visual regressions detected"
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
let exitCode = CI_EXIT.OK;
|
|
790
|
+
const complianceResult = checkResults.find((r) => r.check === "compliance");
|
|
791
|
+
const a11yResult = checkResults.find((r) => r.check === "a11y");
|
|
792
|
+
const consoleResult = checkResults.find((r) => r.check === "console-errors");
|
|
793
|
+
const visualResult = checkResults.find((r) => r.check === "visual-regression");
|
|
794
|
+
if (complianceResult !== void 0 && !complianceResult.passed) {
|
|
795
|
+
exitCode = CI_EXIT.COMPLIANCE_BELOW_THRESHOLD;
|
|
796
|
+
} else if (a11yResult !== void 0 && !a11yResult.passed) {
|
|
797
|
+
exitCode = CI_EXIT.A11Y_VIOLATIONS;
|
|
798
|
+
} else if (consoleResult !== void 0 && !consoleResult.passed) {
|
|
799
|
+
exitCode = CI_EXIT.CONSOLE_ERRORS;
|
|
800
|
+
} else if (visualResult !== void 0 && !visualResult.passed) {
|
|
801
|
+
exitCode = CI_EXIT.VISUAL_REGRESSION;
|
|
802
|
+
} else if (!rendersPassed) {
|
|
803
|
+
exitCode = CI_EXIT.RENDER_FAILURES;
|
|
804
|
+
}
|
|
805
|
+
const passed = exitCode === CI_EXIT.OK;
|
|
806
|
+
const wallClockMs = performance.now() - startTime;
|
|
807
|
+
return {
|
|
808
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
809
|
+
passed,
|
|
810
|
+
exitCode,
|
|
811
|
+
checks: checkResults,
|
|
812
|
+
components: {
|
|
813
|
+
total,
|
|
814
|
+
rendered: total - renderFailureCount,
|
|
815
|
+
failed: renderFailureCount
|
|
816
|
+
},
|
|
817
|
+
complianceScore,
|
|
818
|
+
complianceThreshold,
|
|
819
|
+
baselineCompared: baselineDir !== void 0 && existsSync(baselineDir),
|
|
820
|
+
wallClockMs
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function formatCiReport(result) {
|
|
824
|
+
const lines = [];
|
|
825
|
+
const title = "Scope CI Report";
|
|
826
|
+
const rule2 = "=".repeat(Math.max(title.length, 40));
|
|
827
|
+
lines.push(title, rule2);
|
|
828
|
+
const { total, rendered, failed } = result.components;
|
|
829
|
+
lines.push(
|
|
830
|
+
`Components: ${total} total ${rendered} rendered${failed > 0 ? ` ${failed} failed` : ""}`
|
|
831
|
+
);
|
|
832
|
+
lines.push("");
|
|
833
|
+
for (const check of result.checks) {
|
|
834
|
+
const icon = check.passed ? "pass" : "FAIL";
|
|
835
|
+
lines.push(` [${icon}] ${check.message}`);
|
|
836
|
+
}
|
|
837
|
+
if (result.components.failed > 0) {
|
|
838
|
+
lines.push(` [FAIL] ${result.components.failed} component(s) failed to render`);
|
|
839
|
+
}
|
|
840
|
+
lines.push("");
|
|
841
|
+
lines.push(rule2);
|
|
842
|
+
if (result.passed) {
|
|
843
|
+
lines.push(
|
|
844
|
+
`CI passed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
lines.push(
|
|
848
|
+
`CI failed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return lines.join("\n");
|
|
852
|
+
}
|
|
853
|
+
function parseChecks(raw) {
|
|
854
|
+
if (raw === void 0) return void 0;
|
|
855
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
856
|
+
if (parts.length === 0) return void 0;
|
|
857
|
+
const valid = new Set(ALL_CHECKS);
|
|
858
|
+
const parsed = [];
|
|
859
|
+
for (const part of parts) {
|
|
860
|
+
if (!valid.has(part)) {
|
|
861
|
+
process.stderr.write(
|
|
862
|
+
`Warning: unknown check "${part}" \u2014 valid checks are: ${ALL_CHECKS.join(", ")}
|
|
863
|
+
`
|
|
864
|
+
);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
parsed.push(part);
|
|
868
|
+
}
|
|
869
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
870
|
+
}
|
|
871
|
+
function createCiCommand() {
|
|
872
|
+
return new Command("ci").description(
|
|
873
|
+
"Run a non-interactive CI pipeline (manifest -> render -> compliance -> regression) with exit codes"
|
|
874
|
+
).option(
|
|
875
|
+
"-b, --baseline <dir>",
|
|
876
|
+
"Baseline directory for visual regression comparison (omit to skip)"
|
|
877
|
+
).option(
|
|
878
|
+
"--checks <list>",
|
|
879
|
+
`Comma-separated checks to run (default: all). Valid: ${ALL_CHECKS.join(", ")}`
|
|
880
|
+
).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(
|
|
881
|
+
async (opts) => {
|
|
882
|
+
try {
|
|
883
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
884
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
885
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
886
|
+
const complianceThreshold = Number.parseFloat(opts.threshold);
|
|
887
|
+
const checks = parseChecks(opts.checks);
|
|
888
|
+
const result = await runCi({
|
|
889
|
+
baselineDir: opts.baseline,
|
|
890
|
+
checks,
|
|
891
|
+
complianceThreshold,
|
|
892
|
+
viewportWidth,
|
|
893
|
+
viewportHeight
|
|
894
|
+
});
|
|
895
|
+
if (opts.output !== void 0) {
|
|
896
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
897
|
+
writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
|
|
898
|
+
process.stderr.write(`CI result written to ${opts.output}
|
|
899
|
+
`);
|
|
900
|
+
}
|
|
901
|
+
process.stdout.write(`${formatCiReport(result)}
|
|
902
|
+
`);
|
|
903
|
+
if (opts.json) {
|
|
904
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
905
|
+
`);
|
|
906
|
+
}
|
|
907
|
+
process.exit(result.exitCode);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
910
|
+
`);
|
|
911
|
+
process.exit(CI_EXIT.RENDER_FAILURES);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
}
|
|
14
916
|
function hasConfigFile(dir, stem) {
|
|
15
917
|
if (!existsSync(dir)) return false;
|
|
16
918
|
try {
|
|
@@ -205,9 +1107,9 @@ function createRL() {
|
|
|
205
1107
|
});
|
|
206
1108
|
}
|
|
207
1109
|
async function ask(rl, question) {
|
|
208
|
-
return new Promise((
|
|
1110
|
+
return new Promise((resolve15) => {
|
|
209
1111
|
rl.question(question, (answer) => {
|
|
210
|
-
|
|
1112
|
+
resolve15(answer.trim());
|
|
211
1113
|
});
|
|
212
1114
|
});
|
|
213
1115
|
}
|
|
@@ -340,225 +1242,20 @@ async function runInit(options) {
|
|
|
340
1242
|
skipped: false
|
|
341
1243
|
};
|
|
342
1244
|
}
|
|
343
|
-
function createInitCommand() {
|
|
344
|
-
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) => {
|
|
345
|
-
try {
|
|
346
|
-
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
347
|
-
if (!result.success && !result.skipped) {
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
350
|
-
} catch (err) {
|
|
351
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
352
|
-
`);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
358
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
359
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
360
|
-
}
|
|
361
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
362
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
363
|
-
const wrapperCode = (
|
|
364
|
-
/* ts */
|
|
365
|
-
`
|
|
366
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
367
|
-
import { createRoot } from "react-dom/client";
|
|
368
|
-
import { createElement } from "react";
|
|
369
|
-
|
|
370
|
-
(function scopeRenderHarness() {
|
|
371
|
-
var Component =
|
|
372
|
-
__scopeMod["default"] ||
|
|
373
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
374
|
-
(Object.values(__scopeMod).find(
|
|
375
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
376
|
-
));
|
|
377
|
-
|
|
378
|
-
if (!Component) {
|
|
379
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
380
|
-
"No renderable component found. Checked: default, " +
|
|
381
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
382
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
383
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
var props = ${propsJson};
|
|
389
|
-
var rootEl = document.getElementById("scope-root");
|
|
390
|
-
if (!rootEl) {
|
|
391
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
392
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
396
|
-
// Use requestAnimationFrame to let React flush the render
|
|
397
|
-
requestAnimationFrame(function() {
|
|
398
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
399
|
-
});
|
|
400
|
-
} catch (err) {
|
|
401
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
402
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
403
|
-
}
|
|
404
|
-
})();
|
|
405
|
-
`
|
|
406
|
-
);
|
|
407
|
-
const result = await esbuild.build({
|
|
408
|
-
stdin: {
|
|
409
|
-
contents: wrapperCode,
|
|
410
|
-
// Resolve relative imports (within the component's dir)
|
|
411
|
-
resolveDir: dirname(filePath),
|
|
412
|
-
loader: "tsx",
|
|
413
|
-
sourcefile: "__scope_harness__.tsx"
|
|
414
|
-
},
|
|
415
|
-
bundle: true,
|
|
416
|
-
format: "iife",
|
|
417
|
-
write: false,
|
|
418
|
-
platform: "browser",
|
|
419
|
-
jsx: "automatic",
|
|
420
|
-
jsxImportSource: "react",
|
|
421
|
-
target: "es2020",
|
|
422
|
-
// Bundle everything — no externals
|
|
423
|
-
external: [],
|
|
424
|
-
define: {
|
|
425
|
-
"process.env.NODE_ENV": '"development"',
|
|
426
|
-
global: "globalThis"
|
|
427
|
-
},
|
|
428
|
-
logLevel: "silent",
|
|
429
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
430
|
-
banner: {
|
|
431
|
-
js: "/* @agent-scope/cli component harness */"
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
if (result.errors.length > 0) {
|
|
435
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
436
|
-
throw new Error(`esbuild failed to bundle component:
|
|
437
|
-
${msg}`);
|
|
438
|
-
}
|
|
439
|
-
const outputFile = result.outputFiles?.[0];
|
|
440
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
441
|
-
throw new Error("esbuild produced no output");
|
|
442
|
-
}
|
|
443
|
-
return outputFile.text;
|
|
444
|
-
}
|
|
445
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
446
|
-
const projectStyleBlock = "";
|
|
447
|
-
return `<!DOCTYPE html>
|
|
448
|
-
<html lang="en">
|
|
449
|
-
<head>
|
|
450
|
-
<meta charset="UTF-8" />
|
|
451
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
452
|
-
<style>
|
|
453
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
454
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
455
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
456
|
-
</style>
|
|
457
|
-
${projectStyleBlock}
|
|
458
|
-
</head>
|
|
459
|
-
<body>
|
|
460
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
461
|
-
<script>${bundledScript}</script>
|
|
462
|
-
</body>
|
|
463
|
-
</html>`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// src/manifest-formatter.ts
|
|
467
|
-
function isTTY() {
|
|
468
|
-
return process.stdout.isTTY === true;
|
|
469
|
-
}
|
|
470
|
-
function pad(value, width) {
|
|
471
|
-
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
472
|
-
}
|
|
473
|
-
function buildTable(headers, rows) {
|
|
474
|
-
const colWidths = headers.map(
|
|
475
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
476
|
-
);
|
|
477
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
478
|
-
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
479
|
-
const dataRows = rows.map(
|
|
480
|
-
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
481
|
-
);
|
|
482
|
-
return [headerRow, divider, ...dataRows].join("\n");
|
|
483
|
-
}
|
|
484
|
-
function formatListTable(rows) {
|
|
485
|
-
if (rows.length === 0) return "No components found.";
|
|
486
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
487
|
-
const tableRows = rows.map((r) => [
|
|
488
|
-
r.name,
|
|
489
|
-
r.file,
|
|
490
|
-
r.complexityClass,
|
|
491
|
-
String(r.hookCount),
|
|
492
|
-
String(r.contextCount)
|
|
493
|
-
]);
|
|
494
|
-
return buildTable(headers, tableRows);
|
|
495
|
-
}
|
|
496
|
-
function formatListJson(rows) {
|
|
497
|
-
return JSON.stringify(rows, null, 2);
|
|
498
|
-
}
|
|
499
|
-
function formatSideEffects(se) {
|
|
500
|
-
const parts = [];
|
|
501
|
-
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
502
|
-
if (se.timers) parts.push("timers");
|
|
503
|
-
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
504
|
-
if (se.globalListeners) parts.push("globalListeners");
|
|
505
|
-
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
506
|
-
}
|
|
507
|
-
function formatGetTable(name, descriptor) {
|
|
508
|
-
const propNames = Object.keys(descriptor.props);
|
|
509
|
-
const lines = [
|
|
510
|
-
`Component: ${name}`,
|
|
511
|
-
` File: ${descriptor.filePath}`,
|
|
512
|
-
` Export: ${descriptor.exportType}`,
|
|
513
|
-
` Display Name: ${descriptor.displayName}`,
|
|
514
|
-
` Complexity: ${descriptor.complexityClass}`,
|
|
515
|
-
` Memoized: ${descriptor.memoized}`,
|
|
516
|
-
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
517
|
-
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
518
|
-
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
519
|
-
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
520
|
-
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
521
|
-
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
522
|
-
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
523
|
-
"",
|
|
524
|
-
` Props (${propNames.length}):`
|
|
525
|
-
];
|
|
526
|
-
if (propNames.length === 0) {
|
|
527
|
-
lines.push(" (none)");
|
|
528
|
-
} else {
|
|
529
|
-
for (const propName of propNames) {
|
|
530
|
-
const p = descriptor.props[propName];
|
|
531
|
-
if (p === void 0) continue;
|
|
532
|
-
const req = p.required ? "required" : "optional";
|
|
533
|
-
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
534
|
-
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
535
|
-
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return lines.join("\n");
|
|
539
|
-
}
|
|
540
|
-
function formatGetJson(name, descriptor) {
|
|
541
|
-
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
542
|
-
}
|
|
543
|
-
function formatQueryTable(rows, queryDesc) {
|
|
544
|
-
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
545
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
546
|
-
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
547
|
-
return `Query: ${queryDesc}
|
|
548
|
-
|
|
549
|
-
${buildTable(headers, tableRows)}`;
|
|
550
|
-
}
|
|
551
|
-
function formatQueryJson(rows) {
|
|
552
|
-
return JSON.stringify(rows, null, 2);
|
|
553
|
-
}
|
|
554
|
-
function matchGlob(pattern, value) {
|
|
555
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
556
|
-
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
557
|
-
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
558
|
-
return regex.test(value);
|
|
1245
|
+
function createInitCommand() {
|
|
1246
|
+
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) => {
|
|
1247
|
+
try {
|
|
1248
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1249
|
+
if (!result.success && !result.skipped) {
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1254
|
+
`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
559
1258
|
}
|
|
560
|
-
|
|
561
|
-
// src/manifest-commands.ts
|
|
562
1259
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
563
1260
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
564
1261
|
const absPath = resolve(process.cwd(), manifestPath);
|
|
@@ -637,257 +1334,97 @@ function registerQuery(manifestCmd) {
|
|
|
637
1334
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
638
1335
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
639
1336
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
640
|
-
if (queryParts.length === 0) {
|
|
641
|
-
process.stderr.write(
|
|
642
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
|
|
643
|
-
);
|
|
644
|
-
process.exit(1);
|
|
645
|
-
}
|
|
646
|
-
const queryDesc = queryParts.join(", ");
|
|
647
|
-
let entries = Object.entries(manifest.components);
|
|
648
|
-
if (opts.context !== void 0) {
|
|
649
|
-
const ctx = opts.context;
|
|
650
|
-
entries = entries.filter(([, d]) => d.requiredContexts.includes(ctx));
|
|
651
|
-
}
|
|
652
|
-
if (opts.hook !== void 0) {
|
|
653
|
-
const hook = opts.hook;
|
|
654
|
-
entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
|
|
655
|
-
}
|
|
656
|
-
if (opts.complexity !== void 0) {
|
|
657
|
-
const cls = opts.complexity;
|
|
658
|
-
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
659
|
-
}
|
|
660
|
-
if (opts.sideEffects) {
|
|
661
|
-
entries = entries.filter(([, d]) => {
|
|
662
|
-
const se = d.sideEffects;
|
|
663
|
-
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
if (opts.hasFetch) {
|
|
667
|
-
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
668
|
-
}
|
|
669
|
-
const rows = entries.map(([name, d]) => ({
|
|
670
|
-
name,
|
|
671
|
-
file: d.filePath,
|
|
672
|
-
complexityClass: d.complexityClass,
|
|
673
|
-
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
674
|
-
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
675
|
-
}));
|
|
676
|
-
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
677
|
-
process.stdout.write(`${output}
|
|
678
|
-
`);
|
|
679
|
-
} catch (err) {
|
|
680
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
681
|
-
`);
|
|
682
|
-
process.exit(1);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
function registerGenerate(manifestCmd) {
|
|
688
|
-
manifestCmd.command("generate").description(
|
|
689
|
-
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
690
|
-
).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) => {
|
|
691
|
-
try {
|
|
692
|
-
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
693
|
-
const outputPath = resolve(process.cwd(), opts.output);
|
|
694
|
-
const include = opts.include?.split(",").map((s) => s.trim());
|
|
695
|
-
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
696
|
-
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
697
|
-
`);
|
|
698
|
-
const manifest = await generateManifest({
|
|
699
|
-
rootDir,
|
|
700
|
-
...include !== void 0 && { include },
|
|
701
|
-
...exclude !== void 0 && { exclude }
|
|
702
|
-
});
|
|
703
|
-
const componentCount = Object.keys(manifest.components).length;
|
|
704
|
-
process.stderr.write(`Found ${componentCount} components.
|
|
705
|
-
`);
|
|
706
|
-
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
707
|
-
if (!existsSync(outputDir)) {
|
|
708
|
-
mkdirSync(outputDir, { recursive: true });
|
|
709
|
-
}
|
|
710
|
-
writeFileSync(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
711
|
-
process.stderr.write(`Manifest written to ${outputPath}
|
|
712
|
-
`);
|
|
713
|
-
process.stdout.write(`${outputPath}
|
|
714
|
-
`);
|
|
715
|
-
} catch (err) {
|
|
716
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
717
|
-
`);
|
|
718
|
-
process.exit(1);
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
function createManifestCommand() {
|
|
723
|
-
const manifestCmd = new Command("manifest").description(
|
|
724
|
-
"Query and explore the component manifest"
|
|
725
|
-
);
|
|
726
|
-
registerList(manifestCmd);
|
|
727
|
-
registerGet(manifestCmd);
|
|
728
|
-
registerQuery(manifestCmd);
|
|
729
|
-
registerGenerate(manifestCmd);
|
|
730
|
-
return manifestCmd;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// src/render-formatter.ts
|
|
734
|
-
function parseViewport(spec) {
|
|
735
|
-
const lower = spec.toLowerCase();
|
|
736
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
737
|
-
if (!match) {
|
|
738
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
739
|
-
}
|
|
740
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
741
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
742
|
-
if (width <= 0 || height <= 0) {
|
|
743
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
744
|
-
}
|
|
745
|
-
return { width, height };
|
|
746
|
-
}
|
|
747
|
-
function formatRenderJson(componentName, props, result) {
|
|
748
|
-
return {
|
|
749
|
-
component: componentName,
|
|
750
|
-
props,
|
|
751
|
-
width: result.width,
|
|
752
|
-
height: result.height,
|
|
753
|
-
renderTimeMs: result.renderTimeMs,
|
|
754
|
-
computedStyles: result.computedStyles,
|
|
755
|
-
screenshot: result.screenshot.toString("base64"),
|
|
756
|
-
dom: result.dom,
|
|
757
|
-
console: result.console,
|
|
758
|
-
accessibility: result.accessibility
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
function formatMatrixJson(result) {
|
|
762
|
-
return {
|
|
763
|
-
axes: result.axes.map((axis) => ({
|
|
764
|
-
name: axis.name,
|
|
765
|
-
values: axis.values.map((v) => String(v))
|
|
766
|
-
})),
|
|
767
|
-
stats: { ...result.stats },
|
|
768
|
-
cells: result.cells.map((cell) => ({
|
|
769
|
-
index: cell.index,
|
|
770
|
-
axisIndices: cell.axisIndices,
|
|
771
|
-
props: cell.props,
|
|
772
|
-
renderTimeMs: cell.result.renderTimeMs,
|
|
773
|
-
width: cell.result.width,
|
|
774
|
-
height: cell.result.height,
|
|
775
|
-
screenshot: cell.result.screenshot.toString("base64")
|
|
776
|
-
}))
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
function formatMatrixHtml(componentName, result) {
|
|
780
|
-
const cellsHtml = result.cells.map((cell) => {
|
|
781
|
-
const b64 = cell.result.screenshot.toString("base64");
|
|
782
|
-
const propLabel = escapeHtml(
|
|
783
|
-
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
784
|
-
);
|
|
785
|
-
return ` <div class="cell">
|
|
786
|
-
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
787
|
-
<div class="label">${propLabel}</div>
|
|
788
|
-
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
789
|
-
</div>`;
|
|
790
|
-
}).join("\n");
|
|
791
|
-
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
792
|
-
return `<!DOCTYPE html>
|
|
793
|
-
<html lang="en">
|
|
794
|
-
<head>
|
|
795
|
-
<meta charset="UTF-8" />
|
|
796
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
797
|
-
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
798
|
-
<style>
|
|
799
|
-
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
800
|
-
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
801
|
-
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
802
|
-
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
803
|
-
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
804
|
-
.cell img { display: block; }
|
|
805
|
-
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
806
|
-
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
807
|
-
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
808
|
-
</style>
|
|
809
|
-
</head>
|
|
810
|
-
<body>
|
|
811
|
-
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
812
|
-
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
813
|
-
<div class="grid">
|
|
814
|
-
${cellsHtml}
|
|
815
|
-
</div>
|
|
816
|
-
<div class="stats">
|
|
817
|
-
${result.stats.totalCells} cells \xB7
|
|
818
|
-
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
819
|
-
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
820
|
-
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
821
|
-
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
822
|
-
</div>
|
|
823
|
-
</body>
|
|
824
|
-
</html>
|
|
825
|
-
`;
|
|
826
|
-
}
|
|
827
|
-
function formatMatrixCsv(componentName, result) {
|
|
828
|
-
const axisNames = result.axes.map((a) => a.name);
|
|
829
|
-
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
830
|
-
const rows = result.cells.map((cell) => {
|
|
831
|
-
const axisVals = result.axes.map((_, i) => {
|
|
832
|
-
const axisIdx = cell.axisIndices[i];
|
|
833
|
-
const axis = result.axes[i];
|
|
834
|
-
if (axisIdx === void 0 || axis === void 0) return "";
|
|
835
|
-
const val = axis.values[axisIdx];
|
|
836
|
-
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
837
|
-
});
|
|
838
|
-
return [
|
|
839
|
-
csvEscape(componentName),
|
|
840
|
-
...axisVals,
|
|
841
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
842
|
-
String(cell.result.width),
|
|
843
|
-
String(cell.result.height)
|
|
844
|
-
].join(",");
|
|
845
|
-
});
|
|
846
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
847
|
-
`;
|
|
848
|
-
}
|
|
849
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
850
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
851
|
-
const empty = barWidth - filled;
|
|
852
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
853
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
854
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
855
|
-
}
|
|
856
|
-
function formatSummaryText(results, outputDir) {
|
|
857
|
-
const total = results.length;
|
|
858
|
-
const passed = results.filter((r) => r.success).length;
|
|
859
|
-
const failed = total - passed;
|
|
860
|
-
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
861
|
-
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
862
|
-
const lines = [
|
|
863
|
-
"\u2500".repeat(60),
|
|
864
|
-
`Render Summary`,
|
|
865
|
-
"\u2500".repeat(60),
|
|
866
|
-
` Total components : ${total}`,
|
|
867
|
-
` Passed : ${passed}`,
|
|
868
|
-
` Failed : ${failed}`,
|
|
869
|
-
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
870
|
-
` Output dir : ${outputDir}`
|
|
871
|
-
];
|
|
872
|
-
if (failed > 0) {
|
|
873
|
-
lines.push("", " Failed components:");
|
|
874
|
-
for (const r of results) {
|
|
875
|
-
if (!r.success) {
|
|
876
|
-
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
1337
|
+
if (queryParts.length === 0) {
|
|
1338
|
+
process.stderr.write(
|
|
1339
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
|
|
1340
|
+
);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
const queryDesc = queryParts.join(", ");
|
|
1344
|
+
let entries = Object.entries(manifest.components);
|
|
1345
|
+
if (opts.context !== void 0) {
|
|
1346
|
+
const ctx = opts.context;
|
|
1347
|
+
entries = entries.filter(([, d]) => d.requiredContexts.includes(ctx));
|
|
1348
|
+
}
|
|
1349
|
+
if (opts.hook !== void 0) {
|
|
1350
|
+
const hook = opts.hook;
|
|
1351
|
+
entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
|
|
1352
|
+
}
|
|
1353
|
+
if (opts.complexity !== void 0) {
|
|
1354
|
+
const cls = opts.complexity;
|
|
1355
|
+
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
1356
|
+
}
|
|
1357
|
+
if (opts.sideEffects) {
|
|
1358
|
+
entries = entries.filter(([, d]) => {
|
|
1359
|
+
const se = d.sideEffects;
|
|
1360
|
+
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
if (opts.hasFetch) {
|
|
1364
|
+
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1365
|
+
}
|
|
1366
|
+
const rows = entries.map(([name, d]) => ({
|
|
1367
|
+
name,
|
|
1368
|
+
file: d.filePath,
|
|
1369
|
+
complexityClass: d.complexityClass,
|
|
1370
|
+
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
1371
|
+
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
1372
|
+
}));
|
|
1373
|
+
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
1374
|
+
process.stdout.write(`${output}
|
|
1375
|
+
`);
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1378
|
+
`);
|
|
1379
|
+
process.exit(1);
|
|
877
1380
|
}
|
|
878
1381
|
}
|
|
879
|
-
|
|
880
|
-
lines.push("\u2500".repeat(60));
|
|
881
|
-
return lines.join("\n");
|
|
1382
|
+
);
|
|
882
1383
|
}
|
|
883
|
-
function
|
|
884
|
-
|
|
1384
|
+
function registerGenerate(manifestCmd) {
|
|
1385
|
+
manifestCmd.command("generate").description(
|
|
1386
|
+
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
1387
|
+
).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) => {
|
|
1388
|
+
try {
|
|
1389
|
+
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
1390
|
+
const outputPath = resolve(process.cwd(), opts.output);
|
|
1391
|
+
const include = opts.include?.split(",").map((s) => s.trim());
|
|
1392
|
+
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
1393
|
+
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
1394
|
+
`);
|
|
1395
|
+
const manifest = await generateManifest({
|
|
1396
|
+
rootDir,
|
|
1397
|
+
...include !== void 0 && { include },
|
|
1398
|
+
...exclude !== void 0 && { exclude }
|
|
1399
|
+
});
|
|
1400
|
+
const componentCount = Object.keys(manifest.components).length;
|
|
1401
|
+
process.stderr.write(`Found ${componentCount} components.
|
|
1402
|
+
`);
|
|
1403
|
+
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
1404
|
+
if (!existsSync(outputDir)) {
|
|
1405
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1406
|
+
}
|
|
1407
|
+
writeFileSync(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
1408
|
+
process.stderr.write(`Manifest written to ${outputPath}
|
|
1409
|
+
`);
|
|
1410
|
+
process.stdout.write(`${outputPath}
|
|
1411
|
+
`);
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1414
|
+
`);
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
885
1418
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1419
|
+
function createManifestCommand() {
|
|
1420
|
+
const manifestCmd = new Command("manifest").description(
|
|
1421
|
+
"Query and explore the component manifest"
|
|
1422
|
+
);
|
|
1423
|
+
registerList(manifestCmd);
|
|
1424
|
+
registerGet(manifestCmd);
|
|
1425
|
+
registerQuery(manifestCmd);
|
|
1426
|
+
registerGenerate(manifestCmd);
|
|
1427
|
+
return manifestCmd;
|
|
891
1428
|
}
|
|
892
1429
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
893
1430
|
function buildHookInstrumentationScript() {
|
|
@@ -1527,22 +2064,22 @@ Available: ${available}`
|
|
|
1527
2064
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1528
2065
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1529
2066
|
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1530
|
-
var
|
|
1531
|
-
async function
|
|
1532
|
-
if (
|
|
1533
|
-
|
|
2067
|
+
var _pool2 = null;
|
|
2068
|
+
async function getPool2() {
|
|
2069
|
+
if (_pool2 === null) {
|
|
2070
|
+
_pool2 = new BrowserPool({
|
|
1534
2071
|
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1535
2072
|
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1536
2073
|
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1537
2074
|
});
|
|
1538
|
-
await
|
|
2075
|
+
await _pool2.init();
|
|
1539
2076
|
}
|
|
1540
|
-
return
|
|
2077
|
+
return _pool2;
|
|
1541
2078
|
}
|
|
1542
|
-
async function
|
|
1543
|
-
if (
|
|
1544
|
-
await
|
|
1545
|
-
|
|
2079
|
+
async function shutdownPool2() {
|
|
2080
|
+
if (_pool2 !== null) {
|
|
2081
|
+
await _pool2.close();
|
|
2082
|
+
_pool2 = null;
|
|
1546
2083
|
}
|
|
1547
2084
|
}
|
|
1548
2085
|
function mapNodeType(node) {
|
|
@@ -1754,7 +2291,7 @@ function formatInstrumentTree(root, showProviderDepth = false) {
|
|
|
1754
2291
|
}
|
|
1755
2292
|
async function runInstrumentTree(options) {
|
|
1756
2293
|
const { componentName, filePath } = options;
|
|
1757
|
-
const pool = await
|
|
2294
|
+
const pool = await getPool2();
|
|
1758
2295
|
const slot = await pool.acquire();
|
|
1759
2296
|
const { page } = slot;
|
|
1760
2297
|
try {
|
|
@@ -1851,7 +2388,7 @@ Available: ${available}`
|
|
|
1851
2388
|
providerDepth: opts.providerDepth,
|
|
1852
2389
|
wastedRenders: opts.wastedRenders
|
|
1853
2390
|
});
|
|
1854
|
-
await
|
|
2391
|
+
await shutdownPool2();
|
|
1855
2392
|
const fmt = resolveFormat2(opts.format);
|
|
1856
2393
|
if (fmt === "json") {
|
|
1857
2394
|
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
@@ -1862,7 +2399,7 @@ Available: ${available}`
|
|
|
1862
2399
|
`);
|
|
1863
2400
|
}
|
|
1864
2401
|
} catch (err) {
|
|
1865
|
-
await
|
|
2402
|
+
await shutdownPool2();
|
|
1866
2403
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1867
2404
|
`);
|
|
1868
2405
|
process.exit(1);
|
|
@@ -2180,22 +2717,22 @@ async function replayInteraction2(page, steps) {
|
|
|
2180
2717
|
}
|
|
2181
2718
|
}
|
|
2182
2719
|
}
|
|
2183
|
-
var
|
|
2184
|
-
async function
|
|
2185
|
-
if (
|
|
2186
|
-
|
|
2720
|
+
var _pool3 = null;
|
|
2721
|
+
async function getPool3() {
|
|
2722
|
+
if (_pool3 === null) {
|
|
2723
|
+
_pool3 = new BrowserPool({
|
|
2187
2724
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
2188
2725
|
viewportWidth: 1280,
|
|
2189
2726
|
viewportHeight: 800
|
|
2190
2727
|
});
|
|
2191
|
-
await
|
|
2728
|
+
await _pool3.init();
|
|
2192
2729
|
}
|
|
2193
|
-
return
|
|
2730
|
+
return _pool3;
|
|
2194
2731
|
}
|
|
2195
|
-
async function
|
|
2196
|
-
if (
|
|
2197
|
-
await
|
|
2198
|
-
|
|
2732
|
+
async function shutdownPool3() {
|
|
2733
|
+
if (_pool3 !== null) {
|
|
2734
|
+
await _pool3.close();
|
|
2735
|
+
_pool3 = null;
|
|
2199
2736
|
}
|
|
2200
2737
|
}
|
|
2201
2738
|
async function analyzeRenders(options) {
|
|
@@ -2212,7 +2749,7 @@ Available: ${available}`
|
|
|
2212
2749
|
const rootDir = process.cwd();
|
|
2213
2750
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
2214
2751
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
2215
|
-
const pool = await
|
|
2752
|
+
const pool = await getPool3();
|
|
2216
2753
|
const slot = await pool.acquire();
|
|
2217
2754
|
const { page } = slot;
|
|
2218
2755
|
const startMs = performance.now();
|
|
@@ -2320,7 +2857,7 @@ function createInstrumentRendersCommand() {
|
|
|
2320
2857
|
interaction,
|
|
2321
2858
|
manifestPath: opts.manifest
|
|
2322
2859
|
});
|
|
2323
|
-
await
|
|
2860
|
+
await shutdownPool3();
|
|
2324
2861
|
if (opts.json || !isTTY()) {
|
|
2325
2862
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2326
2863
|
`);
|
|
@@ -2329,7 +2866,7 @@ function createInstrumentRendersCommand() {
|
|
|
2329
2866
|
`);
|
|
2330
2867
|
}
|
|
2331
2868
|
} catch (err) {
|
|
2332
|
-
await
|
|
2869
|
+
await shutdownPool3();
|
|
2333
2870
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2334
2871
|
`);
|
|
2335
2872
|
process.exit(1);
|
|
@@ -2377,151 +2914,34 @@ async function browserCapture(options) {
|
|
|
2377
2914
|
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
2378
2915
|
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
2379
2916
|
}
|
|
2380
|
-
const report = { ...raw, route: null };
|
|
2381
|
-
return { report };
|
|
2382
|
-
} finally {
|
|
2383
|
-
await browser.close();
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
function writeReportToFile(report, outputPath, pretty) {
|
|
2387
|
-
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2388
|
-
writeFileSync(outputPath, json, "utf-8");
|
|
2389
|
-
}
|
|
2390
|
-
var CONFIG_FILENAMES = [
|
|
2391
|
-
".reactscope/config.json",
|
|
2392
|
-
".reactscope/config.js",
|
|
2393
|
-
".reactscope/config.mjs"
|
|
2394
|
-
];
|
|
2395
|
-
var STYLE_ENTRY_CANDIDATES = [
|
|
2396
|
-
"src/index.css",
|
|
2397
|
-
"src/globals.css",
|
|
2398
|
-
"app/globals.css",
|
|
2399
|
-
"app/index.css",
|
|
2400
|
-
"styles/index.css",
|
|
2401
|
-
"index.css"
|
|
2402
|
-
];
|
|
2403
|
-
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2404
|
-
var compilerCache = null;
|
|
2405
|
-
function getCachedBuild(cwd) {
|
|
2406
|
-
if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
|
|
2407
|
-
return compilerCache.build;
|
|
2408
|
-
}
|
|
2409
|
-
return null;
|
|
2410
|
-
}
|
|
2411
|
-
function findStylesEntry(cwd) {
|
|
2412
|
-
for (const name of CONFIG_FILENAMES) {
|
|
2413
|
-
const p = resolve(cwd, name);
|
|
2414
|
-
if (!existsSync(p)) continue;
|
|
2415
|
-
try {
|
|
2416
|
-
if (name.endsWith(".json")) {
|
|
2417
|
-
const raw = readFileSync(p, "utf-8");
|
|
2418
|
-
const data = JSON.parse(raw);
|
|
2419
|
-
const scope = data.scope;
|
|
2420
|
-
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2421
|
-
if (typeof entry === "string") {
|
|
2422
|
-
const full = resolve(cwd, entry);
|
|
2423
|
-
if (existsSync(full)) return full;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
} catch {
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
const pkgPath = resolve(cwd, "package.json");
|
|
2430
|
-
if (existsSync(pkgPath)) {
|
|
2431
|
-
try {
|
|
2432
|
-
const raw = readFileSync(pkgPath, "utf-8");
|
|
2433
|
-
const pkg = JSON.parse(raw);
|
|
2434
|
-
const entry = pkg.scope?.stylesEntry;
|
|
2435
|
-
if (typeof entry === "string") {
|
|
2436
|
-
const full = resolve(cwd, entry);
|
|
2437
|
-
if (existsSync(full)) return full;
|
|
2438
|
-
}
|
|
2439
|
-
} catch {
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2443
|
-
const full = resolve(cwd, candidate);
|
|
2444
|
-
if (existsSync(full)) {
|
|
2445
|
-
try {
|
|
2446
|
-
const content = readFileSync(full, "utf-8");
|
|
2447
|
-
if (TAILWIND_IMPORT.test(content)) return full;
|
|
2448
|
-
} catch {
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
return null;
|
|
2453
|
-
}
|
|
2454
|
-
async function getTailwindCompiler(cwd) {
|
|
2455
|
-
const cached = getCachedBuild(cwd);
|
|
2456
|
-
if (cached !== null) return cached;
|
|
2457
|
-
const entryPath = findStylesEntry(cwd);
|
|
2458
|
-
if (entryPath === null) return null;
|
|
2459
|
-
let compile;
|
|
2460
|
-
try {
|
|
2461
|
-
const require2 = createRequire(resolve(cwd, "package.json"));
|
|
2462
|
-
const tailwind = require2("tailwindcss");
|
|
2463
|
-
const fn = tailwind.compile;
|
|
2464
|
-
if (typeof fn !== "function") return null;
|
|
2465
|
-
compile = fn;
|
|
2466
|
-
} catch {
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
const entryContent = readFileSync(entryPath, "utf-8");
|
|
2470
|
-
const loadStylesheet = async (id, base) => {
|
|
2471
|
-
if (id === "tailwindcss") {
|
|
2472
|
-
const nodeModules = resolve(cwd, "node_modules");
|
|
2473
|
-
const tailwindCssPath = resolve(nodeModules, "tailwindcss", "index.css");
|
|
2474
|
-
if (!existsSync(tailwindCssPath)) {
|
|
2475
|
-
throw new Error(
|
|
2476
|
-
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
2477
|
-
);
|
|
2478
|
-
}
|
|
2479
|
-
const content = readFileSync(tailwindCssPath, "utf-8");
|
|
2480
|
-
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2481
|
-
}
|
|
2482
|
-
const full = resolve(base, id);
|
|
2483
|
-
if (existsSync(full)) {
|
|
2484
|
-
const content = readFileSync(full, "utf-8");
|
|
2485
|
-
return { path: full, base: resolve(full, ".."), content };
|
|
2486
|
-
}
|
|
2487
|
-
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2488
|
-
};
|
|
2489
|
-
const result = await compile(entryContent, {
|
|
2490
|
-
base: cwd,
|
|
2491
|
-
from: entryPath,
|
|
2492
|
-
loadStylesheet
|
|
2493
|
-
});
|
|
2494
|
-
const build2 = result.build.bind(result);
|
|
2495
|
-
compilerCache = { cwd, build: build2 };
|
|
2496
|
-
return build2;
|
|
2917
|
+
const report = { ...raw, route: null };
|
|
2918
|
+
return { report };
|
|
2919
|
+
} finally {
|
|
2920
|
+
await browser.close();
|
|
2921
|
+
}
|
|
2497
2922
|
}
|
|
2498
|
-
|
|
2499
|
-
const
|
|
2500
|
-
|
|
2501
|
-
const deduped = [...new Set(classes)].filter(Boolean);
|
|
2502
|
-
if (deduped.length === 0) return null;
|
|
2503
|
-
return build2(deduped);
|
|
2923
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
2924
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2925
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
2504
2926
|
}
|
|
2505
|
-
|
|
2506
|
-
// src/render-commands.ts
|
|
2507
2927
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2508
2928
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2509
|
-
var
|
|
2510
|
-
async function
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2929
|
+
var _pool4 = null;
|
|
2930
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
2931
|
+
if (_pool4 === null) {
|
|
2932
|
+
_pool4 = new BrowserPool({
|
|
2513
2933
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2514
2934
|
viewportWidth,
|
|
2515
2935
|
viewportHeight
|
|
2516
2936
|
});
|
|
2517
|
-
await
|
|
2937
|
+
await _pool4.init();
|
|
2518
2938
|
}
|
|
2519
|
-
return
|
|
2939
|
+
return _pool4;
|
|
2520
2940
|
}
|
|
2521
|
-
async function
|
|
2522
|
-
if (
|
|
2523
|
-
await
|
|
2524
|
-
|
|
2941
|
+
async function shutdownPool4() {
|
|
2942
|
+
if (_pool4 !== null) {
|
|
2943
|
+
await _pool4.close();
|
|
2944
|
+
_pool4 = null;
|
|
2525
2945
|
}
|
|
2526
2946
|
}
|
|
2527
2947
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2532,7 +2952,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2532
2952
|
_satori: satori,
|
|
2533
2953
|
async renderCell(props, _complexityClass) {
|
|
2534
2954
|
const startMs = performance.now();
|
|
2535
|
-
const pool = await
|
|
2955
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2536
2956
|
const htmlHarness = await buildComponentHarness(
|
|
2537
2957
|
filePath,
|
|
2538
2958
|
componentName,
|
|
@@ -2668,7 +3088,7 @@ Available: ${available}`
|
|
|
2668
3088
|
}
|
|
2669
3089
|
}
|
|
2670
3090
|
);
|
|
2671
|
-
await
|
|
3091
|
+
await shutdownPool4();
|
|
2672
3092
|
if (outcome.crashed) {
|
|
2673
3093
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2674
3094
|
`);
|
|
@@ -2716,7 +3136,7 @@ Available: ${available}`
|
|
|
2716
3136
|
);
|
|
2717
3137
|
}
|
|
2718
3138
|
} catch (err) {
|
|
2719
|
-
await
|
|
3139
|
+
await shutdownPool4();
|
|
2720
3140
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2721
3141
|
`);
|
|
2722
3142
|
process.exit(1);
|
|
@@ -2801,14 +3221,14 @@ Available: ${available}`
|
|
|
2801
3221
|
concurrency
|
|
2802
3222
|
});
|
|
2803
3223
|
const result = await matrix.render();
|
|
2804
|
-
await
|
|
3224
|
+
await shutdownPool4();
|
|
2805
3225
|
process.stderr.write(
|
|
2806
3226
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2807
3227
|
`
|
|
2808
3228
|
);
|
|
2809
3229
|
if (opts.sprite !== void 0) {
|
|
2810
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2811
|
-
const gen = new
|
|
3230
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3231
|
+
const gen = new SpriteSheetGenerator2();
|
|
2812
3232
|
const sheet = await gen.generate(result);
|
|
2813
3233
|
const spritePath = resolve(process.cwd(), opts.sprite);
|
|
2814
3234
|
writeFileSync(spritePath, sheet.png);
|
|
@@ -2817,8 +3237,8 @@ Available: ${available}`
|
|
|
2817
3237
|
}
|
|
2818
3238
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2819
3239
|
if (fmt === "file") {
|
|
2820
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2821
|
-
const gen = new
|
|
3240
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3241
|
+
const gen = new SpriteSheetGenerator2();
|
|
2822
3242
|
const sheet = await gen.generate(result);
|
|
2823
3243
|
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2824
3244
|
mkdirSync(dir, { recursive: true });
|
|
@@ -2835,8 +3255,8 @@ Available: ${available}`
|
|
|
2835
3255
|
} else if (fmt === "png") {
|
|
2836
3256
|
if (opts.sprite !== void 0) {
|
|
2837
3257
|
} else {
|
|
2838
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2839
|
-
const gen = new
|
|
3258
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3259
|
+
const gen = new SpriteSheetGenerator2();
|
|
2840
3260
|
const sheet = await gen.generate(result);
|
|
2841
3261
|
process.stdout.write(sheet.png);
|
|
2842
3262
|
}
|
|
@@ -2846,7 +3266,7 @@ Available: ${available}`
|
|
|
2846
3266
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2847
3267
|
}
|
|
2848
3268
|
} catch (err) {
|
|
2849
|
-
await
|
|
3269
|
+
await shutdownPool4();
|
|
2850
3270
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2851
3271
|
`);
|
|
2852
3272
|
process.exit(1);
|
|
@@ -2943,13 +3363,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2943
3363
|
workers.push(worker());
|
|
2944
3364
|
}
|
|
2945
3365
|
await Promise.all(workers);
|
|
2946
|
-
await
|
|
3366
|
+
await shutdownPool4();
|
|
2947
3367
|
process.stderr.write("\n");
|
|
2948
3368
|
const summary = formatSummaryText(results, outputDir);
|
|
2949
3369
|
process.stderr.write(`${summary}
|
|
2950
3370
|
`);
|
|
2951
3371
|
} catch (err) {
|
|
2952
|
-
await
|
|
3372
|
+
await shutdownPool4();
|
|
2953
3373
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2954
3374
|
`);
|
|
2955
3375
|
process.exit(1);
|
|
@@ -2991,26 +3411,26 @@ function createRenderCommand() {
|
|
|
2991
3411
|
return renderCmd;
|
|
2992
3412
|
}
|
|
2993
3413
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2994
|
-
var
|
|
2995
|
-
async function
|
|
2996
|
-
if (
|
|
2997
|
-
|
|
3414
|
+
var _pool5 = null;
|
|
3415
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3416
|
+
if (_pool5 === null) {
|
|
3417
|
+
_pool5 = new BrowserPool({
|
|
2998
3418
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2999
3419
|
viewportWidth,
|
|
3000
3420
|
viewportHeight
|
|
3001
3421
|
});
|
|
3002
|
-
await
|
|
3422
|
+
await _pool5.init();
|
|
3003
3423
|
}
|
|
3004
|
-
return
|
|
3424
|
+
return _pool5;
|
|
3005
3425
|
}
|
|
3006
|
-
async function
|
|
3007
|
-
if (
|
|
3008
|
-
await
|
|
3009
|
-
|
|
3426
|
+
async function shutdownPool5() {
|
|
3427
|
+
if (_pool5 !== null) {
|
|
3428
|
+
await _pool5.close();
|
|
3429
|
+
_pool5 = null;
|
|
3010
3430
|
}
|
|
3011
3431
|
}
|
|
3012
|
-
async function
|
|
3013
|
-
const pool = await
|
|
3432
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3433
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3014
3434
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3015
3435
|
const slot = await pool.acquire();
|
|
3016
3436
|
const { page } = slot;
|
|
@@ -3100,7 +3520,7 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
|
|
|
3100
3520
|
pool.release(slot);
|
|
3101
3521
|
}
|
|
3102
3522
|
}
|
|
3103
|
-
function
|
|
3523
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3104
3524
|
const flat = {};
|
|
3105
3525
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3106
3526
|
Object.assign(flat, styles);
|
|
@@ -3143,12 +3563,12 @@ async function runBaseline(options = {}) {
|
|
|
3143
3563
|
mkdirSync(rendersDir, { recursive: true });
|
|
3144
3564
|
let manifest;
|
|
3145
3565
|
if (manifestPath !== void 0) {
|
|
3146
|
-
const { readFileSync:
|
|
3566
|
+
const { readFileSync: readFileSync11 } = await import('fs');
|
|
3147
3567
|
const absPath = resolve(rootDir, manifestPath);
|
|
3148
3568
|
if (!existsSync(absPath)) {
|
|
3149
3569
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3150
3570
|
}
|
|
3151
|
-
manifest = JSON.parse(
|
|
3571
|
+
manifest = JSON.parse(readFileSync11(absPath, "utf-8"));
|
|
3152
3572
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3153
3573
|
`);
|
|
3154
3574
|
} else {
|
|
@@ -3202,7 +3622,7 @@ async function runBaseline(options = {}) {
|
|
|
3202
3622
|
if (descriptor === void 0) return;
|
|
3203
3623
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3204
3624
|
const outcome = await safeRender(
|
|
3205
|
-
() =>
|
|
3625
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3206
3626
|
{
|
|
3207
3627
|
props: {},
|
|
3208
3628
|
sourceLocation: {
|
|
@@ -3244,7 +3664,7 @@ async function runBaseline(options = {}) {
|
|
|
3244
3664
|
JSON.stringify(jsonOutput, null, 2),
|
|
3245
3665
|
"utf-8"
|
|
3246
3666
|
);
|
|
3247
|
-
computedStylesMap.set(name,
|
|
3667
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3248
3668
|
};
|
|
3249
3669
|
const worker = async () => {
|
|
3250
3670
|
while (nextIdx < componentNames.length) {
|
|
@@ -3260,7 +3680,7 @@ async function runBaseline(options = {}) {
|
|
|
3260
3680
|
workers.push(worker());
|
|
3261
3681
|
}
|
|
3262
3682
|
await Promise.all(workers);
|
|
3263
|
-
await
|
|
3683
|
+
await shutdownPool5();
|
|
3264
3684
|
if (isTTY()) {
|
|
3265
3685
|
process.stderr.write("\n");
|
|
3266
3686
|
}
|
|
@@ -3316,31 +3736,31 @@ function loadBaselineCompliance(baselineDir) {
|
|
|
3316
3736
|
const raw = JSON.parse(readFileSync(compliancePath, "utf-8"));
|
|
3317
3737
|
return raw;
|
|
3318
3738
|
}
|
|
3319
|
-
function
|
|
3739
|
+
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3320
3740
|
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3321
3741
|
if (!existsSync(jsonPath)) return null;
|
|
3322
3742
|
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
3323
3743
|
}
|
|
3324
|
-
var
|
|
3325
|
-
async function
|
|
3326
|
-
if (
|
|
3327
|
-
|
|
3744
|
+
var _pool6 = null;
|
|
3745
|
+
async function getPool6(viewportWidth, viewportHeight) {
|
|
3746
|
+
if (_pool6 === null) {
|
|
3747
|
+
_pool6 = new BrowserPool({
|
|
3328
3748
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3329
3749
|
viewportWidth,
|
|
3330
3750
|
viewportHeight
|
|
3331
3751
|
});
|
|
3332
|
-
await
|
|
3752
|
+
await _pool6.init();
|
|
3333
3753
|
}
|
|
3334
|
-
return
|
|
3754
|
+
return _pool6;
|
|
3335
3755
|
}
|
|
3336
|
-
async function
|
|
3337
|
-
if (
|
|
3338
|
-
await
|
|
3339
|
-
|
|
3756
|
+
async function shutdownPool6() {
|
|
3757
|
+
if (_pool6 !== null) {
|
|
3758
|
+
await _pool6.close();
|
|
3759
|
+
_pool6 = null;
|
|
3340
3760
|
}
|
|
3341
3761
|
}
|
|
3342
|
-
async function
|
|
3343
|
-
const pool = await
|
|
3762
|
+
async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3763
|
+
const pool = await getPool6(viewportWidth, viewportHeight);
|
|
3344
3764
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3345
3765
|
const slot = await pool.acquire();
|
|
3346
3766
|
const { page } = slot;
|
|
@@ -3430,7 +3850,7 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
|
|
|
3430
3850
|
pool.release(slot);
|
|
3431
3851
|
}
|
|
3432
3852
|
}
|
|
3433
|
-
function
|
|
3853
|
+
function extractComputedStyles3(computedStylesRaw) {
|
|
3434
3854
|
const flat = {};
|
|
3435
3855
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3436
3856
|
Object.assign(flat, styles);
|
|
@@ -3546,7 +3966,7 @@ async function runDiff(options = {}) {
|
|
|
3546
3966
|
if (descriptor === void 0) return;
|
|
3547
3967
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3548
3968
|
const outcome = await safeRender(
|
|
3549
|
-
() =>
|
|
3969
|
+
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3550
3970
|
{
|
|
3551
3971
|
props: {},
|
|
3552
3972
|
sourceLocation: {
|
|
@@ -3571,7 +3991,7 @@ async function runDiff(options = {}) {
|
|
|
3571
3991
|
height: result.height,
|
|
3572
3992
|
renderTimeMs: result.renderTimeMs
|
|
3573
3993
|
});
|
|
3574
|
-
computedStylesMap.set(name,
|
|
3994
|
+
computedStylesMap.set(name, extractComputedStyles3(result.computedStyles));
|
|
3575
3995
|
};
|
|
3576
3996
|
if (total > 0) {
|
|
3577
3997
|
const worker = async () => {
|
|
@@ -3589,7 +4009,7 @@ async function runDiff(options = {}) {
|
|
|
3589
4009
|
}
|
|
3590
4010
|
await Promise.all(workers);
|
|
3591
4011
|
}
|
|
3592
|
-
await
|
|
4012
|
+
await shutdownPool6();
|
|
3593
4013
|
if (isTTY() && total > 0) {
|
|
3594
4014
|
process.stderr.write("\n");
|
|
3595
4015
|
}
|
|
@@ -3600,7 +4020,7 @@ async function runDiff(options = {}) {
|
|
|
3600
4020
|
for (const name of componentNames) {
|
|
3601
4021
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3602
4022
|
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3603
|
-
const baselineMeta =
|
|
4023
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3604
4024
|
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3605
4025
|
const failed = renderFailures.has(name);
|
|
3606
4026
|
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
@@ -3620,7 +4040,7 @@ async function runDiff(options = {}) {
|
|
|
3620
4040
|
}
|
|
3621
4041
|
for (const name of removedNames) {
|
|
3622
4042
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3623
|
-
const baselineMeta =
|
|
4043
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3624
4044
|
entries.push({
|
|
3625
4045
|
name,
|
|
3626
4046
|
status: "removed",
|
|
@@ -4060,10 +4480,178 @@ function buildStructuredReport(report) {
|
|
|
4060
4480
|
route: report.route?.pattern ?? null
|
|
4061
4481
|
};
|
|
4062
4482
|
}
|
|
4483
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4484
|
+
function loadStylesFile(stylesPath) {
|
|
4485
|
+
const absPath = resolve(process.cwd(), stylesPath);
|
|
4486
|
+
if (!existsSync(absPath)) {
|
|
4487
|
+
throw new Error(
|
|
4488
|
+
`Compliance styles file not found at ${absPath}.
|
|
4489
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4490
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
4494
|
+
let parsed;
|
|
4495
|
+
try {
|
|
4496
|
+
parsed = JSON.parse(raw);
|
|
4497
|
+
} catch (err) {
|
|
4498
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4499
|
+
}
|
|
4500
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4501
|
+
throw new Error(
|
|
4502
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4503
|
+
);
|
|
4504
|
+
}
|
|
4505
|
+
return parsed;
|
|
4506
|
+
}
|
|
4507
|
+
function categoryForProperty(property) {
|
|
4508
|
+
const lower = property.toLowerCase();
|
|
4509
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4510
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4511
|
+
return "color";
|
|
4512
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4513
|
+
return "spacing";
|
|
4514
|
+
if (lower.includes("border")) return "border";
|
|
4515
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4516
|
+
return "typography";
|
|
4517
|
+
return "spacing";
|
|
4518
|
+
}
|
|
4519
|
+
function buildCategorySummary(batch) {
|
|
4520
|
+
const cats = {
|
|
4521
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4522
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4523
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4524
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4525
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4526
|
+
};
|
|
4527
|
+
for (const report of Object.values(batch.components)) {
|
|
4528
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4529
|
+
const cat = categoryForProperty(property);
|
|
4530
|
+
const summary = cats[cat];
|
|
4531
|
+
if (summary === void 0) continue;
|
|
4532
|
+
summary.total++;
|
|
4533
|
+
if (result.status === "on_system") {
|
|
4534
|
+
summary.onSystem++;
|
|
4535
|
+
} else {
|
|
4536
|
+
summary.offSystem++;
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
for (const summary of Object.values(cats)) {
|
|
4541
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4542
|
+
}
|
|
4543
|
+
return cats;
|
|
4544
|
+
}
|
|
4545
|
+
function collectOffenders(batch, limit = 10) {
|
|
4546
|
+
const offenders = [];
|
|
4547
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4548
|
+
name,
|
|
4549
|
+
report,
|
|
4550
|
+
offSystemCount: report.offSystem
|
|
4551
|
+
}));
|
|
4552
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4553
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4554
|
+
if (offSystemCount === 0) continue;
|
|
4555
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4556
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4557
|
+
offenders.push({
|
|
4558
|
+
component: name,
|
|
4559
|
+
property,
|
|
4560
|
+
value: result.value,
|
|
4561
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4562
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4563
|
+
offSystemCount
|
|
4564
|
+
});
|
|
4565
|
+
if (offenders.length >= limit) break;
|
|
4566
|
+
}
|
|
4567
|
+
if (offenders.length >= limit) break;
|
|
4568
|
+
}
|
|
4569
|
+
return offenders;
|
|
4570
|
+
}
|
|
4571
|
+
function formatPct(n) {
|
|
4572
|
+
return `${Math.round(n * 100)}%`;
|
|
4573
|
+
}
|
|
4574
|
+
function truncate(s, max) {
|
|
4575
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4576
|
+
}
|
|
4577
|
+
function formatComplianceReport(batch, threshold) {
|
|
4578
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4579
|
+
const lines = [];
|
|
4580
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4581
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4582
|
+
lines.push("");
|
|
4583
|
+
const cats = buildCategorySummary(batch);
|
|
4584
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4585
|
+
if (catEntries.length > 0) {
|
|
4586
|
+
lines.push("By category:");
|
|
4587
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4588
|
+
for (const [cat, summary] of catEntries) {
|
|
4589
|
+
const label = cat.padEnd(catWidth);
|
|
4590
|
+
lines.push(
|
|
4591
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4594
|
+
lines.push("");
|
|
4595
|
+
}
|
|
4596
|
+
const offenders = collectOffenders(batch);
|
|
4597
|
+
if (offenders.length > 0) {
|
|
4598
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4599
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4600
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4601
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4602
|
+
for (const offender of offenders) {
|
|
4603
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4604
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4605
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4606
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4607
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4608
|
+
}
|
|
4609
|
+
} else {
|
|
4610
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4611
|
+
}
|
|
4612
|
+
return lines.join("\n");
|
|
4613
|
+
}
|
|
4614
|
+
function registerCompliance(tokensCmd) {
|
|
4615
|
+
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) => {
|
|
4616
|
+
try {
|
|
4617
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4618
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4619
|
+
const resolver = new TokenResolver(tokens);
|
|
4620
|
+
const engine = new ComplianceEngine(resolver);
|
|
4621
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4622
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4623
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4624
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4625
|
+
componentMap.set(name, styles);
|
|
4626
|
+
}
|
|
4627
|
+
if (componentMap.size === 0) {
|
|
4628
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4629
|
+
`);
|
|
4630
|
+
}
|
|
4631
|
+
const batch = engine.auditBatch(componentMap);
|
|
4632
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4633
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4634
|
+
if (useJson) {
|
|
4635
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4636
|
+
`);
|
|
4637
|
+
} else {
|
|
4638
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4639
|
+
`);
|
|
4640
|
+
}
|
|
4641
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4642
|
+
process.exit(1);
|
|
4643
|
+
}
|
|
4644
|
+
} catch (err) {
|
|
4645
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4646
|
+
`);
|
|
4647
|
+
process.exit(1);
|
|
4648
|
+
}
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4063
4651
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
4064
4652
|
var CONFIG_FILE = "reactscope.config.json";
|
|
4065
4653
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
4066
|
-
function
|
|
4654
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
4067
4655
|
if (fileFlag !== void 0) {
|
|
4068
4656
|
return resolve(process.cwd(), fileFlag);
|
|
4069
4657
|
}
|
|
@@ -4097,7 +4685,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
4097
4685
|
}
|
|
4098
4686
|
const format = opts.format;
|
|
4099
4687
|
try {
|
|
4100
|
-
const filePath =
|
|
4688
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4101
4689
|
if (!existsSync(filePath)) {
|
|
4102
4690
|
throw new Error(
|
|
4103
4691
|
`Token file not found at ${filePath}.
|
|
@@ -4161,6 +4749,311 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
4161
4749
|
}
|
|
4162
4750
|
);
|
|
4163
4751
|
}
|
|
4752
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4753
|
+
var SEVERITY_EMOJI = {
|
|
4754
|
+
none: "\u25CB",
|
|
4755
|
+
subtle: "\u25D4",
|
|
4756
|
+
moderate: "\u25D1",
|
|
4757
|
+
significant: "\u25CF"
|
|
4758
|
+
};
|
|
4759
|
+
function formatImpactReport(report) {
|
|
4760
|
+
const lines = [];
|
|
4761
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4762
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4763
|
+
if (report.components.length === 0) {
|
|
4764
|
+
lines.push("");
|
|
4765
|
+
lines.push("No components reference this token.");
|
|
4766
|
+
return lines.join("\n");
|
|
4767
|
+
}
|
|
4768
|
+
lines.push("");
|
|
4769
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4770
|
+
const propWidth = Math.max(
|
|
4771
|
+
8,
|
|
4772
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4773
|
+
);
|
|
4774
|
+
for (const comp of report.components) {
|
|
4775
|
+
for (const property of comp.affectedProperties) {
|
|
4776
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4777
|
+
const prop = property.padEnd(propWidth);
|
|
4778
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4779
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
lines.push("");
|
|
4783
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4784
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4785
|
+
lines.push(
|
|
4786
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4787
|
+
);
|
|
4788
|
+
if (report.colorDelta !== void 0) {
|
|
4789
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4790
|
+
}
|
|
4791
|
+
return lines.join("\n");
|
|
4792
|
+
}
|
|
4793
|
+
function formatImpactSummary(report) {
|
|
4794
|
+
if (report.components.length === 0) {
|
|
4795
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4796
|
+
}
|
|
4797
|
+
const parts = report.components.map(
|
|
4798
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4799
|
+
);
|
|
4800
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4801
|
+
}
|
|
4802
|
+
function registerImpact(tokensCmd) {
|
|
4803
|
+
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(
|
|
4804
|
+
(tokenPath, opts) => {
|
|
4805
|
+
try {
|
|
4806
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4807
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4808
|
+
const resolver = new TokenResolver(tokens);
|
|
4809
|
+
const engine = new ComplianceEngine(resolver);
|
|
4810
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4811
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4812
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4813
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4814
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4815
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4816
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4817
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4818
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4819
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4820
|
+
if (useJson) {
|
|
4821
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4822
|
+
`);
|
|
4823
|
+
} else {
|
|
4824
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4825
|
+
`);
|
|
4826
|
+
if (isTTY()) {
|
|
4827
|
+
process.stdout.write(`
|
|
4828
|
+
${formatImpactSummary(report)}
|
|
4829
|
+
`);
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
} catch (err) {
|
|
4833
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4834
|
+
`);
|
|
4835
|
+
process.exit(1);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
);
|
|
4839
|
+
}
|
|
4840
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4841
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4842
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4843
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4844
|
+
const htmlHarness = await buildComponentHarness(
|
|
4845
|
+
filePath,
|
|
4846
|
+
componentName,
|
|
4847
|
+
{},
|
|
4848
|
+
// no props
|
|
4849
|
+
vpWidth,
|
|
4850
|
+
cssOverride
|
|
4851
|
+
// injected as <style>
|
|
4852
|
+
);
|
|
4853
|
+
const pool = new BrowserPool({
|
|
4854
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4855
|
+
viewportWidth: vpWidth,
|
|
4856
|
+
viewportHeight: vpHeight
|
|
4857
|
+
});
|
|
4858
|
+
await pool.init();
|
|
4859
|
+
const slot = await pool.acquire();
|
|
4860
|
+
const { page } = slot;
|
|
4861
|
+
try {
|
|
4862
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4863
|
+
await page.waitForFunction(
|
|
4864
|
+
() => {
|
|
4865
|
+
const w = window;
|
|
4866
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4867
|
+
},
|
|
4868
|
+
{ timeout: timeoutMs }
|
|
4869
|
+
);
|
|
4870
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
4871
|
+
const bb = await rootLocator.boundingBox();
|
|
4872
|
+
const PAD = 16;
|
|
4873
|
+
const MIN_W = 320;
|
|
4874
|
+
const MIN_H = 120;
|
|
4875
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
4876
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
4877
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
4878
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
4879
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
4880
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
4881
|
+
const screenshot = await page.screenshot({
|
|
4882
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
4883
|
+
type: "png"
|
|
4884
|
+
});
|
|
4885
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
4886
|
+
} finally {
|
|
4887
|
+
pool.release(slot);
|
|
4888
|
+
await pool.close().catch(() => void 0);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
function registerPreview(tokensCmd) {
|
|
4892
|
+
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(
|
|
4893
|
+
async (tokenPath, opts) => {
|
|
4894
|
+
try {
|
|
4895
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4896
|
+
const { tokens } = loadTokens(tokenFilePath);
|
|
4897
|
+
const resolver = new TokenResolver(tokens);
|
|
4898
|
+
const engine = new ComplianceEngine(resolver);
|
|
4899
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
4900
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4901
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4902
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4903
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4904
|
+
const analyzer = new ImpactAnalyzer(resolver, complianceReports);
|
|
4905
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4906
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
4907
|
+
if (impactReport.components.length === 0) {
|
|
4908
|
+
process.stdout.write(
|
|
4909
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
4910
|
+
`
|
|
4911
|
+
);
|
|
4912
|
+
return;
|
|
4913
|
+
}
|
|
4914
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
4915
|
+
process.stderr.write(
|
|
4916
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
4917
|
+
`
|
|
4918
|
+
);
|
|
4919
|
+
const manifest = loadManifest(opts.manifest);
|
|
4920
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
4921
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
4922
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
4923
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
4924
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
4925
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
4926
|
+
const renders = [];
|
|
4927
|
+
for (const componentName of affectedNames) {
|
|
4928
|
+
const descriptor = manifest.components[componentName];
|
|
4929
|
+
if (descriptor === void 0) {
|
|
4930
|
+
process.stderr.write(
|
|
4931
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
4932
|
+
`
|
|
4933
|
+
);
|
|
4934
|
+
continue;
|
|
4935
|
+
}
|
|
4936
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
4937
|
+
`);
|
|
4938
|
+
const before = await renderComponentWithCssOverride(
|
|
4939
|
+
descriptor.filePath,
|
|
4940
|
+
componentName,
|
|
4941
|
+
beforeCss,
|
|
4942
|
+
vpWidth,
|
|
4943
|
+
vpHeight,
|
|
4944
|
+
timeout
|
|
4945
|
+
);
|
|
4946
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
4947
|
+
`);
|
|
4948
|
+
const after = await renderComponentWithCssOverride(
|
|
4949
|
+
descriptor.filePath,
|
|
4950
|
+
componentName,
|
|
4951
|
+
afterCss,
|
|
4952
|
+
vpWidth,
|
|
4953
|
+
vpHeight,
|
|
4954
|
+
timeout
|
|
4955
|
+
);
|
|
4956
|
+
renders.push({ name: componentName, before, after });
|
|
4957
|
+
}
|
|
4958
|
+
if (renders.length === 0) {
|
|
4959
|
+
process.stderr.write(
|
|
4960
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
4961
|
+
);
|
|
4962
|
+
return;
|
|
4963
|
+
}
|
|
4964
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
4965
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
4966
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
4967
|
+
{
|
|
4968
|
+
props: { version: "before", component: r.name },
|
|
4969
|
+
result: {
|
|
4970
|
+
screenshot: r.before.screenshot,
|
|
4971
|
+
width: cellW,
|
|
4972
|
+
height: cellH,
|
|
4973
|
+
renderTimeMs: 0,
|
|
4974
|
+
computedStyles: {}
|
|
4975
|
+
},
|
|
4976
|
+
index: colIdx * 2,
|
|
4977
|
+
axisIndices: [0, colIdx]
|
|
4978
|
+
},
|
|
4979
|
+
{
|
|
4980
|
+
props: { version: "after", component: r.name },
|
|
4981
|
+
result: {
|
|
4982
|
+
screenshot: r.after.screenshot,
|
|
4983
|
+
width: cellW,
|
|
4984
|
+
height: cellH,
|
|
4985
|
+
renderTimeMs: 0,
|
|
4986
|
+
computedStyles: {}
|
|
4987
|
+
},
|
|
4988
|
+
index: colIdx * 2 + 1,
|
|
4989
|
+
axisIndices: [1, colIdx]
|
|
4990
|
+
}
|
|
4991
|
+
]);
|
|
4992
|
+
const matrixResult = {
|
|
4993
|
+
cells,
|
|
4994
|
+
axes: [
|
|
4995
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
4996
|
+
{ name: "version", values: ["before", "after"] }
|
|
4997
|
+
],
|
|
4998
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
4999
|
+
rows: 2,
|
|
5000
|
+
cols: renders.length,
|
|
5001
|
+
stats: {
|
|
5002
|
+
totalCells: cells.length,
|
|
5003
|
+
totalRenderTimeMs: 0,
|
|
5004
|
+
avgRenderTimeMs: 0,
|
|
5005
|
+
minRenderTimeMs: 0,
|
|
5006
|
+
maxRenderTimeMs: 0,
|
|
5007
|
+
wallClockTimeMs: 0
|
|
5008
|
+
}
|
|
5009
|
+
};
|
|
5010
|
+
const generator = new SpriteSheetGenerator({
|
|
5011
|
+
cellPadding: 8,
|
|
5012
|
+
borderWidth: 1,
|
|
5013
|
+
labelHeight: 32,
|
|
5014
|
+
labelWidth: 120
|
|
5015
|
+
});
|
|
5016
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
5017
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
5018
|
+
const outputPath = opts.output ?? resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
5019
|
+
const outputDir = resolve(outputPath, "..");
|
|
5020
|
+
mkdirSync(outputDir, { recursive: true });
|
|
5021
|
+
writeFileSync(outputPath, spriteResult.png);
|
|
5022
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
5023
|
+
if (useJson) {
|
|
5024
|
+
process.stdout.write(
|
|
5025
|
+
`${JSON.stringify(
|
|
5026
|
+
{
|
|
5027
|
+
tokenPath,
|
|
5028
|
+
oldValue: currentValue,
|
|
5029
|
+
newValue: opts.newValue,
|
|
5030
|
+
outputPath,
|
|
5031
|
+
width: spriteResult.width,
|
|
5032
|
+
height: spriteResult.height,
|
|
5033
|
+
components: renders.map((r) => r.name),
|
|
5034
|
+
cells: spriteResult.coordinates.length
|
|
5035
|
+
},
|
|
5036
|
+
null,
|
|
5037
|
+
2
|
|
5038
|
+
)}
|
|
5039
|
+
`
|
|
5040
|
+
);
|
|
5041
|
+
} else {
|
|
5042
|
+
process.stdout.write(
|
|
5043
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
5044
|
+
`
|
|
5045
|
+
);
|
|
5046
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
5047
|
+
`);
|
|
5048
|
+
}
|
|
5049
|
+
} catch (err) {
|
|
5050
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
5051
|
+
`);
|
|
5052
|
+
process.exit(1);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
4164
5057
|
|
|
4165
5058
|
// src/tokens/commands.ts
|
|
4166
5059
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
@@ -4182,7 +5075,7 @@ function buildTable2(headers, rows) {
|
|
|
4182
5075
|
);
|
|
4183
5076
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
4184
5077
|
}
|
|
4185
|
-
function
|
|
5078
|
+
function resolveTokenFilePath(fileFlag) {
|
|
4186
5079
|
if (fileFlag !== void 0) {
|
|
4187
5080
|
return resolve(process.cwd(), fileFlag);
|
|
4188
5081
|
}
|
|
@@ -4245,7 +5138,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
4245
5138
|
function registerGet2(tokensCmd) {
|
|
4246
5139
|
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) => {
|
|
4247
5140
|
try {
|
|
4248
|
-
const filePath =
|
|
5141
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4249
5142
|
const { tokens } = loadTokens(filePath);
|
|
4250
5143
|
const resolver = new TokenResolver(tokens);
|
|
4251
5144
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -4271,7 +5164,7 @@ function registerList2(tokensCmd) {
|
|
|
4271
5164
|
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(
|
|
4272
5165
|
(category, opts) => {
|
|
4273
5166
|
try {
|
|
4274
|
-
const filePath =
|
|
5167
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4275
5168
|
const { tokens } = loadTokens(filePath);
|
|
4276
5169
|
const resolver = new TokenResolver(tokens);
|
|
4277
5170
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -4301,7 +5194,7 @@ function registerSearch(tokensCmd) {
|
|
|
4301
5194
|
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(
|
|
4302
5195
|
(value, opts) => {
|
|
4303
5196
|
try {
|
|
4304
|
-
const filePath =
|
|
5197
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4305
5198
|
const { tokens } = loadTokens(filePath);
|
|
4306
5199
|
const resolver = new TokenResolver(tokens);
|
|
4307
5200
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -4383,7 +5276,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
4383
5276
|
function registerResolve(tokensCmd) {
|
|
4384
5277
|
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) => {
|
|
4385
5278
|
try {
|
|
4386
|
-
const filePath =
|
|
5279
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4387
5280
|
const absFilePath = filePath;
|
|
4388
5281
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
4389
5282
|
const resolver = new TokenResolver(tokens);
|
|
@@ -4420,7 +5313,7 @@ function registerValidate(tokensCmd) {
|
|
|
4420
5313
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
4421
5314
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4422
5315
|
try {
|
|
4423
|
-
const filePath =
|
|
5316
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4424
5317
|
if (!existsSync(filePath)) {
|
|
4425
5318
|
throw new Error(
|
|
4426
5319
|
`Token file not found at ${filePath}.
|
|
@@ -4504,6 +5397,9 @@ function createTokensCommand() {
|
|
|
4504
5397
|
registerResolve(tokensCmd);
|
|
4505
5398
|
registerValidate(tokensCmd);
|
|
4506
5399
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
5400
|
+
registerCompliance(tokensCmd);
|
|
5401
|
+
registerImpact(tokensCmd);
|
|
5402
|
+
registerPreview(tokensCmd);
|
|
4507
5403
|
return tokensCmd;
|
|
4508
5404
|
}
|
|
4509
5405
|
|
|
@@ -4596,6 +5492,7 @@ function createProgram(options = {}) {
|
|
|
4596
5492
|
program.addCommand(createTokensCommand());
|
|
4597
5493
|
program.addCommand(createInstrumentCommand());
|
|
4598
5494
|
program.addCommand(createInitCommand());
|
|
5495
|
+
program.addCommand(createCiCommand());
|
|
4599
5496
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
4600
5497
|
if (existingReportCmd !== void 0) {
|
|
4601
5498
|
registerBaselineSubCommand(existingReportCmd);
|
|
@@ -4604,6 +5501,6 @@ function createProgram(options = {}) {
|
|
|
4604
5501
|
return program;
|
|
4605
5502
|
}
|
|
4606
5503
|
|
|
4607
|
-
export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath, runInit };
|
|
5504
|
+
export { CI_EXIT, createCiCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
|
|
4608
5505
|
//# sourceMappingURL=index.js.map
|
|
4609
5506
|
//# sourceMappingURL=index.js.map
|