@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.cjs
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
|
-
var
|
|
6
|
-
var commander = require('commander');
|
|
5
|
+
var manifest = require('@agent-scope/manifest');
|
|
7
6
|
var render = require('@agent-scope/render');
|
|
7
|
+
var tokens = require('@agent-scope/tokens');
|
|
8
|
+
var commander = require('commander');
|
|
8
9
|
var esbuild = require('esbuild');
|
|
9
|
-
var
|
|
10
|
+
var module$1 = require('module');
|
|
11
|
+
var readline = require('readline');
|
|
10
12
|
var playwright$1 = require('playwright');
|
|
11
13
|
var playwright = require('@agent-scope/playwright');
|
|
12
|
-
var module$1 = require('module');
|
|
13
|
-
var tokens = require('@agent-scope/tokens');
|
|
14
14
|
|
|
15
15
|
function _interopNamespace(e) {
|
|
16
16
|
if (e && e.__esModule) return e;
|
|
@@ -24,16 +24,918 @@ function _interopNamespace(e) {
|
|
|
24
24
|
get: function () { return e[k]; }
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
n.default = e;
|
|
30
|
-
return Object.freeze(n);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
n.default = e;
|
|
30
|
+
return Object.freeze(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild);
|
|
34
|
+
var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
|
|
35
|
+
|
|
36
|
+
// src/ci/commands.ts
|
|
37
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
38
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
39
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
40
|
+
}
|
|
41
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
42
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
43
|
+
const wrapperCode = (
|
|
44
|
+
/* ts */
|
|
45
|
+
`
|
|
46
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
47
|
+
import { createRoot } from "react-dom/client";
|
|
48
|
+
import { createElement } from "react";
|
|
49
|
+
|
|
50
|
+
(function scopeRenderHarness() {
|
|
51
|
+
var Component =
|
|
52
|
+
__scopeMod["default"] ||
|
|
53
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
54
|
+
(Object.values(__scopeMod).find(
|
|
55
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
56
|
+
));
|
|
57
|
+
|
|
58
|
+
if (!Component) {
|
|
59
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
60
|
+
"No renderable component found. Checked: default, " +
|
|
61
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
62
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
63
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
var props = ${propsJson};
|
|
69
|
+
var rootEl = document.getElementById("scope-root");
|
|
70
|
+
if (!rootEl) {
|
|
71
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
72
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
76
|
+
// Use requestAnimationFrame to let React flush the render
|
|
77
|
+
requestAnimationFrame(function() {
|
|
78
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
82
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
`
|
|
86
|
+
);
|
|
87
|
+
const result = await esbuild__namespace.build({
|
|
88
|
+
stdin: {
|
|
89
|
+
contents: wrapperCode,
|
|
90
|
+
// Resolve relative imports (within the component's dir)
|
|
91
|
+
resolveDir: path.dirname(filePath),
|
|
92
|
+
loader: "tsx",
|
|
93
|
+
sourcefile: "__scope_harness__.tsx"
|
|
94
|
+
},
|
|
95
|
+
bundle: true,
|
|
96
|
+
format: "iife",
|
|
97
|
+
write: false,
|
|
98
|
+
platform: "browser",
|
|
99
|
+
jsx: "automatic",
|
|
100
|
+
jsxImportSource: "react",
|
|
101
|
+
target: "es2020",
|
|
102
|
+
// Bundle everything — no externals
|
|
103
|
+
external: [],
|
|
104
|
+
define: {
|
|
105
|
+
"process.env.NODE_ENV": '"development"',
|
|
106
|
+
global: "globalThis"
|
|
107
|
+
},
|
|
108
|
+
logLevel: "silent",
|
|
109
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
110
|
+
banner: {
|
|
111
|
+
js: "/* @agent-scope/cli component harness */"
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
if (result.errors.length > 0) {
|
|
115
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
116
|
+
throw new Error(`esbuild failed to bundle component:
|
|
117
|
+
${msg}`);
|
|
118
|
+
}
|
|
119
|
+
const outputFile = result.outputFiles?.[0];
|
|
120
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
121
|
+
throw new Error("esbuild produced no output");
|
|
122
|
+
}
|
|
123
|
+
return outputFile.text;
|
|
124
|
+
}
|
|
125
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
126
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
127
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
128
|
+
</style>` : "";
|
|
129
|
+
return `<!DOCTYPE html>
|
|
130
|
+
<html lang="en">
|
|
131
|
+
<head>
|
|
132
|
+
<meta charset="UTF-8" />
|
|
133
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
134
|
+
<style>
|
|
135
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
136
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
137
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
138
|
+
</style>
|
|
139
|
+
${projectStyleBlock}
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
143
|
+
<script>${bundledScript}</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/manifest-formatter.ts
|
|
149
|
+
function isTTY() {
|
|
150
|
+
return process.stdout.isTTY === true;
|
|
151
|
+
}
|
|
152
|
+
function pad(value, width) {
|
|
153
|
+
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
154
|
+
}
|
|
155
|
+
function buildTable(headers, rows) {
|
|
156
|
+
const colWidths = headers.map(
|
|
157
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
158
|
+
);
|
|
159
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
160
|
+
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
161
|
+
const dataRows = rows.map(
|
|
162
|
+
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
163
|
+
);
|
|
164
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
165
|
+
}
|
|
166
|
+
function formatListTable(rows) {
|
|
167
|
+
if (rows.length === 0) return "No components found.";
|
|
168
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
169
|
+
const tableRows = rows.map((r) => [
|
|
170
|
+
r.name,
|
|
171
|
+
r.file,
|
|
172
|
+
r.complexityClass,
|
|
173
|
+
String(r.hookCount),
|
|
174
|
+
String(r.contextCount)
|
|
175
|
+
]);
|
|
176
|
+
return buildTable(headers, tableRows);
|
|
177
|
+
}
|
|
178
|
+
function formatListJson(rows) {
|
|
179
|
+
return JSON.stringify(rows, null, 2);
|
|
180
|
+
}
|
|
181
|
+
function formatSideEffects(se) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
184
|
+
if (se.timers) parts.push("timers");
|
|
185
|
+
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
186
|
+
if (se.globalListeners) parts.push("globalListeners");
|
|
187
|
+
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
188
|
+
}
|
|
189
|
+
function formatGetTable(name, descriptor) {
|
|
190
|
+
const propNames = Object.keys(descriptor.props);
|
|
191
|
+
const lines = [
|
|
192
|
+
`Component: ${name}`,
|
|
193
|
+
` File: ${descriptor.filePath}`,
|
|
194
|
+
` Export: ${descriptor.exportType}`,
|
|
195
|
+
` Display Name: ${descriptor.displayName}`,
|
|
196
|
+
` Complexity: ${descriptor.complexityClass}`,
|
|
197
|
+
` Memoized: ${descriptor.memoized}`,
|
|
198
|
+
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
199
|
+
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
200
|
+
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
201
|
+
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
202
|
+
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
203
|
+
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
204
|
+
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
205
|
+
"",
|
|
206
|
+
` Props (${propNames.length}):`
|
|
207
|
+
];
|
|
208
|
+
if (propNames.length === 0) {
|
|
209
|
+
lines.push(" (none)");
|
|
210
|
+
} else {
|
|
211
|
+
for (const propName of propNames) {
|
|
212
|
+
const p = descriptor.props[propName];
|
|
213
|
+
if (p === void 0) continue;
|
|
214
|
+
const req = p.required ? "required" : "optional";
|
|
215
|
+
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
216
|
+
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
217
|
+
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
function formatGetJson(name, descriptor) {
|
|
223
|
+
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
224
|
+
}
|
|
225
|
+
function formatQueryTable(rows, queryDesc) {
|
|
226
|
+
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
227
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
228
|
+
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
229
|
+
return `Query: ${queryDesc}
|
|
230
|
+
|
|
231
|
+
${buildTable(headers, tableRows)}`;
|
|
232
|
+
}
|
|
233
|
+
function formatQueryJson(rows) {
|
|
234
|
+
return JSON.stringify(rows, null, 2);
|
|
235
|
+
}
|
|
236
|
+
function matchGlob(pattern, value) {
|
|
237
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
238
|
+
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
239
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
240
|
+
return regex.test(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/render-formatter.ts
|
|
244
|
+
function parseViewport(spec) {
|
|
245
|
+
const lower = spec.toLowerCase();
|
|
246
|
+
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
247
|
+
if (!match) {
|
|
248
|
+
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
249
|
+
}
|
|
250
|
+
const width = parseInt(match[1] ?? "0", 10);
|
|
251
|
+
const height = parseInt(match[2] ?? "0", 10);
|
|
252
|
+
if (width <= 0 || height <= 0) {
|
|
253
|
+
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
254
|
+
}
|
|
255
|
+
return { width, height };
|
|
256
|
+
}
|
|
257
|
+
function formatRenderJson(componentName, props, result) {
|
|
258
|
+
return {
|
|
259
|
+
component: componentName,
|
|
260
|
+
props,
|
|
261
|
+
width: result.width,
|
|
262
|
+
height: result.height,
|
|
263
|
+
renderTimeMs: result.renderTimeMs,
|
|
264
|
+
computedStyles: result.computedStyles,
|
|
265
|
+
screenshot: result.screenshot.toString("base64"),
|
|
266
|
+
dom: result.dom,
|
|
267
|
+
console: result.console,
|
|
268
|
+
accessibility: result.accessibility
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function formatMatrixJson(result) {
|
|
272
|
+
return {
|
|
273
|
+
axes: result.axes.map((axis) => ({
|
|
274
|
+
name: axis.name,
|
|
275
|
+
values: axis.values.map((v) => String(v))
|
|
276
|
+
})),
|
|
277
|
+
stats: { ...result.stats },
|
|
278
|
+
cells: result.cells.map((cell) => ({
|
|
279
|
+
index: cell.index,
|
|
280
|
+
axisIndices: cell.axisIndices,
|
|
281
|
+
props: cell.props,
|
|
282
|
+
renderTimeMs: cell.result.renderTimeMs,
|
|
283
|
+
width: cell.result.width,
|
|
284
|
+
height: cell.result.height,
|
|
285
|
+
screenshot: cell.result.screenshot.toString("base64")
|
|
286
|
+
}))
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function formatMatrixHtml(componentName, result) {
|
|
290
|
+
const cellsHtml = result.cells.map((cell) => {
|
|
291
|
+
const b64 = cell.result.screenshot.toString("base64");
|
|
292
|
+
const propLabel = escapeHtml(
|
|
293
|
+
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
294
|
+
);
|
|
295
|
+
return ` <div class="cell">
|
|
296
|
+
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
297
|
+
<div class="label">${propLabel}</div>
|
|
298
|
+
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
299
|
+
</div>`;
|
|
300
|
+
}).join("\n");
|
|
301
|
+
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
302
|
+
return `<!DOCTYPE html>
|
|
303
|
+
<html lang="en">
|
|
304
|
+
<head>
|
|
305
|
+
<meta charset="UTF-8" />
|
|
306
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
307
|
+
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
308
|
+
<style>
|
|
309
|
+
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
310
|
+
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
311
|
+
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
312
|
+
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
313
|
+
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
314
|
+
.cell img { display: block; }
|
|
315
|
+
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
316
|
+
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
317
|
+
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
318
|
+
</style>
|
|
319
|
+
</head>
|
|
320
|
+
<body>
|
|
321
|
+
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
322
|
+
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
323
|
+
<div class="grid">
|
|
324
|
+
${cellsHtml}
|
|
325
|
+
</div>
|
|
326
|
+
<div class="stats">
|
|
327
|
+
${result.stats.totalCells} cells \xB7
|
|
328
|
+
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
329
|
+
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
330
|
+
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
331
|
+
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
332
|
+
</div>
|
|
333
|
+
</body>
|
|
334
|
+
</html>
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
function formatMatrixCsv(componentName, result) {
|
|
338
|
+
const axisNames = result.axes.map((a) => a.name);
|
|
339
|
+
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
340
|
+
const rows = result.cells.map((cell) => {
|
|
341
|
+
const axisVals = result.axes.map((_, i) => {
|
|
342
|
+
const axisIdx = cell.axisIndices[i];
|
|
343
|
+
const axis = result.axes[i];
|
|
344
|
+
if (axisIdx === void 0 || axis === void 0) return "";
|
|
345
|
+
const val = axis.values[axisIdx];
|
|
346
|
+
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
347
|
+
});
|
|
348
|
+
return [
|
|
349
|
+
csvEscape(componentName),
|
|
350
|
+
...axisVals,
|
|
351
|
+
cell.result.renderTimeMs.toFixed(3),
|
|
352
|
+
String(cell.result.width),
|
|
353
|
+
String(cell.result.height)
|
|
354
|
+
].join(",");
|
|
355
|
+
});
|
|
356
|
+
return `${[headers.join(","), ...rows].join("\n")}
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
360
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
361
|
+
const empty = barWidth - filled;
|
|
362
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
363
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
364
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
365
|
+
}
|
|
366
|
+
function formatSummaryText(results, outputDir) {
|
|
367
|
+
const total = results.length;
|
|
368
|
+
const passed = results.filter((r) => r.success).length;
|
|
369
|
+
const failed = total - passed;
|
|
370
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
371
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
372
|
+
const lines = [
|
|
373
|
+
"\u2500".repeat(60),
|
|
374
|
+
`Render Summary`,
|
|
375
|
+
"\u2500".repeat(60),
|
|
376
|
+
` Total components : ${total}`,
|
|
377
|
+
` Passed : ${passed}`,
|
|
378
|
+
` Failed : ${failed}`,
|
|
379
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
380
|
+
` Output dir : ${outputDir}`
|
|
381
|
+
];
|
|
382
|
+
if (failed > 0) {
|
|
383
|
+
lines.push("", " Failed components:");
|
|
384
|
+
for (const r of results) {
|
|
385
|
+
if (!r.success) {
|
|
386
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
lines.push("\u2500".repeat(60));
|
|
391
|
+
return lines.join("\n");
|
|
392
|
+
}
|
|
393
|
+
function escapeHtml(str) {
|
|
394
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
395
|
+
}
|
|
396
|
+
function csvEscape(value) {
|
|
397
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
398
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
var CONFIG_FILENAMES = [
|
|
403
|
+
".reactscope/config.json",
|
|
404
|
+
".reactscope/config.js",
|
|
405
|
+
".reactscope/config.mjs"
|
|
406
|
+
];
|
|
407
|
+
var STYLE_ENTRY_CANDIDATES = [
|
|
408
|
+
"src/index.css",
|
|
409
|
+
"src/globals.css",
|
|
410
|
+
"app/globals.css",
|
|
411
|
+
"app/index.css",
|
|
412
|
+
"styles/index.css",
|
|
413
|
+
"index.css"
|
|
414
|
+
];
|
|
415
|
+
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
416
|
+
var compilerCache = null;
|
|
417
|
+
function getCachedBuild(cwd) {
|
|
418
|
+
if (compilerCache !== null && path.resolve(compilerCache.cwd) === path.resolve(cwd)) {
|
|
419
|
+
return compilerCache.build;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
function findStylesEntry(cwd) {
|
|
424
|
+
for (const name of CONFIG_FILENAMES) {
|
|
425
|
+
const p = path.resolve(cwd, name);
|
|
426
|
+
if (!fs.existsSync(p)) continue;
|
|
427
|
+
try {
|
|
428
|
+
if (name.endsWith(".json")) {
|
|
429
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
430
|
+
const data = JSON.parse(raw);
|
|
431
|
+
const scope = data.scope;
|
|
432
|
+
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
433
|
+
if (typeof entry === "string") {
|
|
434
|
+
const full = path.resolve(cwd, entry);
|
|
435
|
+
if (fs.existsSync(full)) return full;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const pkgPath = path.resolve(cwd, "package.json");
|
|
442
|
+
if (fs.existsSync(pkgPath)) {
|
|
443
|
+
try {
|
|
444
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
445
|
+
const pkg = JSON.parse(raw);
|
|
446
|
+
const entry = pkg.scope?.stylesEntry;
|
|
447
|
+
if (typeof entry === "string") {
|
|
448
|
+
const full = path.resolve(cwd, entry);
|
|
449
|
+
if (fs.existsSync(full)) return full;
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
455
|
+
const full = path.resolve(cwd, candidate);
|
|
456
|
+
if (fs.existsSync(full)) {
|
|
457
|
+
try {
|
|
458
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
459
|
+
if (TAILWIND_IMPORT.test(content)) return full;
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
async function getTailwindCompiler(cwd) {
|
|
467
|
+
const cached = getCachedBuild(cwd);
|
|
468
|
+
if (cached !== null) return cached;
|
|
469
|
+
const entryPath = findStylesEntry(cwd);
|
|
470
|
+
if (entryPath === null) return null;
|
|
471
|
+
let compile;
|
|
472
|
+
try {
|
|
473
|
+
const require2 = module$1.createRequire(path.resolve(cwd, "package.json"));
|
|
474
|
+
const tailwind = require2("tailwindcss");
|
|
475
|
+
const fn = tailwind.compile;
|
|
476
|
+
if (typeof fn !== "function") return null;
|
|
477
|
+
compile = fn;
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
const entryContent = fs.readFileSync(entryPath, "utf-8");
|
|
482
|
+
const loadStylesheet = async (id, base) => {
|
|
483
|
+
if (id === "tailwindcss") {
|
|
484
|
+
const nodeModules = path.resolve(cwd, "node_modules");
|
|
485
|
+
const tailwindCssPath = path.resolve(nodeModules, "tailwindcss", "index.css");
|
|
486
|
+
if (!fs.existsSync(tailwindCssPath)) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const content = fs.readFileSync(tailwindCssPath, "utf-8");
|
|
492
|
+
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
493
|
+
}
|
|
494
|
+
const full = path.resolve(base, id);
|
|
495
|
+
if (fs.existsSync(full)) {
|
|
496
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
497
|
+
return { path: full, base: path.resolve(full, ".."), content };
|
|
498
|
+
}
|
|
499
|
+
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
500
|
+
};
|
|
501
|
+
const result = await compile(entryContent, {
|
|
502
|
+
base: cwd,
|
|
503
|
+
from: entryPath,
|
|
504
|
+
loadStylesheet
|
|
505
|
+
});
|
|
506
|
+
const build2 = result.build.bind(result);
|
|
507
|
+
compilerCache = { cwd, build: build2 };
|
|
508
|
+
return build2;
|
|
509
|
+
}
|
|
510
|
+
async function getCompiledCssForClasses(cwd, classes) {
|
|
511
|
+
const build2 = await getTailwindCompiler(cwd);
|
|
512
|
+
if (build2 === null) return null;
|
|
513
|
+
const deduped = [...new Set(classes)].filter(Boolean);
|
|
514
|
+
if (deduped.length === 0) return null;
|
|
515
|
+
return build2(deduped);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/ci/commands.ts
|
|
519
|
+
var CI_EXIT = {
|
|
520
|
+
/** All checks passed */
|
|
521
|
+
OK: 0,
|
|
522
|
+
/** Compliance score below threshold */
|
|
523
|
+
COMPLIANCE_BELOW_THRESHOLD: 1,
|
|
524
|
+
/** Accessibility violations found */
|
|
525
|
+
A11Y_VIOLATIONS: 2,
|
|
526
|
+
/** Console errors detected during render */
|
|
527
|
+
CONSOLE_ERRORS: 3,
|
|
528
|
+
/** Visual regression detected against baseline */
|
|
529
|
+
VISUAL_REGRESSION: 4,
|
|
530
|
+
/** One or more components failed to render */
|
|
531
|
+
RENDER_FAILURES: 5
|
|
532
|
+
};
|
|
533
|
+
var ALL_CHECKS = ["compliance", "a11y", "console-errors", "visual-regression"];
|
|
534
|
+
var _pool = null;
|
|
535
|
+
async function getPool(viewportWidth, viewportHeight) {
|
|
536
|
+
if (_pool === null) {
|
|
537
|
+
_pool = new render.BrowserPool({
|
|
538
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
539
|
+
viewportWidth,
|
|
540
|
+
viewportHeight
|
|
541
|
+
});
|
|
542
|
+
await _pool.init();
|
|
543
|
+
}
|
|
544
|
+
return _pool;
|
|
545
|
+
}
|
|
546
|
+
async function shutdownPool() {
|
|
547
|
+
if (_pool !== null) {
|
|
548
|
+
await _pool.close();
|
|
549
|
+
_pool = null;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
553
|
+
const pool = await getPool(viewportWidth, viewportHeight);
|
|
554
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
555
|
+
const slot = await pool.acquire();
|
|
556
|
+
const { page } = slot;
|
|
557
|
+
try {
|
|
558
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
559
|
+
await page.waitForFunction(
|
|
560
|
+
() => {
|
|
561
|
+
const w = window;
|
|
562
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
563
|
+
},
|
|
564
|
+
{ timeout: 15e3 }
|
|
565
|
+
);
|
|
566
|
+
const renderError = await page.evaluate(() => {
|
|
567
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
568
|
+
});
|
|
569
|
+
if (renderError !== null) {
|
|
570
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
571
|
+
}
|
|
572
|
+
const rootDir = process.cwd();
|
|
573
|
+
const classes = await page.evaluate(() => {
|
|
574
|
+
const set = /* @__PURE__ */ new Set();
|
|
575
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
576
|
+
for (const c of el.className.split(/\s+/)) {
|
|
577
|
+
if (c) set.add(c);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return [...set];
|
|
581
|
+
});
|
|
582
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
583
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
584
|
+
await page.addStyleTag({ content: projectCss });
|
|
585
|
+
}
|
|
586
|
+
const startMs = performance.now();
|
|
587
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
588
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
589
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const PAD = 24;
|
|
595
|
+
const MIN_W = 320;
|
|
596
|
+
const MIN_H = 200;
|
|
597
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
598
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
599
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
600
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
601
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
602
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
603
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
604
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
605
|
+
const screenshot = await page.screenshot({
|
|
606
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
607
|
+
type: "png"
|
|
608
|
+
});
|
|
609
|
+
const computedStylesRaw = {};
|
|
610
|
+
const styles = await page.evaluate((sel) => {
|
|
611
|
+
const el = document.querySelector(sel);
|
|
612
|
+
if (el === null) return {};
|
|
613
|
+
const computed = window.getComputedStyle(el);
|
|
614
|
+
const out = {};
|
|
615
|
+
for (const prop of [
|
|
616
|
+
"display",
|
|
617
|
+
"width",
|
|
618
|
+
"height",
|
|
619
|
+
"color",
|
|
620
|
+
"backgroundColor",
|
|
621
|
+
"fontSize",
|
|
622
|
+
"fontFamily",
|
|
623
|
+
"padding",
|
|
624
|
+
"margin"
|
|
625
|
+
]) {
|
|
626
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}, "[data-reactscope-root] > *");
|
|
630
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
631
|
+
const renderTimeMs = performance.now() - startMs;
|
|
632
|
+
return {
|
|
633
|
+
screenshot,
|
|
634
|
+
width: Math.round(safeW),
|
|
635
|
+
height: Math.round(safeH),
|
|
636
|
+
renderTimeMs,
|
|
637
|
+
computedStyles: computedStylesRaw
|
|
638
|
+
};
|
|
639
|
+
} finally {
|
|
640
|
+
pool.release(slot);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
644
|
+
const flat = {};
|
|
645
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
646
|
+
Object.assign(flat, styles);
|
|
647
|
+
}
|
|
648
|
+
const colors = {};
|
|
649
|
+
const spacing = {};
|
|
650
|
+
const typography = {};
|
|
651
|
+
const borders = {};
|
|
652
|
+
const shadows = {};
|
|
653
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
654
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
655
|
+
colors[prop] = value;
|
|
656
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
657
|
+
spacing[prop] = value;
|
|
658
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
659
|
+
typography[prop] = value;
|
|
660
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
661
|
+
borders[prop] = value;
|
|
662
|
+
} else if (prop === "boxShadow") {
|
|
663
|
+
shadows[prop] = value;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return { colors, spacing, typography, borders, shadows };
|
|
667
|
+
}
|
|
668
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
669
|
+
const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
|
|
670
|
+
if (!fs.existsSync(jsonPath)) return null;
|
|
671
|
+
return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
672
|
+
}
|
|
673
|
+
async function runCi(options = {}) {
|
|
674
|
+
const {
|
|
675
|
+
baselineDir: baselineDirRaw,
|
|
676
|
+
checks = ALL_CHECKS,
|
|
677
|
+
complianceThreshold = 0.9,
|
|
678
|
+
viewportWidth = 375,
|
|
679
|
+
viewportHeight = 812
|
|
680
|
+
} = options;
|
|
681
|
+
const startTime = performance.now();
|
|
682
|
+
const rootDir = process.cwd();
|
|
683
|
+
const checksSet = new Set(checks);
|
|
684
|
+
const baselineDir = baselineDirRaw !== void 0 ? path.resolve(rootDir, baselineDirRaw) : void 0;
|
|
685
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
686
|
+
const manifest$1 = await manifest.generateManifest({ rootDir });
|
|
687
|
+
const componentNames = Object.keys(manifest$1.components);
|
|
688
|
+
const total = componentNames.length;
|
|
689
|
+
process.stderr.write(`Found ${total} components.
|
|
690
|
+
`);
|
|
691
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
692
|
+
`);
|
|
693
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
694
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
695
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
696
|
+
let completed = 0;
|
|
697
|
+
const CONCURRENCY = 4;
|
|
698
|
+
let nextIdx = 0;
|
|
699
|
+
const renderOne = async (name) => {
|
|
700
|
+
const descriptor = manifest$1.components[name];
|
|
701
|
+
if (descriptor === void 0) return;
|
|
702
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
703
|
+
const outcome = await render.safeRender(
|
|
704
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
705
|
+
{
|
|
706
|
+
props: {},
|
|
707
|
+
sourceLocation: {
|
|
708
|
+
file: descriptor.filePath,
|
|
709
|
+
line: descriptor.loc.start,
|
|
710
|
+
column: 0
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
completed++;
|
|
715
|
+
const pct = Math.round(completed / total * 100);
|
|
716
|
+
if (isTTY()) {
|
|
717
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
718
|
+
}
|
|
719
|
+
if (outcome.crashed) {
|
|
720
|
+
renderFailures.add(name);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const result = outcome.result;
|
|
724
|
+
currentRenderMeta.set(name, { width: result.width, height: result.height });
|
|
725
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
726
|
+
};
|
|
727
|
+
if (total > 0) {
|
|
728
|
+
const worker = async () => {
|
|
729
|
+
while (nextIdx < componentNames.length) {
|
|
730
|
+
const i = nextIdx++;
|
|
731
|
+
const name = componentNames[i];
|
|
732
|
+
if (name !== void 0) {
|
|
733
|
+
await renderOne(name);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
const workers = [];
|
|
738
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
739
|
+
workers.push(worker());
|
|
740
|
+
}
|
|
741
|
+
await Promise.all(workers);
|
|
742
|
+
}
|
|
743
|
+
await shutdownPool();
|
|
744
|
+
if (isTTY() && total > 0) {
|
|
745
|
+
process.stderr.write("\n");
|
|
746
|
+
}
|
|
747
|
+
const resolver = new tokens.TokenResolver([]);
|
|
748
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
749
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
750
|
+
const complianceScore = batchReport.aggregateCompliance;
|
|
751
|
+
const checkResults = [];
|
|
752
|
+
const renderFailureCount = renderFailures.size;
|
|
753
|
+
const rendersPassed = renderFailureCount === 0;
|
|
754
|
+
if (checksSet.has("compliance")) {
|
|
755
|
+
const compliancePassed = complianceScore >= complianceThreshold;
|
|
756
|
+
checkResults.push({
|
|
757
|
+
check: "compliance",
|
|
758
|
+
passed: compliancePassed,
|
|
759
|
+
message: compliancePassed ? `Compliance ${(complianceScore * 100).toFixed(1)}% >= threshold ${(complianceThreshold * 100).toFixed(1)}%` : `Compliance ${(complianceScore * 100).toFixed(1)}% < threshold ${(complianceThreshold * 100).toFixed(1)}%`,
|
|
760
|
+
value: complianceScore,
|
|
761
|
+
threshold: complianceThreshold
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (checksSet.has("a11y")) {
|
|
765
|
+
checkResults.push({
|
|
766
|
+
check: "a11y",
|
|
767
|
+
passed: true,
|
|
768
|
+
message: "Accessibility audit not yet implemented \u2014 skipped"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (checksSet.has("console-errors")) {
|
|
772
|
+
const consoleErrorsPassed = rendersPassed;
|
|
773
|
+
checkResults.push({
|
|
774
|
+
check: "console-errors",
|
|
775
|
+
passed: consoleErrorsPassed,
|
|
776
|
+
message: consoleErrorsPassed ? "No console errors detected" : `Console errors likely \u2014 ${renderFailureCount} component(s) failed to render`
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
let hasVisualRegression = false;
|
|
780
|
+
if (checksSet.has("visual-regression") && baselineDir !== void 0) {
|
|
781
|
+
if (!fs.existsSync(baselineDir)) {
|
|
782
|
+
process.stderr.write(
|
|
783
|
+
`Warning: baseline directory not found at "${baselineDir}" \u2014 skipping visual regression check.
|
|
784
|
+
`
|
|
785
|
+
);
|
|
786
|
+
checkResults.push({
|
|
787
|
+
check: "visual-regression",
|
|
788
|
+
passed: true,
|
|
789
|
+
message: `Baseline not found at ${baselineDir} \u2014 skipped`
|
|
790
|
+
});
|
|
791
|
+
} else {
|
|
792
|
+
const regressions = [];
|
|
793
|
+
for (const name of componentNames) {
|
|
794
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
795
|
+
const currentMeta = currentRenderMeta.get(name);
|
|
796
|
+
if (baselineMeta !== null && currentMeta !== void 0) {
|
|
797
|
+
const dw = Math.abs(currentMeta.width - baselineMeta.width);
|
|
798
|
+
const dh = Math.abs(currentMeta.height - baselineMeta.height);
|
|
799
|
+
if (dw > 10 || dh > 10) {
|
|
800
|
+
regressions.push(name);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
hasVisualRegression = regressions.length > 0;
|
|
805
|
+
checkResults.push({
|
|
806
|
+
check: "visual-regression",
|
|
807
|
+
passed: !hasVisualRegression,
|
|
808
|
+
message: hasVisualRegression ? `Visual regression detected in ${regressions.length} component(s): ${regressions.slice(0, 5).join(", ")}${regressions.length > 5 ? "..." : ""}` : "No visual regressions detected"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
let exitCode = CI_EXIT.OK;
|
|
813
|
+
const complianceResult = checkResults.find((r) => r.check === "compliance");
|
|
814
|
+
const a11yResult = checkResults.find((r) => r.check === "a11y");
|
|
815
|
+
const consoleResult = checkResults.find((r) => r.check === "console-errors");
|
|
816
|
+
const visualResult = checkResults.find((r) => r.check === "visual-regression");
|
|
817
|
+
if (complianceResult !== void 0 && !complianceResult.passed) {
|
|
818
|
+
exitCode = CI_EXIT.COMPLIANCE_BELOW_THRESHOLD;
|
|
819
|
+
} else if (a11yResult !== void 0 && !a11yResult.passed) {
|
|
820
|
+
exitCode = CI_EXIT.A11Y_VIOLATIONS;
|
|
821
|
+
} else if (consoleResult !== void 0 && !consoleResult.passed) {
|
|
822
|
+
exitCode = CI_EXIT.CONSOLE_ERRORS;
|
|
823
|
+
} else if (visualResult !== void 0 && !visualResult.passed) {
|
|
824
|
+
exitCode = CI_EXIT.VISUAL_REGRESSION;
|
|
825
|
+
} else if (!rendersPassed) {
|
|
826
|
+
exitCode = CI_EXIT.RENDER_FAILURES;
|
|
827
|
+
}
|
|
828
|
+
const passed = exitCode === CI_EXIT.OK;
|
|
829
|
+
const wallClockMs = performance.now() - startTime;
|
|
830
|
+
return {
|
|
831
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
832
|
+
passed,
|
|
833
|
+
exitCode,
|
|
834
|
+
checks: checkResults,
|
|
835
|
+
components: {
|
|
836
|
+
total,
|
|
837
|
+
rendered: total - renderFailureCount,
|
|
838
|
+
failed: renderFailureCount
|
|
839
|
+
},
|
|
840
|
+
complianceScore,
|
|
841
|
+
complianceThreshold,
|
|
842
|
+
baselineCompared: baselineDir !== void 0 && fs.existsSync(baselineDir),
|
|
843
|
+
wallClockMs
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function formatCiReport(result) {
|
|
847
|
+
const lines = [];
|
|
848
|
+
const title = "Scope CI Report";
|
|
849
|
+
const rule2 = "=".repeat(Math.max(title.length, 40));
|
|
850
|
+
lines.push(title, rule2);
|
|
851
|
+
const { total, rendered, failed } = result.components;
|
|
852
|
+
lines.push(
|
|
853
|
+
`Components: ${total} total ${rendered} rendered${failed > 0 ? ` ${failed} failed` : ""}`
|
|
854
|
+
);
|
|
855
|
+
lines.push("");
|
|
856
|
+
for (const check of result.checks) {
|
|
857
|
+
const icon = check.passed ? "pass" : "FAIL";
|
|
858
|
+
lines.push(` [${icon}] ${check.message}`);
|
|
859
|
+
}
|
|
860
|
+
if (result.components.failed > 0) {
|
|
861
|
+
lines.push(` [FAIL] ${result.components.failed} component(s) failed to render`);
|
|
862
|
+
}
|
|
863
|
+
lines.push("");
|
|
864
|
+
lines.push(rule2);
|
|
865
|
+
if (result.passed) {
|
|
866
|
+
lines.push(
|
|
867
|
+
`CI passed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
868
|
+
);
|
|
869
|
+
} else {
|
|
870
|
+
lines.push(
|
|
871
|
+
`CI failed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return lines.join("\n");
|
|
875
|
+
}
|
|
876
|
+
function parseChecks(raw) {
|
|
877
|
+
if (raw === void 0) return void 0;
|
|
878
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
879
|
+
if (parts.length === 0) return void 0;
|
|
880
|
+
const valid = new Set(ALL_CHECKS);
|
|
881
|
+
const parsed = [];
|
|
882
|
+
for (const part of parts) {
|
|
883
|
+
if (!valid.has(part)) {
|
|
884
|
+
process.stderr.write(
|
|
885
|
+
`Warning: unknown check "${part}" \u2014 valid checks are: ${ALL_CHECKS.join(", ")}
|
|
886
|
+
`
|
|
887
|
+
);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
parsed.push(part);
|
|
891
|
+
}
|
|
892
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
893
|
+
}
|
|
894
|
+
function createCiCommand() {
|
|
895
|
+
return new commander.Command("ci").description(
|
|
896
|
+
"Run a non-interactive CI pipeline (manifest -> render -> compliance -> regression) with exit codes"
|
|
897
|
+
).option(
|
|
898
|
+
"-b, --baseline <dir>",
|
|
899
|
+
"Baseline directory for visual regression comparison (omit to skip)"
|
|
900
|
+
).option(
|
|
901
|
+
"--checks <list>",
|
|
902
|
+
`Comma-separated checks to run (default: all). Valid: ${ALL_CHECKS.join(", ")}`
|
|
903
|
+
).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(
|
|
904
|
+
async (opts) => {
|
|
905
|
+
try {
|
|
906
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
907
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
908
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
909
|
+
const complianceThreshold = Number.parseFloat(opts.threshold);
|
|
910
|
+
const checks = parseChecks(opts.checks);
|
|
911
|
+
const result = await runCi({
|
|
912
|
+
baselineDir: opts.baseline,
|
|
913
|
+
checks,
|
|
914
|
+
complianceThreshold,
|
|
915
|
+
viewportWidth,
|
|
916
|
+
viewportHeight
|
|
917
|
+
});
|
|
918
|
+
if (opts.output !== void 0) {
|
|
919
|
+
const outPath = path.resolve(process.cwd(), opts.output);
|
|
920
|
+
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
|
|
921
|
+
process.stderr.write(`CI result written to ${opts.output}
|
|
922
|
+
`);
|
|
923
|
+
}
|
|
924
|
+
process.stdout.write(`${formatCiReport(result)}
|
|
925
|
+
`);
|
|
926
|
+
if (opts.json) {
|
|
927
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
928
|
+
`);
|
|
929
|
+
}
|
|
930
|
+
process.exit(result.exitCode);
|
|
931
|
+
} catch (err) {
|
|
932
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
933
|
+
`);
|
|
934
|
+
process.exit(CI_EXIT.RENDER_FAILURES);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
);
|
|
31
938
|
}
|
|
32
|
-
|
|
33
|
-
var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
|
|
34
|
-
var esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild);
|
|
35
|
-
|
|
36
|
-
// src/init/index.ts
|
|
37
939
|
function hasConfigFile(dir, stem) {
|
|
38
940
|
if (!fs.existsSync(dir)) return false;
|
|
39
941
|
try {
|
|
@@ -228,9 +1130,9 @@ function createRL() {
|
|
|
228
1130
|
});
|
|
229
1131
|
}
|
|
230
1132
|
async function ask(rl, question) {
|
|
231
|
-
return new Promise((
|
|
1133
|
+
return new Promise((resolve15) => {
|
|
232
1134
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
1135
|
+
resolve15(answer.trim());
|
|
234
1136
|
});
|
|
235
1137
|
});
|
|
236
1138
|
}
|
|
@@ -363,225 +1265,20 @@ async function runInit(options) {
|
|
|
363
1265
|
skipped: false
|
|
364
1266
|
};
|
|
365
1267
|
}
|
|
366
|
-
function createInitCommand() {
|
|
367
|
-
return new commander.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) => {
|
|
368
|
-
try {
|
|
369
|
-
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
370
|
-
if (!result.success && !result.skipped) {
|
|
371
|
-
process.exit(1);
|
|
372
|
-
}
|
|
373
|
-
} catch (err) {
|
|
374
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
375
|
-
`);
|
|
376
|
-
process.exit(1);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
381
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
382
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
383
|
-
}
|
|
384
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
385
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
386
|
-
const wrapperCode = (
|
|
387
|
-
/* ts */
|
|
388
|
-
`
|
|
389
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
390
|
-
import { createRoot } from "react-dom/client";
|
|
391
|
-
import { createElement } from "react";
|
|
392
|
-
|
|
393
|
-
(function scopeRenderHarness() {
|
|
394
|
-
var Component =
|
|
395
|
-
__scopeMod["default"] ||
|
|
396
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
397
|
-
(Object.values(__scopeMod).find(
|
|
398
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
399
|
-
));
|
|
400
|
-
|
|
401
|
-
if (!Component) {
|
|
402
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
403
|
-
"No renderable component found. Checked: default, " +
|
|
404
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
405
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
406
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
var props = ${propsJson};
|
|
412
|
-
var rootEl = document.getElementById("scope-root");
|
|
413
|
-
if (!rootEl) {
|
|
414
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
415
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
419
|
-
// Use requestAnimationFrame to let React flush the render
|
|
420
|
-
requestAnimationFrame(function() {
|
|
421
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
422
|
-
});
|
|
423
|
-
} catch (err) {
|
|
424
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
425
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
426
|
-
}
|
|
427
|
-
})();
|
|
428
|
-
`
|
|
429
|
-
);
|
|
430
|
-
const result = await esbuild__namespace.build({
|
|
431
|
-
stdin: {
|
|
432
|
-
contents: wrapperCode,
|
|
433
|
-
// Resolve relative imports (within the component's dir)
|
|
434
|
-
resolveDir: path.dirname(filePath),
|
|
435
|
-
loader: "tsx",
|
|
436
|
-
sourcefile: "__scope_harness__.tsx"
|
|
437
|
-
},
|
|
438
|
-
bundle: true,
|
|
439
|
-
format: "iife",
|
|
440
|
-
write: false,
|
|
441
|
-
platform: "browser",
|
|
442
|
-
jsx: "automatic",
|
|
443
|
-
jsxImportSource: "react",
|
|
444
|
-
target: "es2020",
|
|
445
|
-
// Bundle everything — no externals
|
|
446
|
-
external: [],
|
|
447
|
-
define: {
|
|
448
|
-
"process.env.NODE_ENV": '"development"',
|
|
449
|
-
global: "globalThis"
|
|
450
|
-
},
|
|
451
|
-
logLevel: "silent",
|
|
452
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
453
|
-
banner: {
|
|
454
|
-
js: "/* @agent-scope/cli component harness */"
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
if (result.errors.length > 0) {
|
|
458
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
459
|
-
throw new Error(`esbuild failed to bundle component:
|
|
460
|
-
${msg}`);
|
|
461
|
-
}
|
|
462
|
-
const outputFile = result.outputFiles?.[0];
|
|
463
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
464
|
-
throw new Error("esbuild produced no output");
|
|
465
|
-
}
|
|
466
|
-
return outputFile.text;
|
|
467
|
-
}
|
|
468
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
469
|
-
const projectStyleBlock = "";
|
|
470
|
-
return `<!DOCTYPE html>
|
|
471
|
-
<html lang="en">
|
|
472
|
-
<head>
|
|
473
|
-
<meta charset="UTF-8" />
|
|
474
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
475
|
-
<style>
|
|
476
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
477
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
478
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
479
|
-
</style>
|
|
480
|
-
${projectStyleBlock}
|
|
481
|
-
</head>
|
|
482
|
-
<body>
|
|
483
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
484
|
-
<script>${bundledScript}</script>
|
|
485
|
-
</body>
|
|
486
|
-
</html>`;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// src/manifest-formatter.ts
|
|
490
|
-
function isTTY() {
|
|
491
|
-
return process.stdout.isTTY === true;
|
|
492
|
-
}
|
|
493
|
-
function pad(value, width) {
|
|
494
|
-
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
495
|
-
}
|
|
496
|
-
function buildTable(headers, rows) {
|
|
497
|
-
const colWidths = headers.map(
|
|
498
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
499
|
-
);
|
|
500
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
501
|
-
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
502
|
-
const dataRows = rows.map(
|
|
503
|
-
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
504
|
-
);
|
|
505
|
-
return [headerRow, divider, ...dataRows].join("\n");
|
|
506
|
-
}
|
|
507
|
-
function formatListTable(rows) {
|
|
508
|
-
if (rows.length === 0) return "No components found.";
|
|
509
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
510
|
-
const tableRows = rows.map((r) => [
|
|
511
|
-
r.name,
|
|
512
|
-
r.file,
|
|
513
|
-
r.complexityClass,
|
|
514
|
-
String(r.hookCount),
|
|
515
|
-
String(r.contextCount)
|
|
516
|
-
]);
|
|
517
|
-
return buildTable(headers, tableRows);
|
|
518
|
-
}
|
|
519
|
-
function formatListJson(rows) {
|
|
520
|
-
return JSON.stringify(rows, null, 2);
|
|
521
|
-
}
|
|
522
|
-
function formatSideEffects(se) {
|
|
523
|
-
const parts = [];
|
|
524
|
-
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
525
|
-
if (se.timers) parts.push("timers");
|
|
526
|
-
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
527
|
-
if (se.globalListeners) parts.push("globalListeners");
|
|
528
|
-
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
529
|
-
}
|
|
530
|
-
function formatGetTable(name, descriptor) {
|
|
531
|
-
const propNames = Object.keys(descriptor.props);
|
|
532
|
-
const lines = [
|
|
533
|
-
`Component: ${name}`,
|
|
534
|
-
` File: ${descriptor.filePath}`,
|
|
535
|
-
` Export: ${descriptor.exportType}`,
|
|
536
|
-
` Display Name: ${descriptor.displayName}`,
|
|
537
|
-
` Complexity: ${descriptor.complexityClass}`,
|
|
538
|
-
` Memoized: ${descriptor.memoized}`,
|
|
539
|
-
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
540
|
-
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
541
|
-
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
542
|
-
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
543
|
-
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
544
|
-
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
545
|
-
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
546
|
-
"",
|
|
547
|
-
` Props (${propNames.length}):`
|
|
548
|
-
];
|
|
549
|
-
if (propNames.length === 0) {
|
|
550
|
-
lines.push(" (none)");
|
|
551
|
-
} else {
|
|
552
|
-
for (const propName of propNames) {
|
|
553
|
-
const p = descriptor.props[propName];
|
|
554
|
-
if (p === void 0) continue;
|
|
555
|
-
const req = p.required ? "required" : "optional";
|
|
556
|
-
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
557
|
-
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
558
|
-
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return lines.join("\n");
|
|
562
|
-
}
|
|
563
|
-
function formatGetJson(name, descriptor) {
|
|
564
|
-
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
565
|
-
}
|
|
566
|
-
function formatQueryTable(rows, queryDesc) {
|
|
567
|
-
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
568
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
569
|
-
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
570
|
-
return `Query: ${queryDesc}
|
|
571
|
-
|
|
572
|
-
${buildTable(headers, tableRows)}`;
|
|
573
|
-
}
|
|
574
|
-
function formatQueryJson(rows) {
|
|
575
|
-
return JSON.stringify(rows, null, 2);
|
|
576
|
-
}
|
|
577
|
-
function matchGlob(pattern, value) {
|
|
578
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
579
|
-
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
580
|
-
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
581
|
-
return regex.test(value);
|
|
1268
|
+
function createInitCommand() {
|
|
1269
|
+
return new commander.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) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1272
|
+
if (!result.success && !result.skipped) {
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1277
|
+
`);
|
|
1278
|
+
process.exit(1);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
582
1281
|
}
|
|
583
|
-
|
|
584
|
-
// src/manifest-commands.ts
|
|
585
1282
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
586
1283
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
587
1284
|
const absPath = path.resolve(process.cwd(), manifestPath);
|
|
@@ -660,257 +1357,97 @@ function registerQuery(manifestCmd) {
|
|
|
660
1357
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
661
1358
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
662
1359
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
663
|
-
if (queryParts.length === 0) {
|
|
664
|
-
process.stderr.write(
|
|
665
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
|
|
666
|
-
);
|
|
667
|
-
process.exit(1);
|
|
668
|
-
}
|
|
669
|
-
const queryDesc = queryParts.join(", ");
|
|
670
|
-
let entries = Object.entries(manifest.components);
|
|
671
|
-
if (opts.context !== void 0) {
|
|
672
|
-
const ctx = opts.context;
|
|
673
|
-
entries = entries.filter(([, d]) => d.requiredContexts.includes(ctx));
|
|
674
|
-
}
|
|
675
|
-
if (opts.hook !== void 0) {
|
|
676
|
-
const hook = opts.hook;
|
|
677
|
-
entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
|
|
678
|
-
}
|
|
679
|
-
if (opts.complexity !== void 0) {
|
|
680
|
-
const cls = opts.complexity;
|
|
681
|
-
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
682
|
-
}
|
|
683
|
-
if (opts.sideEffects) {
|
|
684
|
-
entries = entries.filter(([, d]) => {
|
|
685
|
-
const se = d.sideEffects;
|
|
686
|
-
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
if (opts.hasFetch) {
|
|
690
|
-
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
691
|
-
}
|
|
692
|
-
const rows = entries.map(([name, d]) => ({
|
|
693
|
-
name,
|
|
694
|
-
file: d.filePath,
|
|
695
|
-
complexityClass: d.complexityClass,
|
|
696
|
-
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
697
|
-
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
698
|
-
}));
|
|
699
|
-
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
700
|
-
process.stdout.write(`${output}
|
|
701
|
-
`);
|
|
702
|
-
} catch (err) {
|
|
703
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
704
|
-
`);
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
function registerGenerate(manifestCmd) {
|
|
711
|
-
manifestCmd.command("generate").description(
|
|
712
|
-
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
713
|
-
).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) => {
|
|
714
|
-
try {
|
|
715
|
-
const rootDir = path.resolve(process.cwd(), opts.root ?? ".");
|
|
716
|
-
const outputPath = path.resolve(process.cwd(), opts.output);
|
|
717
|
-
const include = opts.include?.split(",").map((s) => s.trim());
|
|
718
|
-
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
719
|
-
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
720
|
-
`);
|
|
721
|
-
const manifest$1 = await manifest.generateManifest({
|
|
722
|
-
rootDir,
|
|
723
|
-
...include !== void 0 && { include },
|
|
724
|
-
...exclude !== void 0 && { exclude }
|
|
725
|
-
});
|
|
726
|
-
const componentCount = Object.keys(manifest$1.components).length;
|
|
727
|
-
process.stderr.write(`Found ${componentCount} components.
|
|
728
|
-
`);
|
|
729
|
-
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
730
|
-
if (!fs.existsSync(outputDir)) {
|
|
731
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
732
|
-
}
|
|
733
|
-
fs.writeFileSync(outputPath, JSON.stringify(manifest$1, null, 2), "utf-8");
|
|
734
|
-
process.stderr.write(`Manifest written to ${outputPath}
|
|
735
|
-
`);
|
|
736
|
-
process.stdout.write(`${outputPath}
|
|
737
|
-
`);
|
|
738
|
-
} catch (err) {
|
|
739
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
740
|
-
`);
|
|
741
|
-
process.exit(1);
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
function createManifestCommand() {
|
|
746
|
-
const manifestCmd = new commander.Command("manifest").description(
|
|
747
|
-
"Query and explore the component manifest"
|
|
748
|
-
);
|
|
749
|
-
registerList(manifestCmd);
|
|
750
|
-
registerGet(manifestCmd);
|
|
751
|
-
registerQuery(manifestCmd);
|
|
752
|
-
registerGenerate(manifestCmd);
|
|
753
|
-
return manifestCmd;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// src/render-formatter.ts
|
|
757
|
-
function parseViewport(spec) {
|
|
758
|
-
const lower = spec.toLowerCase();
|
|
759
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
760
|
-
if (!match) {
|
|
761
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
762
|
-
}
|
|
763
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
764
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
765
|
-
if (width <= 0 || height <= 0) {
|
|
766
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
767
|
-
}
|
|
768
|
-
return { width, height };
|
|
769
|
-
}
|
|
770
|
-
function formatRenderJson(componentName, props, result) {
|
|
771
|
-
return {
|
|
772
|
-
component: componentName,
|
|
773
|
-
props,
|
|
774
|
-
width: result.width,
|
|
775
|
-
height: result.height,
|
|
776
|
-
renderTimeMs: result.renderTimeMs,
|
|
777
|
-
computedStyles: result.computedStyles,
|
|
778
|
-
screenshot: result.screenshot.toString("base64"),
|
|
779
|
-
dom: result.dom,
|
|
780
|
-
console: result.console,
|
|
781
|
-
accessibility: result.accessibility
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
function formatMatrixJson(result) {
|
|
785
|
-
return {
|
|
786
|
-
axes: result.axes.map((axis) => ({
|
|
787
|
-
name: axis.name,
|
|
788
|
-
values: axis.values.map((v) => String(v))
|
|
789
|
-
})),
|
|
790
|
-
stats: { ...result.stats },
|
|
791
|
-
cells: result.cells.map((cell) => ({
|
|
792
|
-
index: cell.index,
|
|
793
|
-
axisIndices: cell.axisIndices,
|
|
794
|
-
props: cell.props,
|
|
795
|
-
renderTimeMs: cell.result.renderTimeMs,
|
|
796
|
-
width: cell.result.width,
|
|
797
|
-
height: cell.result.height,
|
|
798
|
-
screenshot: cell.result.screenshot.toString("base64")
|
|
799
|
-
}))
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
function formatMatrixHtml(componentName, result) {
|
|
803
|
-
const cellsHtml = result.cells.map((cell) => {
|
|
804
|
-
const b64 = cell.result.screenshot.toString("base64");
|
|
805
|
-
const propLabel = escapeHtml(
|
|
806
|
-
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
807
|
-
);
|
|
808
|
-
return ` <div class="cell">
|
|
809
|
-
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
810
|
-
<div class="label">${propLabel}</div>
|
|
811
|
-
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
812
|
-
</div>`;
|
|
813
|
-
}).join("\n");
|
|
814
|
-
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
815
|
-
return `<!DOCTYPE html>
|
|
816
|
-
<html lang="en">
|
|
817
|
-
<head>
|
|
818
|
-
<meta charset="UTF-8" />
|
|
819
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
820
|
-
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
821
|
-
<style>
|
|
822
|
-
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
823
|
-
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
824
|
-
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
825
|
-
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
826
|
-
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
827
|
-
.cell img { display: block; }
|
|
828
|
-
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
829
|
-
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
830
|
-
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
831
|
-
</style>
|
|
832
|
-
</head>
|
|
833
|
-
<body>
|
|
834
|
-
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
835
|
-
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
836
|
-
<div class="grid">
|
|
837
|
-
${cellsHtml}
|
|
838
|
-
</div>
|
|
839
|
-
<div class="stats">
|
|
840
|
-
${result.stats.totalCells} cells \xB7
|
|
841
|
-
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
842
|
-
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
843
|
-
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
844
|
-
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
845
|
-
</div>
|
|
846
|
-
</body>
|
|
847
|
-
</html>
|
|
848
|
-
`;
|
|
849
|
-
}
|
|
850
|
-
function formatMatrixCsv(componentName, result) {
|
|
851
|
-
const axisNames = result.axes.map((a) => a.name);
|
|
852
|
-
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
853
|
-
const rows = result.cells.map((cell) => {
|
|
854
|
-
const axisVals = result.axes.map((_, i) => {
|
|
855
|
-
const axisIdx = cell.axisIndices[i];
|
|
856
|
-
const axis = result.axes[i];
|
|
857
|
-
if (axisIdx === void 0 || axis === void 0) return "";
|
|
858
|
-
const val = axis.values[axisIdx];
|
|
859
|
-
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
860
|
-
});
|
|
861
|
-
return [
|
|
862
|
-
csvEscape(componentName),
|
|
863
|
-
...axisVals,
|
|
864
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
865
|
-
String(cell.result.width),
|
|
866
|
-
String(cell.result.height)
|
|
867
|
-
].join(",");
|
|
868
|
-
});
|
|
869
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
870
|
-
`;
|
|
871
|
-
}
|
|
872
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
873
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
874
|
-
const empty = barWidth - filled;
|
|
875
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
876
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
877
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
878
|
-
}
|
|
879
|
-
function formatSummaryText(results, outputDir) {
|
|
880
|
-
const total = results.length;
|
|
881
|
-
const passed = results.filter((r) => r.success).length;
|
|
882
|
-
const failed = total - passed;
|
|
883
|
-
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
884
|
-
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
885
|
-
const lines = [
|
|
886
|
-
"\u2500".repeat(60),
|
|
887
|
-
`Render Summary`,
|
|
888
|
-
"\u2500".repeat(60),
|
|
889
|
-
` Total components : ${total}`,
|
|
890
|
-
` Passed : ${passed}`,
|
|
891
|
-
` Failed : ${failed}`,
|
|
892
|
-
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
893
|
-
` Output dir : ${outputDir}`
|
|
894
|
-
];
|
|
895
|
-
if (failed > 0) {
|
|
896
|
-
lines.push("", " Failed components:");
|
|
897
|
-
for (const r of results) {
|
|
898
|
-
if (!r.success) {
|
|
899
|
-
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
1360
|
+
if (queryParts.length === 0) {
|
|
1361
|
+
process.stderr.write(
|
|
1362
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
|
|
1363
|
+
);
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
const queryDesc = queryParts.join(", ");
|
|
1367
|
+
let entries = Object.entries(manifest.components);
|
|
1368
|
+
if (opts.context !== void 0) {
|
|
1369
|
+
const ctx = opts.context;
|
|
1370
|
+
entries = entries.filter(([, d]) => d.requiredContexts.includes(ctx));
|
|
1371
|
+
}
|
|
1372
|
+
if (opts.hook !== void 0) {
|
|
1373
|
+
const hook = opts.hook;
|
|
1374
|
+
entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
|
|
1375
|
+
}
|
|
1376
|
+
if (opts.complexity !== void 0) {
|
|
1377
|
+
const cls = opts.complexity;
|
|
1378
|
+
entries = entries.filter(([, d]) => d.complexityClass === cls);
|
|
1379
|
+
}
|
|
1380
|
+
if (opts.sideEffects) {
|
|
1381
|
+
entries = entries.filter(([, d]) => {
|
|
1382
|
+
const se = d.sideEffects;
|
|
1383
|
+
return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
if (opts.hasFetch) {
|
|
1387
|
+
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1388
|
+
}
|
|
1389
|
+
const rows = entries.map(([name, d]) => ({
|
|
1390
|
+
name,
|
|
1391
|
+
file: d.filePath,
|
|
1392
|
+
complexityClass: d.complexityClass,
|
|
1393
|
+
hooks: d.detectedHooks.join(", ") || "\u2014",
|
|
1394
|
+
contexts: d.requiredContexts.join(", ") || "\u2014"
|
|
1395
|
+
}));
|
|
1396
|
+
const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
|
|
1397
|
+
process.stdout.write(`${output}
|
|
1398
|
+
`);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1401
|
+
`);
|
|
1402
|
+
process.exit(1);
|
|
900
1403
|
}
|
|
901
1404
|
}
|
|
902
|
-
|
|
903
|
-
lines.push("\u2500".repeat(60));
|
|
904
|
-
return lines.join("\n");
|
|
1405
|
+
);
|
|
905
1406
|
}
|
|
906
|
-
function
|
|
907
|
-
|
|
1407
|
+
function registerGenerate(manifestCmd) {
|
|
1408
|
+
manifestCmd.command("generate").description(
|
|
1409
|
+
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
1410
|
+
).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) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const rootDir = path.resolve(process.cwd(), opts.root ?? ".");
|
|
1413
|
+
const outputPath = path.resolve(process.cwd(), opts.output);
|
|
1414
|
+
const include = opts.include?.split(",").map((s) => s.trim());
|
|
1415
|
+
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
1416
|
+
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
1417
|
+
`);
|
|
1418
|
+
const manifest$1 = await manifest.generateManifest({
|
|
1419
|
+
rootDir,
|
|
1420
|
+
...include !== void 0 && { include },
|
|
1421
|
+
...exclude !== void 0 && { exclude }
|
|
1422
|
+
});
|
|
1423
|
+
const componentCount = Object.keys(manifest$1.components).length;
|
|
1424
|
+
process.stderr.write(`Found ${componentCount} components.
|
|
1425
|
+
`);
|
|
1426
|
+
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
1427
|
+
if (!fs.existsSync(outputDir)) {
|
|
1428
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1429
|
+
}
|
|
1430
|
+
fs.writeFileSync(outputPath, JSON.stringify(manifest$1, null, 2), "utf-8");
|
|
1431
|
+
process.stderr.write(`Manifest written to ${outputPath}
|
|
1432
|
+
`);
|
|
1433
|
+
process.stdout.write(`${outputPath}
|
|
1434
|
+
`);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1437
|
+
`);
|
|
1438
|
+
process.exit(1);
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
908
1441
|
}
|
|
909
|
-
function
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1442
|
+
function createManifestCommand() {
|
|
1443
|
+
const manifestCmd = new commander.Command("manifest").description(
|
|
1444
|
+
"Query and explore the component manifest"
|
|
1445
|
+
);
|
|
1446
|
+
registerList(manifestCmd);
|
|
1447
|
+
registerGet(manifestCmd);
|
|
1448
|
+
registerQuery(manifestCmd);
|
|
1449
|
+
registerGenerate(manifestCmd);
|
|
1450
|
+
return manifestCmd;
|
|
914
1451
|
}
|
|
915
1452
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
916
1453
|
function buildHookInstrumentationScript() {
|
|
@@ -1550,22 +2087,22 @@ Available: ${available}`
|
|
|
1550
2087
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1551
2088
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1552
2089
|
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1553
|
-
var
|
|
1554
|
-
async function
|
|
1555
|
-
if (
|
|
1556
|
-
|
|
2090
|
+
var _pool2 = null;
|
|
2091
|
+
async function getPool2() {
|
|
2092
|
+
if (_pool2 === null) {
|
|
2093
|
+
_pool2 = new render.BrowserPool({
|
|
1557
2094
|
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1558
2095
|
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1559
2096
|
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1560
2097
|
});
|
|
1561
|
-
await
|
|
2098
|
+
await _pool2.init();
|
|
1562
2099
|
}
|
|
1563
|
-
return
|
|
2100
|
+
return _pool2;
|
|
1564
2101
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
if (
|
|
1567
|
-
await
|
|
1568
|
-
|
|
2102
|
+
async function shutdownPool2() {
|
|
2103
|
+
if (_pool2 !== null) {
|
|
2104
|
+
await _pool2.close();
|
|
2105
|
+
_pool2 = null;
|
|
1569
2106
|
}
|
|
1570
2107
|
}
|
|
1571
2108
|
function mapNodeType(node) {
|
|
@@ -1777,7 +2314,7 @@ function formatInstrumentTree(root, showProviderDepth = false) {
|
|
|
1777
2314
|
}
|
|
1778
2315
|
async function runInstrumentTree(options) {
|
|
1779
2316
|
const { componentName, filePath } = options;
|
|
1780
|
-
const pool = await
|
|
2317
|
+
const pool = await getPool2();
|
|
1781
2318
|
const slot = await pool.acquire();
|
|
1782
2319
|
const { page } = slot;
|
|
1783
2320
|
try {
|
|
@@ -1874,7 +2411,7 @@ Available: ${available}`
|
|
|
1874
2411
|
providerDepth: opts.providerDepth,
|
|
1875
2412
|
wastedRenders: opts.wastedRenders
|
|
1876
2413
|
});
|
|
1877
|
-
await
|
|
2414
|
+
await shutdownPool2();
|
|
1878
2415
|
const fmt = resolveFormat2(opts.format);
|
|
1879
2416
|
if (fmt === "json") {
|
|
1880
2417
|
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
@@ -1885,7 +2422,7 @@ Available: ${available}`
|
|
|
1885
2422
|
`);
|
|
1886
2423
|
}
|
|
1887
2424
|
} catch (err) {
|
|
1888
|
-
await
|
|
2425
|
+
await shutdownPool2();
|
|
1889
2426
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1890
2427
|
`);
|
|
1891
2428
|
process.exit(1);
|
|
@@ -2203,22 +2740,22 @@ async function replayInteraction2(page, steps) {
|
|
|
2203
2740
|
}
|
|
2204
2741
|
}
|
|
2205
2742
|
}
|
|
2206
|
-
var
|
|
2207
|
-
async function
|
|
2208
|
-
if (
|
|
2209
|
-
|
|
2743
|
+
var _pool3 = null;
|
|
2744
|
+
async function getPool3() {
|
|
2745
|
+
if (_pool3 === null) {
|
|
2746
|
+
_pool3 = new render.BrowserPool({
|
|
2210
2747
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
2211
2748
|
viewportWidth: 1280,
|
|
2212
2749
|
viewportHeight: 800
|
|
2213
2750
|
});
|
|
2214
|
-
await
|
|
2751
|
+
await _pool3.init();
|
|
2215
2752
|
}
|
|
2216
|
-
return
|
|
2753
|
+
return _pool3;
|
|
2217
2754
|
}
|
|
2218
|
-
async function
|
|
2219
|
-
if (
|
|
2220
|
-
await
|
|
2221
|
-
|
|
2755
|
+
async function shutdownPool3() {
|
|
2756
|
+
if (_pool3 !== null) {
|
|
2757
|
+
await _pool3.close();
|
|
2758
|
+
_pool3 = null;
|
|
2222
2759
|
}
|
|
2223
2760
|
}
|
|
2224
2761
|
async function analyzeRenders(options) {
|
|
@@ -2235,7 +2772,7 @@ Available: ${available}`
|
|
|
2235
2772
|
const rootDir = process.cwd();
|
|
2236
2773
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
2237
2774
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
2238
|
-
const pool = await
|
|
2775
|
+
const pool = await getPool3();
|
|
2239
2776
|
const slot = await pool.acquire();
|
|
2240
2777
|
const { page } = slot;
|
|
2241
2778
|
const startMs = performance.now();
|
|
@@ -2343,7 +2880,7 @@ function createInstrumentRendersCommand() {
|
|
|
2343
2880
|
interaction,
|
|
2344
2881
|
manifestPath: opts.manifest
|
|
2345
2882
|
});
|
|
2346
|
-
await
|
|
2883
|
+
await shutdownPool3();
|
|
2347
2884
|
if (opts.json || !isTTY()) {
|
|
2348
2885
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2349
2886
|
`);
|
|
@@ -2352,7 +2889,7 @@ function createInstrumentRendersCommand() {
|
|
|
2352
2889
|
`);
|
|
2353
2890
|
}
|
|
2354
2891
|
} catch (err) {
|
|
2355
|
-
await
|
|
2892
|
+
await shutdownPool3();
|
|
2356
2893
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2357
2894
|
`);
|
|
2358
2895
|
process.exit(1);
|
|
@@ -2400,151 +2937,34 @@ async function browserCapture(options) {
|
|
|
2400
2937
|
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
2401
2938
|
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
2402
2939
|
}
|
|
2403
|
-
const report = { ...raw, route: null };
|
|
2404
|
-
return { report };
|
|
2405
|
-
} finally {
|
|
2406
|
-
await browser.close();
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
function writeReportToFile(report, outputPath, pretty) {
|
|
2410
|
-
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2411
|
-
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2412
|
-
}
|
|
2413
|
-
var CONFIG_FILENAMES = [
|
|
2414
|
-
".reactscope/config.json",
|
|
2415
|
-
".reactscope/config.js",
|
|
2416
|
-
".reactscope/config.mjs"
|
|
2417
|
-
];
|
|
2418
|
-
var STYLE_ENTRY_CANDIDATES = [
|
|
2419
|
-
"src/index.css",
|
|
2420
|
-
"src/globals.css",
|
|
2421
|
-
"app/globals.css",
|
|
2422
|
-
"app/index.css",
|
|
2423
|
-
"styles/index.css",
|
|
2424
|
-
"index.css"
|
|
2425
|
-
];
|
|
2426
|
-
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2427
|
-
var compilerCache = null;
|
|
2428
|
-
function getCachedBuild(cwd) {
|
|
2429
|
-
if (compilerCache !== null && path.resolve(compilerCache.cwd) === path.resolve(cwd)) {
|
|
2430
|
-
return compilerCache.build;
|
|
2431
|
-
}
|
|
2432
|
-
return null;
|
|
2433
|
-
}
|
|
2434
|
-
function findStylesEntry(cwd) {
|
|
2435
|
-
for (const name of CONFIG_FILENAMES) {
|
|
2436
|
-
const p = path.resolve(cwd, name);
|
|
2437
|
-
if (!fs.existsSync(p)) continue;
|
|
2438
|
-
try {
|
|
2439
|
-
if (name.endsWith(".json")) {
|
|
2440
|
-
const raw = fs.readFileSync(p, "utf-8");
|
|
2441
|
-
const data = JSON.parse(raw);
|
|
2442
|
-
const scope = data.scope;
|
|
2443
|
-
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2444
|
-
if (typeof entry === "string") {
|
|
2445
|
-
const full = path.resolve(cwd, entry);
|
|
2446
|
-
if (fs.existsSync(full)) return full;
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
} catch {
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
const pkgPath = path.resolve(cwd, "package.json");
|
|
2453
|
-
if (fs.existsSync(pkgPath)) {
|
|
2454
|
-
try {
|
|
2455
|
-
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
2456
|
-
const pkg = JSON.parse(raw);
|
|
2457
|
-
const entry = pkg.scope?.stylesEntry;
|
|
2458
|
-
if (typeof entry === "string") {
|
|
2459
|
-
const full = path.resolve(cwd, entry);
|
|
2460
|
-
if (fs.existsSync(full)) return full;
|
|
2461
|
-
}
|
|
2462
|
-
} catch {
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2466
|
-
const full = path.resolve(cwd, candidate);
|
|
2467
|
-
if (fs.existsSync(full)) {
|
|
2468
|
-
try {
|
|
2469
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
2470
|
-
if (TAILWIND_IMPORT.test(content)) return full;
|
|
2471
|
-
} catch {
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
return null;
|
|
2476
|
-
}
|
|
2477
|
-
async function getTailwindCompiler(cwd) {
|
|
2478
|
-
const cached = getCachedBuild(cwd);
|
|
2479
|
-
if (cached !== null) return cached;
|
|
2480
|
-
const entryPath = findStylesEntry(cwd);
|
|
2481
|
-
if (entryPath === null) return null;
|
|
2482
|
-
let compile;
|
|
2483
|
-
try {
|
|
2484
|
-
const require2 = module$1.createRequire(path.resolve(cwd, "package.json"));
|
|
2485
|
-
const tailwind = require2("tailwindcss");
|
|
2486
|
-
const fn = tailwind.compile;
|
|
2487
|
-
if (typeof fn !== "function") return null;
|
|
2488
|
-
compile = fn;
|
|
2489
|
-
} catch {
|
|
2490
|
-
return null;
|
|
2491
|
-
}
|
|
2492
|
-
const entryContent = fs.readFileSync(entryPath, "utf-8");
|
|
2493
|
-
const loadStylesheet = async (id, base) => {
|
|
2494
|
-
if (id === "tailwindcss") {
|
|
2495
|
-
const nodeModules = path.resolve(cwd, "node_modules");
|
|
2496
|
-
const tailwindCssPath = path.resolve(nodeModules, "tailwindcss", "index.css");
|
|
2497
|
-
if (!fs.existsSync(tailwindCssPath)) {
|
|
2498
|
-
throw new Error(
|
|
2499
|
-
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
2500
|
-
);
|
|
2501
|
-
}
|
|
2502
|
-
const content = fs.readFileSync(tailwindCssPath, "utf-8");
|
|
2503
|
-
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2504
|
-
}
|
|
2505
|
-
const full = path.resolve(base, id);
|
|
2506
|
-
if (fs.existsSync(full)) {
|
|
2507
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
2508
|
-
return { path: full, base: path.resolve(full, ".."), content };
|
|
2509
|
-
}
|
|
2510
|
-
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2511
|
-
};
|
|
2512
|
-
const result = await compile(entryContent, {
|
|
2513
|
-
base: cwd,
|
|
2514
|
-
from: entryPath,
|
|
2515
|
-
loadStylesheet
|
|
2516
|
-
});
|
|
2517
|
-
const build2 = result.build.bind(result);
|
|
2518
|
-
compilerCache = { cwd, build: build2 };
|
|
2519
|
-
return build2;
|
|
2940
|
+
const report = { ...raw, route: null };
|
|
2941
|
+
return { report };
|
|
2942
|
+
} finally {
|
|
2943
|
+
await browser.close();
|
|
2944
|
+
}
|
|
2520
2945
|
}
|
|
2521
|
-
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
const deduped = [...new Set(classes)].filter(Boolean);
|
|
2525
|
-
if (deduped.length === 0) return null;
|
|
2526
|
-
return build2(deduped);
|
|
2946
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
2947
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2948
|
+
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2527
2949
|
}
|
|
2528
|
-
|
|
2529
|
-
// src/render-commands.ts
|
|
2530
2950
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2531
2951
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2532
|
-
var
|
|
2533
|
-
async function
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
2952
|
+
var _pool4 = null;
|
|
2953
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
2954
|
+
if (_pool4 === null) {
|
|
2955
|
+
_pool4 = new render.BrowserPool({
|
|
2536
2956
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2537
2957
|
viewportWidth,
|
|
2538
2958
|
viewportHeight
|
|
2539
2959
|
});
|
|
2540
|
-
await
|
|
2960
|
+
await _pool4.init();
|
|
2541
2961
|
}
|
|
2542
|
-
return
|
|
2962
|
+
return _pool4;
|
|
2543
2963
|
}
|
|
2544
|
-
async function
|
|
2545
|
-
if (
|
|
2546
|
-
await
|
|
2547
|
-
|
|
2964
|
+
async function shutdownPool4() {
|
|
2965
|
+
if (_pool4 !== null) {
|
|
2966
|
+
await _pool4.close();
|
|
2967
|
+
_pool4 = null;
|
|
2548
2968
|
}
|
|
2549
2969
|
}
|
|
2550
2970
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2555,7 +2975,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2555
2975
|
_satori: satori,
|
|
2556
2976
|
async renderCell(props, _complexityClass) {
|
|
2557
2977
|
const startMs = performance.now();
|
|
2558
|
-
const pool = await
|
|
2978
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2559
2979
|
const htmlHarness = await buildComponentHarness(
|
|
2560
2980
|
filePath,
|
|
2561
2981
|
componentName,
|
|
@@ -2691,7 +3111,7 @@ Available: ${available}`
|
|
|
2691
3111
|
}
|
|
2692
3112
|
}
|
|
2693
3113
|
);
|
|
2694
|
-
await
|
|
3114
|
+
await shutdownPool4();
|
|
2695
3115
|
if (outcome.crashed) {
|
|
2696
3116
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2697
3117
|
`);
|
|
@@ -2739,7 +3159,7 @@ Available: ${available}`
|
|
|
2739
3159
|
);
|
|
2740
3160
|
}
|
|
2741
3161
|
} catch (err) {
|
|
2742
|
-
await
|
|
3162
|
+
await shutdownPool4();
|
|
2743
3163
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2744
3164
|
`);
|
|
2745
3165
|
process.exit(1);
|
|
@@ -2824,14 +3244,14 @@ Available: ${available}`
|
|
|
2824
3244
|
concurrency
|
|
2825
3245
|
});
|
|
2826
3246
|
const result = await matrix.render();
|
|
2827
|
-
await
|
|
3247
|
+
await shutdownPool4();
|
|
2828
3248
|
process.stderr.write(
|
|
2829
3249
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2830
3250
|
`
|
|
2831
3251
|
);
|
|
2832
3252
|
if (opts.sprite !== void 0) {
|
|
2833
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2834
|
-
const gen = new
|
|
3253
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3254
|
+
const gen = new SpriteSheetGenerator2();
|
|
2835
3255
|
const sheet = await gen.generate(result);
|
|
2836
3256
|
const spritePath = path.resolve(process.cwd(), opts.sprite);
|
|
2837
3257
|
fs.writeFileSync(spritePath, sheet.png);
|
|
@@ -2840,8 +3260,8 @@ Available: ${available}`
|
|
|
2840
3260
|
}
|
|
2841
3261
|
const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
2842
3262
|
if (fmt === "file") {
|
|
2843
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2844
|
-
const gen = new
|
|
3263
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3264
|
+
const gen = new SpriteSheetGenerator2();
|
|
2845
3265
|
const sheet = await gen.generate(result);
|
|
2846
3266
|
const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2847
3267
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -2858,8 +3278,8 @@ Available: ${available}`
|
|
|
2858
3278
|
} else if (fmt === "png") {
|
|
2859
3279
|
if (opts.sprite !== void 0) {
|
|
2860
3280
|
} else {
|
|
2861
|
-
const { SpriteSheetGenerator } = await import('@agent-scope/render');
|
|
2862
|
-
const gen = new
|
|
3281
|
+
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
3282
|
+
const gen = new SpriteSheetGenerator2();
|
|
2863
3283
|
const sheet = await gen.generate(result);
|
|
2864
3284
|
process.stdout.write(sheet.png);
|
|
2865
3285
|
}
|
|
@@ -2869,7 +3289,7 @@ Available: ${available}`
|
|
|
2869
3289
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2870
3290
|
}
|
|
2871
3291
|
} catch (err) {
|
|
2872
|
-
await
|
|
3292
|
+
await shutdownPool4();
|
|
2873
3293
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2874
3294
|
`);
|
|
2875
3295
|
process.exit(1);
|
|
@@ -2966,13 +3386,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2966
3386
|
workers.push(worker());
|
|
2967
3387
|
}
|
|
2968
3388
|
await Promise.all(workers);
|
|
2969
|
-
await
|
|
3389
|
+
await shutdownPool4();
|
|
2970
3390
|
process.stderr.write("\n");
|
|
2971
3391
|
const summary = formatSummaryText(results, outputDir);
|
|
2972
3392
|
process.stderr.write(`${summary}
|
|
2973
3393
|
`);
|
|
2974
3394
|
} catch (err) {
|
|
2975
|
-
await
|
|
3395
|
+
await shutdownPool4();
|
|
2976
3396
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2977
3397
|
`);
|
|
2978
3398
|
process.exit(1);
|
|
@@ -3014,26 +3434,26 @@ function createRenderCommand() {
|
|
|
3014
3434
|
return renderCmd;
|
|
3015
3435
|
}
|
|
3016
3436
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
3017
|
-
var
|
|
3018
|
-
async function
|
|
3019
|
-
if (
|
|
3020
|
-
|
|
3437
|
+
var _pool5 = null;
|
|
3438
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3439
|
+
if (_pool5 === null) {
|
|
3440
|
+
_pool5 = new render.BrowserPool({
|
|
3021
3441
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3022
3442
|
viewportWidth,
|
|
3023
3443
|
viewportHeight
|
|
3024
3444
|
});
|
|
3025
|
-
await
|
|
3445
|
+
await _pool5.init();
|
|
3026
3446
|
}
|
|
3027
|
-
return
|
|
3447
|
+
return _pool5;
|
|
3028
3448
|
}
|
|
3029
|
-
async function
|
|
3030
|
-
if (
|
|
3031
|
-
await
|
|
3032
|
-
|
|
3449
|
+
async function shutdownPool5() {
|
|
3450
|
+
if (_pool5 !== null) {
|
|
3451
|
+
await _pool5.close();
|
|
3452
|
+
_pool5 = null;
|
|
3033
3453
|
}
|
|
3034
3454
|
}
|
|
3035
|
-
async function
|
|
3036
|
-
const pool = await
|
|
3455
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3456
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3037
3457
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3038
3458
|
const slot = await pool.acquire();
|
|
3039
3459
|
const { page } = slot;
|
|
@@ -3123,7 +3543,7 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
|
|
|
3123
3543
|
pool.release(slot);
|
|
3124
3544
|
}
|
|
3125
3545
|
}
|
|
3126
|
-
function
|
|
3546
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3127
3547
|
const flat = {};
|
|
3128
3548
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3129
3549
|
Object.assign(flat, styles);
|
|
@@ -3166,12 +3586,12 @@ async function runBaseline(options = {}) {
|
|
|
3166
3586
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3167
3587
|
let manifest$1;
|
|
3168
3588
|
if (manifestPath !== void 0) {
|
|
3169
|
-
const { readFileSync:
|
|
3589
|
+
const { readFileSync: readFileSync11 } = await import('fs');
|
|
3170
3590
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3171
3591
|
if (!fs.existsSync(absPath)) {
|
|
3172
3592
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3173
3593
|
}
|
|
3174
|
-
manifest$1 = JSON.parse(
|
|
3594
|
+
manifest$1 = JSON.parse(readFileSync11(absPath, "utf-8"));
|
|
3175
3595
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3176
3596
|
`);
|
|
3177
3597
|
} else {
|
|
@@ -3225,7 +3645,7 @@ async function runBaseline(options = {}) {
|
|
|
3225
3645
|
if (descriptor === void 0) return;
|
|
3226
3646
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3227
3647
|
const outcome = await render.safeRender(
|
|
3228
|
-
() =>
|
|
3648
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3229
3649
|
{
|
|
3230
3650
|
props: {},
|
|
3231
3651
|
sourceLocation: {
|
|
@@ -3267,7 +3687,7 @@ async function runBaseline(options = {}) {
|
|
|
3267
3687
|
JSON.stringify(jsonOutput, null, 2),
|
|
3268
3688
|
"utf-8"
|
|
3269
3689
|
);
|
|
3270
|
-
computedStylesMap.set(name,
|
|
3690
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3271
3691
|
};
|
|
3272
3692
|
const worker = async () => {
|
|
3273
3693
|
while (nextIdx < componentNames.length) {
|
|
@@ -3283,7 +3703,7 @@ async function runBaseline(options = {}) {
|
|
|
3283
3703
|
workers.push(worker());
|
|
3284
3704
|
}
|
|
3285
3705
|
await Promise.all(workers);
|
|
3286
|
-
await
|
|
3706
|
+
await shutdownPool5();
|
|
3287
3707
|
if (isTTY()) {
|
|
3288
3708
|
process.stderr.write("\n");
|
|
3289
3709
|
}
|
|
@@ -3339,31 +3759,31 @@ function loadBaselineCompliance(baselineDir) {
|
|
|
3339
3759
|
const raw = JSON.parse(fs.readFileSync(compliancePath, "utf-8"));
|
|
3340
3760
|
return raw;
|
|
3341
3761
|
}
|
|
3342
|
-
function
|
|
3762
|
+
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3343
3763
|
const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3344
3764
|
if (!fs.existsSync(jsonPath)) return null;
|
|
3345
3765
|
return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
3346
3766
|
}
|
|
3347
|
-
var
|
|
3348
|
-
async function
|
|
3349
|
-
if (
|
|
3350
|
-
|
|
3767
|
+
var _pool6 = null;
|
|
3768
|
+
async function getPool6(viewportWidth, viewportHeight) {
|
|
3769
|
+
if (_pool6 === null) {
|
|
3770
|
+
_pool6 = new render.BrowserPool({
|
|
3351
3771
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3352
3772
|
viewportWidth,
|
|
3353
3773
|
viewportHeight
|
|
3354
3774
|
});
|
|
3355
|
-
await
|
|
3775
|
+
await _pool6.init();
|
|
3356
3776
|
}
|
|
3357
|
-
return
|
|
3777
|
+
return _pool6;
|
|
3358
3778
|
}
|
|
3359
|
-
async function
|
|
3360
|
-
if (
|
|
3361
|
-
await
|
|
3362
|
-
|
|
3779
|
+
async function shutdownPool6() {
|
|
3780
|
+
if (_pool6 !== null) {
|
|
3781
|
+
await _pool6.close();
|
|
3782
|
+
_pool6 = null;
|
|
3363
3783
|
}
|
|
3364
3784
|
}
|
|
3365
|
-
async function
|
|
3366
|
-
const pool = await
|
|
3785
|
+
async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3786
|
+
const pool = await getPool6(viewportWidth, viewportHeight);
|
|
3367
3787
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3368
3788
|
const slot = await pool.acquire();
|
|
3369
3789
|
const { page } = slot;
|
|
@@ -3453,7 +3873,7 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
|
|
|
3453
3873
|
pool.release(slot);
|
|
3454
3874
|
}
|
|
3455
3875
|
}
|
|
3456
|
-
function
|
|
3876
|
+
function extractComputedStyles3(computedStylesRaw) {
|
|
3457
3877
|
const flat = {};
|
|
3458
3878
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3459
3879
|
Object.assign(flat, styles);
|
|
@@ -3569,7 +3989,7 @@ async function runDiff(options = {}) {
|
|
|
3569
3989
|
if (descriptor === void 0) return;
|
|
3570
3990
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3571
3991
|
const outcome = await render.safeRender(
|
|
3572
|
-
() =>
|
|
3992
|
+
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3573
3993
|
{
|
|
3574
3994
|
props: {},
|
|
3575
3995
|
sourceLocation: {
|
|
@@ -3594,7 +4014,7 @@ async function runDiff(options = {}) {
|
|
|
3594
4014
|
height: result.height,
|
|
3595
4015
|
renderTimeMs: result.renderTimeMs
|
|
3596
4016
|
});
|
|
3597
|
-
computedStylesMap.set(name,
|
|
4017
|
+
computedStylesMap.set(name, extractComputedStyles3(result.computedStyles));
|
|
3598
4018
|
};
|
|
3599
4019
|
if (total > 0) {
|
|
3600
4020
|
const worker = async () => {
|
|
@@ -3612,7 +4032,7 @@ async function runDiff(options = {}) {
|
|
|
3612
4032
|
}
|
|
3613
4033
|
await Promise.all(workers);
|
|
3614
4034
|
}
|
|
3615
|
-
await
|
|
4035
|
+
await shutdownPool6();
|
|
3616
4036
|
if (isTTY() && total > 0) {
|
|
3617
4037
|
process.stderr.write("\n");
|
|
3618
4038
|
}
|
|
@@ -3623,7 +4043,7 @@ async function runDiff(options = {}) {
|
|
|
3623
4043
|
for (const name of componentNames) {
|
|
3624
4044
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3625
4045
|
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3626
|
-
const baselineMeta =
|
|
4046
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3627
4047
|
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3628
4048
|
const failed = renderFailures.has(name);
|
|
3629
4049
|
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
@@ -3643,7 +4063,7 @@ async function runDiff(options = {}) {
|
|
|
3643
4063
|
}
|
|
3644
4064
|
for (const name of removedNames) {
|
|
3645
4065
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3646
|
-
const baselineMeta =
|
|
4066
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3647
4067
|
entries.push({
|
|
3648
4068
|
name,
|
|
3649
4069
|
status: "removed",
|
|
@@ -4083,10 +4503,178 @@ function buildStructuredReport(report) {
|
|
|
4083
4503
|
route: report.route?.pattern ?? null
|
|
4084
4504
|
};
|
|
4085
4505
|
}
|
|
4506
|
+
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4507
|
+
function loadStylesFile(stylesPath) {
|
|
4508
|
+
const absPath = path.resolve(process.cwd(), stylesPath);
|
|
4509
|
+
if (!fs.existsSync(absPath)) {
|
|
4510
|
+
throw new Error(
|
|
4511
|
+
`Compliance styles file not found at ${absPath}.
|
|
4512
|
+
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4513
|
+
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4514
|
+
);
|
|
4515
|
+
}
|
|
4516
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
4517
|
+
let parsed;
|
|
4518
|
+
try {
|
|
4519
|
+
parsed = JSON.parse(raw);
|
|
4520
|
+
} catch (err) {
|
|
4521
|
+
throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
|
|
4522
|
+
}
|
|
4523
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4524
|
+
throw new Error(
|
|
4525
|
+
`Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
|
|
4526
|
+
);
|
|
4527
|
+
}
|
|
4528
|
+
return parsed;
|
|
4529
|
+
}
|
|
4530
|
+
function categoryForProperty(property) {
|
|
4531
|
+
const lower = property.toLowerCase();
|
|
4532
|
+
if (lower.includes("shadow")) return "shadow";
|
|
4533
|
+
if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
|
|
4534
|
+
return "color";
|
|
4535
|
+
if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
|
|
4536
|
+
return "spacing";
|
|
4537
|
+
if (lower.includes("border")) return "border";
|
|
4538
|
+
if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
|
|
4539
|
+
return "typography";
|
|
4540
|
+
return "spacing";
|
|
4541
|
+
}
|
|
4542
|
+
function buildCategorySummary(batch) {
|
|
4543
|
+
const cats = {
|
|
4544
|
+
color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4545
|
+
spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4546
|
+
typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4547
|
+
border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
|
|
4548
|
+
shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
|
|
4549
|
+
};
|
|
4550
|
+
for (const report of Object.values(batch.components)) {
|
|
4551
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4552
|
+
const cat = categoryForProperty(property);
|
|
4553
|
+
const summary = cats[cat];
|
|
4554
|
+
if (summary === void 0) continue;
|
|
4555
|
+
summary.total++;
|
|
4556
|
+
if (result.status === "on_system") {
|
|
4557
|
+
summary.onSystem++;
|
|
4558
|
+
} else {
|
|
4559
|
+
summary.offSystem++;
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
for (const summary of Object.values(cats)) {
|
|
4564
|
+
summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
|
|
4565
|
+
}
|
|
4566
|
+
return cats;
|
|
4567
|
+
}
|
|
4568
|
+
function collectOffenders(batch, limit = 10) {
|
|
4569
|
+
const offenders = [];
|
|
4570
|
+
const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
|
|
4571
|
+
name,
|
|
4572
|
+
report,
|
|
4573
|
+
offSystemCount: report.offSystem
|
|
4574
|
+
}));
|
|
4575
|
+
componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
|
|
4576
|
+
for (const { name, report, offSystemCount } of componentEntries) {
|
|
4577
|
+
if (offSystemCount === 0) continue;
|
|
4578
|
+
for (const [property, result] of Object.entries(report.properties)) {
|
|
4579
|
+
if (result.status !== "OFF_SYSTEM") continue;
|
|
4580
|
+
offenders.push({
|
|
4581
|
+
component: name,
|
|
4582
|
+
property,
|
|
4583
|
+
value: result.value,
|
|
4584
|
+
nearestToken: result.nearest?.token ?? "\u2014",
|
|
4585
|
+
nearestValue: result.nearest?.value ?? "\u2014",
|
|
4586
|
+
offSystemCount
|
|
4587
|
+
});
|
|
4588
|
+
if (offenders.length >= limit) break;
|
|
4589
|
+
}
|
|
4590
|
+
if (offenders.length >= limit) break;
|
|
4591
|
+
}
|
|
4592
|
+
return offenders;
|
|
4593
|
+
}
|
|
4594
|
+
function formatPct(n) {
|
|
4595
|
+
return `${Math.round(n * 100)}%`;
|
|
4596
|
+
}
|
|
4597
|
+
function truncate(s, max) {
|
|
4598
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4599
|
+
}
|
|
4600
|
+
function formatComplianceReport(batch, threshold) {
|
|
4601
|
+
const pct = Math.round(batch.aggregateCompliance * 100);
|
|
4602
|
+
const lines = [];
|
|
4603
|
+
const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
|
|
4604
|
+
lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
|
|
4605
|
+
lines.push("");
|
|
4606
|
+
const cats = buildCategorySummary(batch);
|
|
4607
|
+
const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
|
|
4608
|
+
if (catEntries.length > 0) {
|
|
4609
|
+
lines.push("By category:");
|
|
4610
|
+
const catWidth = Math.max(...catEntries.map(([k]) => k.length));
|
|
4611
|
+
for (const [cat, summary] of catEntries) {
|
|
4612
|
+
const label = cat.padEnd(catWidth);
|
|
4613
|
+
lines.push(
|
|
4614
|
+
` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
lines.push("");
|
|
4618
|
+
}
|
|
4619
|
+
const offenders = collectOffenders(batch);
|
|
4620
|
+
if (offenders.length > 0) {
|
|
4621
|
+
lines.push("Top off-system offenders (sorted by count):");
|
|
4622
|
+
const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
|
|
4623
|
+
const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
|
|
4624
|
+
const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
|
|
4625
|
+
for (const offender of offenders) {
|
|
4626
|
+
const name = offender.component.padEnd(nameWidth);
|
|
4627
|
+
const prop = offender.property.padEnd(propWidth);
|
|
4628
|
+
const val = truncate(offender.value, 40).padEnd(valWidth);
|
|
4629
|
+
const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
|
|
4630
|
+
lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
|
|
4631
|
+
}
|
|
4632
|
+
} else {
|
|
4633
|
+
lines.push("No off-system values detected. \u{1F389}");
|
|
4634
|
+
}
|
|
4635
|
+
return lines.join("\n");
|
|
4636
|
+
}
|
|
4637
|
+
function registerCompliance(tokensCmd) {
|
|
4638
|
+
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) => {
|
|
4639
|
+
try {
|
|
4640
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4641
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4642
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4643
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4644
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
|
|
4645
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4646
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
4647
|
+
for (const [name, styles] of Object.entries(stylesFile)) {
|
|
4648
|
+
componentMap.set(name, styles);
|
|
4649
|
+
}
|
|
4650
|
+
if (componentMap.size === 0) {
|
|
4651
|
+
process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
|
|
4652
|
+
`);
|
|
4653
|
+
}
|
|
4654
|
+
const batch = engine.auditBatch(componentMap);
|
|
4655
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4656
|
+
const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
|
|
4657
|
+
if (useJson) {
|
|
4658
|
+
process.stdout.write(`${JSON.stringify(batch, null, 2)}
|
|
4659
|
+
`);
|
|
4660
|
+
} else {
|
|
4661
|
+
process.stdout.write(`${formatComplianceReport(batch, threshold)}
|
|
4662
|
+
`);
|
|
4663
|
+
}
|
|
4664
|
+
if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
|
|
4665
|
+
process.exit(1);
|
|
4666
|
+
}
|
|
4667
|
+
} catch (err) {
|
|
4668
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4669
|
+
`);
|
|
4670
|
+
process.exit(1);
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
}
|
|
4086
4674
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
4087
4675
|
var CONFIG_FILE = "reactscope.config.json";
|
|
4088
4676
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
4089
|
-
function
|
|
4677
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
4090
4678
|
if (fileFlag !== void 0) {
|
|
4091
4679
|
return path.resolve(process.cwd(), fileFlag);
|
|
4092
4680
|
}
|
|
@@ -4120,7 +4708,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
4120
4708
|
}
|
|
4121
4709
|
const format = opts.format;
|
|
4122
4710
|
try {
|
|
4123
|
-
const filePath =
|
|
4711
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4124
4712
|
if (!fs.existsSync(filePath)) {
|
|
4125
4713
|
throw new Error(
|
|
4126
4714
|
`Token file not found at ${filePath}.
|
|
@@ -4184,6 +4772,311 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
4184
4772
|
}
|
|
4185
4773
|
);
|
|
4186
4774
|
}
|
|
4775
|
+
var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
|
|
4776
|
+
var SEVERITY_EMOJI = {
|
|
4777
|
+
none: "\u25CB",
|
|
4778
|
+
subtle: "\u25D4",
|
|
4779
|
+
moderate: "\u25D1",
|
|
4780
|
+
significant: "\u25CF"
|
|
4781
|
+
};
|
|
4782
|
+
function formatImpactReport(report) {
|
|
4783
|
+
const lines = [];
|
|
4784
|
+
const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
|
|
4785
|
+
lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
|
|
4786
|
+
if (report.components.length === 0) {
|
|
4787
|
+
lines.push("");
|
|
4788
|
+
lines.push("No components reference this token.");
|
|
4789
|
+
return lines.join("\n");
|
|
4790
|
+
}
|
|
4791
|
+
lines.push("");
|
|
4792
|
+
const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
|
|
4793
|
+
const propWidth = Math.max(
|
|
4794
|
+
8,
|
|
4795
|
+
...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
|
|
4796
|
+
);
|
|
4797
|
+
for (const comp of report.components) {
|
|
4798
|
+
for (const property of comp.affectedProperties) {
|
|
4799
|
+
const name = comp.name.padEnd(nameWidth);
|
|
4800
|
+
const prop = property.padEnd(propWidth);
|
|
4801
|
+
const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
|
|
4802
|
+
lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
lines.push("");
|
|
4806
|
+
const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
|
|
4807
|
+
const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
|
|
4808
|
+
lines.push(
|
|
4809
|
+
`${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
|
|
4810
|
+
);
|
|
4811
|
+
if (report.colorDelta !== void 0) {
|
|
4812
|
+
lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
|
|
4813
|
+
}
|
|
4814
|
+
return lines.join("\n");
|
|
4815
|
+
}
|
|
4816
|
+
function formatImpactSummary(report) {
|
|
4817
|
+
if (report.components.length === 0) {
|
|
4818
|
+
return `No components reference token "${report.tokenPath}".`;
|
|
4819
|
+
}
|
|
4820
|
+
const parts = report.components.map(
|
|
4821
|
+
(c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
|
|
4822
|
+
);
|
|
4823
|
+
return `\u2192 ${parts.join(", ")}`;
|
|
4824
|
+
}
|
|
4825
|
+
function registerImpact(tokensCmd) {
|
|
4826
|
+
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(
|
|
4827
|
+
(tokenPath, opts) => {
|
|
4828
|
+
try {
|
|
4829
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4830
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4831
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4832
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4833
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
|
|
4834
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4835
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4836
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4837
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4838
|
+
const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
|
|
4839
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4840
|
+
const newValue = opts.newValue ?? currentValue;
|
|
4841
|
+
const report = analyzer.impactOf(tokenPath, newValue);
|
|
4842
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
4843
|
+
if (useJson) {
|
|
4844
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4845
|
+
`);
|
|
4846
|
+
} else {
|
|
4847
|
+
process.stdout.write(`${formatImpactReport(report)}
|
|
4848
|
+
`);
|
|
4849
|
+
if (isTTY()) {
|
|
4850
|
+
process.stdout.write(`
|
|
4851
|
+
${formatImpactSummary(report)}
|
|
4852
|
+
`);
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
} catch (err) {
|
|
4856
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4857
|
+
`);
|
|
4858
|
+
process.exit(1);
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
);
|
|
4862
|
+
}
|
|
4863
|
+
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
4864
|
+
var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
|
|
4865
|
+
var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
|
|
4866
|
+
async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
|
|
4867
|
+
const htmlHarness = await buildComponentHarness(
|
|
4868
|
+
filePath,
|
|
4869
|
+
componentName,
|
|
4870
|
+
{},
|
|
4871
|
+
// no props
|
|
4872
|
+
vpWidth,
|
|
4873
|
+
cssOverride
|
|
4874
|
+
// injected as <style>
|
|
4875
|
+
);
|
|
4876
|
+
const pool = new render.BrowserPool({
|
|
4877
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
4878
|
+
viewportWidth: vpWidth,
|
|
4879
|
+
viewportHeight: vpHeight
|
|
4880
|
+
});
|
|
4881
|
+
await pool.init();
|
|
4882
|
+
const slot = await pool.acquire();
|
|
4883
|
+
const { page } = slot;
|
|
4884
|
+
try {
|
|
4885
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
4886
|
+
await page.waitForFunction(
|
|
4887
|
+
() => {
|
|
4888
|
+
const w = window;
|
|
4889
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
4890
|
+
},
|
|
4891
|
+
{ timeout: timeoutMs }
|
|
4892
|
+
);
|
|
4893
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
4894
|
+
const bb = await rootLocator.boundingBox();
|
|
4895
|
+
const PAD = 16;
|
|
4896
|
+
const MIN_W = 320;
|
|
4897
|
+
const MIN_H = 120;
|
|
4898
|
+
const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
|
|
4899
|
+
const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
|
|
4900
|
+
const rawW = (bb?.width ?? MIN_W) + PAD * 2;
|
|
4901
|
+
const rawH = (bb?.height ?? MIN_H) + PAD * 2;
|
|
4902
|
+
const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
|
|
4903
|
+
const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
|
|
4904
|
+
const screenshot = await page.screenshot({
|
|
4905
|
+
clip: { x: clipX, y: clipY, width: clipW, height: clipH },
|
|
4906
|
+
type: "png"
|
|
4907
|
+
});
|
|
4908
|
+
return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
|
|
4909
|
+
} finally {
|
|
4910
|
+
pool.release(slot);
|
|
4911
|
+
await pool.close().catch(() => void 0);
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
function registerPreview(tokensCmd) {
|
|
4915
|
+
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(
|
|
4916
|
+
async (tokenPath, opts) => {
|
|
4917
|
+
try {
|
|
4918
|
+
const tokenFilePath = resolveTokenFilePath(opts.file);
|
|
4919
|
+
const { tokens: tokens$1 } = loadTokens(tokenFilePath);
|
|
4920
|
+
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4921
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
4922
|
+
const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
|
|
4923
|
+
const stylesFile = loadStylesFile(stylesPath);
|
|
4924
|
+
const componentMap = new Map(Object.entries(stylesFile));
|
|
4925
|
+
const batchReport = engine.auditBatch(componentMap);
|
|
4926
|
+
const complianceReports = new Map(Object.entries(batchReport.components));
|
|
4927
|
+
const analyzer = new tokens.ImpactAnalyzer(resolver, complianceReports);
|
|
4928
|
+
const currentValue = resolver.resolve(tokenPath);
|
|
4929
|
+
const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
|
|
4930
|
+
if (impactReport.components.length === 0) {
|
|
4931
|
+
process.stdout.write(
|
|
4932
|
+
`No components reference token "${tokenPath}". Nothing to preview.
|
|
4933
|
+
`
|
|
4934
|
+
);
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4937
|
+
const affectedNames = impactReport.components.map((c) => c.name);
|
|
4938
|
+
process.stderr.write(
|
|
4939
|
+
`Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
|
|
4940
|
+
`
|
|
4941
|
+
);
|
|
4942
|
+
const manifest = loadManifest(opts.manifest);
|
|
4943
|
+
const vpWidth = Number.parseInt(opts.viewportWidth, 10);
|
|
4944
|
+
const vpHeight = Number.parseInt(opts.viewportHeight, 10);
|
|
4945
|
+
const timeout = Number.parseInt(opts.timeout, 10);
|
|
4946
|
+
const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
|
|
4947
|
+
const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
|
|
4948
|
+
const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
|
|
4949
|
+
const renders = [];
|
|
4950
|
+
for (const componentName of affectedNames) {
|
|
4951
|
+
const descriptor = manifest.components[componentName];
|
|
4952
|
+
if (descriptor === void 0) {
|
|
4953
|
+
process.stderr.write(
|
|
4954
|
+
`Warning: "${componentName}" not found in manifest \u2014 skipping
|
|
4955
|
+
`
|
|
4956
|
+
);
|
|
4957
|
+
continue;
|
|
4958
|
+
}
|
|
4959
|
+
process.stderr.write(` Rendering ${componentName} (before)...
|
|
4960
|
+
`);
|
|
4961
|
+
const before = await renderComponentWithCssOverride(
|
|
4962
|
+
descriptor.filePath,
|
|
4963
|
+
componentName,
|
|
4964
|
+
beforeCss,
|
|
4965
|
+
vpWidth,
|
|
4966
|
+
vpHeight,
|
|
4967
|
+
timeout
|
|
4968
|
+
);
|
|
4969
|
+
process.stderr.write(` Rendering ${componentName} (after)...
|
|
4970
|
+
`);
|
|
4971
|
+
const after = await renderComponentWithCssOverride(
|
|
4972
|
+
descriptor.filePath,
|
|
4973
|
+
componentName,
|
|
4974
|
+
afterCss,
|
|
4975
|
+
vpWidth,
|
|
4976
|
+
vpHeight,
|
|
4977
|
+
timeout
|
|
4978
|
+
);
|
|
4979
|
+
renders.push({ name: componentName, before, after });
|
|
4980
|
+
}
|
|
4981
|
+
if (renders.length === 0) {
|
|
4982
|
+
process.stderr.write(
|
|
4983
|
+
"Warning: No components could be rendered (all missing from manifest).\n"
|
|
4984
|
+
);
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
|
|
4988
|
+
const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
|
|
4989
|
+
const cells = renders.flatMap((r, colIdx) => [
|
|
4990
|
+
{
|
|
4991
|
+
props: { version: "before", component: r.name },
|
|
4992
|
+
result: {
|
|
4993
|
+
screenshot: r.before.screenshot,
|
|
4994
|
+
width: cellW,
|
|
4995
|
+
height: cellH,
|
|
4996
|
+
renderTimeMs: 0,
|
|
4997
|
+
computedStyles: {}
|
|
4998
|
+
},
|
|
4999
|
+
index: colIdx * 2,
|
|
5000
|
+
axisIndices: [0, colIdx]
|
|
5001
|
+
},
|
|
5002
|
+
{
|
|
5003
|
+
props: { version: "after", component: r.name },
|
|
5004
|
+
result: {
|
|
5005
|
+
screenshot: r.after.screenshot,
|
|
5006
|
+
width: cellW,
|
|
5007
|
+
height: cellH,
|
|
5008
|
+
renderTimeMs: 0,
|
|
5009
|
+
computedStyles: {}
|
|
5010
|
+
},
|
|
5011
|
+
index: colIdx * 2 + 1,
|
|
5012
|
+
axisIndices: [1, colIdx]
|
|
5013
|
+
}
|
|
5014
|
+
]);
|
|
5015
|
+
const matrixResult = {
|
|
5016
|
+
cells,
|
|
5017
|
+
axes: [
|
|
5018
|
+
{ name: "component", values: renders.map((r) => r.name) },
|
|
5019
|
+
{ name: "version", values: ["before", "after"] }
|
|
5020
|
+
],
|
|
5021
|
+
axisLabels: [renders.map((r) => r.name), ["before", "after"]],
|
|
5022
|
+
rows: 2,
|
|
5023
|
+
cols: renders.length,
|
|
5024
|
+
stats: {
|
|
5025
|
+
totalCells: cells.length,
|
|
5026
|
+
totalRenderTimeMs: 0,
|
|
5027
|
+
avgRenderTimeMs: 0,
|
|
5028
|
+
minRenderTimeMs: 0,
|
|
5029
|
+
maxRenderTimeMs: 0,
|
|
5030
|
+
wallClockTimeMs: 0
|
|
5031
|
+
}
|
|
5032
|
+
};
|
|
5033
|
+
const generator = new render.SpriteSheetGenerator({
|
|
5034
|
+
cellPadding: 8,
|
|
5035
|
+
borderWidth: 1,
|
|
5036
|
+
labelHeight: 32,
|
|
5037
|
+
labelWidth: 120
|
|
5038
|
+
});
|
|
5039
|
+
const spriteResult = await generator.generate(matrixResult);
|
|
5040
|
+
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
5041
|
+
const outputPath = opts.output ?? path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
5042
|
+
const outputDir = path.resolve(outputPath, "..");
|
|
5043
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
5044
|
+
fs.writeFileSync(outputPath, spriteResult.png);
|
|
5045
|
+
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
5046
|
+
if (useJson) {
|
|
5047
|
+
process.stdout.write(
|
|
5048
|
+
`${JSON.stringify(
|
|
5049
|
+
{
|
|
5050
|
+
tokenPath,
|
|
5051
|
+
oldValue: currentValue,
|
|
5052
|
+
newValue: opts.newValue,
|
|
5053
|
+
outputPath,
|
|
5054
|
+
width: spriteResult.width,
|
|
5055
|
+
height: spriteResult.height,
|
|
5056
|
+
components: renders.map((r) => r.name),
|
|
5057
|
+
cells: spriteResult.coordinates.length
|
|
5058
|
+
},
|
|
5059
|
+
null,
|
|
5060
|
+
2
|
|
5061
|
+
)}
|
|
5062
|
+
`
|
|
5063
|
+
);
|
|
5064
|
+
} else {
|
|
5065
|
+
process.stdout.write(
|
|
5066
|
+
`Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
|
|
5067
|
+
`
|
|
5068
|
+
);
|
|
5069
|
+
process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
|
|
5070
|
+
`);
|
|
5071
|
+
}
|
|
5072
|
+
} catch (err) {
|
|
5073
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
5074
|
+
`);
|
|
5075
|
+
process.exit(1);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
);
|
|
5079
|
+
}
|
|
4187
5080
|
|
|
4188
5081
|
// src/tokens/commands.ts
|
|
4189
5082
|
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
@@ -4205,7 +5098,7 @@ function buildTable2(headers, rows) {
|
|
|
4205
5098
|
);
|
|
4206
5099
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
4207
5100
|
}
|
|
4208
|
-
function
|
|
5101
|
+
function resolveTokenFilePath(fileFlag) {
|
|
4209
5102
|
if (fileFlag !== void 0) {
|
|
4210
5103
|
return path.resolve(process.cwd(), fileFlag);
|
|
4211
5104
|
}
|
|
@@ -4268,7 +5161,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
4268
5161
|
function registerGet2(tokensCmd) {
|
|
4269
5162
|
tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
4270
5163
|
try {
|
|
4271
|
-
const filePath =
|
|
5164
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4272
5165
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4273
5166
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4274
5167
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -4294,7 +5187,7 @@ function registerList2(tokensCmd) {
|
|
|
4294
5187
|
tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
4295
5188
|
(category, opts) => {
|
|
4296
5189
|
try {
|
|
4297
|
-
const filePath =
|
|
5190
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4298
5191
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4299
5192
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4300
5193
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -4324,7 +5217,7 @@ function registerSearch(tokensCmd) {
|
|
|
4324
5217
|
tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
4325
5218
|
(value, opts) => {
|
|
4326
5219
|
try {
|
|
4327
|
-
const filePath =
|
|
5220
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4328
5221
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
4329
5222
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
4330
5223
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -4406,7 +5299,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
4406
5299
|
function registerResolve(tokensCmd) {
|
|
4407
5300
|
tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
4408
5301
|
try {
|
|
4409
|
-
const filePath =
|
|
5302
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4410
5303
|
const absFilePath = filePath;
|
|
4411
5304
|
const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
|
|
4412
5305
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
@@ -4443,7 +5336,7 @@ function registerValidate(tokensCmd) {
|
|
|
4443
5336
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
4444
5337
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
4445
5338
|
try {
|
|
4446
|
-
const filePath =
|
|
5339
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
4447
5340
|
if (!fs.existsSync(filePath)) {
|
|
4448
5341
|
throw new Error(
|
|
4449
5342
|
`Token file not found at ${filePath}.
|
|
@@ -4527,6 +5420,9 @@ function createTokensCommand() {
|
|
|
4527
5420
|
registerResolve(tokensCmd);
|
|
4528
5421
|
registerValidate(tokensCmd);
|
|
4529
5422
|
tokensCmd.addCommand(createTokensExportCommand());
|
|
5423
|
+
registerCompliance(tokensCmd);
|
|
5424
|
+
registerImpact(tokensCmd);
|
|
5425
|
+
registerPreview(tokensCmd);
|
|
4530
5426
|
return tokensCmd;
|
|
4531
5427
|
}
|
|
4532
5428
|
|
|
@@ -4619,6 +5515,7 @@ function createProgram(options = {}) {
|
|
|
4619
5515
|
program.addCommand(createTokensCommand());
|
|
4620
5516
|
program.addCommand(createInstrumentCommand());
|
|
4621
5517
|
program.addCommand(createInitCommand());
|
|
5518
|
+
program.addCommand(createCiCommand());
|
|
4622
5519
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
4623
5520
|
if (existingReportCmd !== void 0) {
|
|
4624
5521
|
registerBaselineSubCommand(existingReportCmd);
|
|
@@ -4627,15 +5524,19 @@ function createProgram(options = {}) {
|
|
|
4627
5524
|
return program;
|
|
4628
5525
|
}
|
|
4629
5526
|
|
|
5527
|
+
exports.CI_EXIT = CI_EXIT;
|
|
5528
|
+
exports.createCiCommand = createCiCommand;
|
|
4630
5529
|
exports.createInitCommand = createInitCommand;
|
|
4631
5530
|
exports.createInstrumentCommand = createInstrumentCommand;
|
|
4632
5531
|
exports.createManifestCommand = createManifestCommand;
|
|
4633
5532
|
exports.createProgram = createProgram;
|
|
4634
5533
|
exports.createTokensCommand = createTokensCommand;
|
|
4635
5534
|
exports.createTokensExportCommand = createTokensExportCommand;
|
|
5535
|
+
exports.formatCiReport = formatCiReport;
|
|
4636
5536
|
exports.isTTY = isTTY;
|
|
4637
5537
|
exports.matchGlob = matchGlob;
|
|
4638
|
-
exports.resolveTokenFilePath =
|
|
5538
|
+
exports.resolveTokenFilePath = resolveTokenFilePath2;
|
|
5539
|
+
exports.runCi = runCi;
|
|
4639
5540
|
exports.runInit = runInit;
|
|
4640
5541
|
//# sourceMappingURL=index.cjs.map
|
|
4641
5542
|
//# sourceMappingURL=index.cjs.map
|