@agent-scope/cli 1.14.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 +1245 -818
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1082 -659
- 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 +1073 -654
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,16 +1,918 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, appendFileSync, readdirSync, rmSync } from 'fs';
|
|
2
|
+
import { resolve, join, dirname } from 'path';
|
|
3
|
+
import { generateManifest } from '@agent-scope/manifest';
|
|
4
|
+
import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
5
|
+
import { TokenResolver, ComplianceEngine, parseTokenFileSync, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
4
6
|
import { Command } from 'commander';
|
|
5
|
-
import { SpriteSheetGenerator, BrowserPool, safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
6
7
|
import * as esbuild from 'esbuild';
|
|
7
|
-
import {
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import * as readline from 'readline';
|
|
8
10
|
import { chromium } from 'playwright';
|
|
9
11
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
|
-
import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
12
12
|
|
|
13
|
-
// src/
|
|
13
|
+
// src/ci/commands.ts
|
|
14
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
15
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
16
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
17
|
+
}
|
|
18
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
19
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
20
|
+
const wrapperCode = (
|
|
21
|
+
/* ts */
|
|
22
|
+
`
|
|
23
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
24
|
+
import { createRoot } from "react-dom/client";
|
|
25
|
+
import { createElement } from "react";
|
|
26
|
+
|
|
27
|
+
(function scopeRenderHarness() {
|
|
28
|
+
var Component =
|
|
29
|
+
__scopeMod["default"] ||
|
|
30
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
31
|
+
(Object.values(__scopeMod).find(
|
|
32
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
if (!Component) {
|
|
36
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
37
|
+
"No renderable component found. Checked: default, " +
|
|
38
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
39
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
40
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
var props = ${propsJson};
|
|
46
|
+
var rootEl = document.getElementById("scope-root");
|
|
47
|
+
if (!rootEl) {
|
|
48
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
49
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
53
|
+
// Use requestAnimationFrame to let React flush the render
|
|
54
|
+
requestAnimationFrame(function() {
|
|
55
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
59
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
const result = await esbuild.build({
|
|
65
|
+
stdin: {
|
|
66
|
+
contents: wrapperCode,
|
|
67
|
+
// Resolve relative imports (within the component's dir)
|
|
68
|
+
resolveDir: dirname(filePath),
|
|
69
|
+
loader: "tsx",
|
|
70
|
+
sourcefile: "__scope_harness__.tsx"
|
|
71
|
+
},
|
|
72
|
+
bundle: true,
|
|
73
|
+
format: "iife",
|
|
74
|
+
write: false,
|
|
75
|
+
platform: "browser",
|
|
76
|
+
jsx: "automatic",
|
|
77
|
+
jsxImportSource: "react",
|
|
78
|
+
target: "es2020",
|
|
79
|
+
// Bundle everything — no externals
|
|
80
|
+
external: [],
|
|
81
|
+
define: {
|
|
82
|
+
"process.env.NODE_ENV": '"development"',
|
|
83
|
+
global: "globalThis"
|
|
84
|
+
},
|
|
85
|
+
logLevel: "silent",
|
|
86
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
87
|
+
banner: {
|
|
88
|
+
js: "/* @agent-scope/cli component harness */"
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (result.errors.length > 0) {
|
|
92
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
93
|
+
throw new Error(`esbuild failed to bundle component:
|
|
94
|
+
${msg}`);
|
|
95
|
+
}
|
|
96
|
+
const outputFile = result.outputFiles?.[0];
|
|
97
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
98
|
+
throw new Error("esbuild produced no output");
|
|
99
|
+
}
|
|
100
|
+
return outputFile.text;
|
|
101
|
+
}
|
|
102
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
103
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
104
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
105
|
+
</style>` : "";
|
|
106
|
+
return `<!DOCTYPE html>
|
|
107
|
+
<html lang="en">
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="UTF-8" />
|
|
110
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
111
|
+
<style>
|
|
112
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
113
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
114
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
115
|
+
</style>
|
|
116
|
+
${projectStyleBlock}
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
120
|
+
<script>${bundledScript}</script>
|
|
121
|
+
</body>
|
|
122
|
+
</html>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/manifest-formatter.ts
|
|
126
|
+
function isTTY() {
|
|
127
|
+
return process.stdout.isTTY === true;
|
|
128
|
+
}
|
|
129
|
+
function pad(value, width) {
|
|
130
|
+
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
131
|
+
}
|
|
132
|
+
function buildTable(headers, rows) {
|
|
133
|
+
const colWidths = headers.map(
|
|
134
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
135
|
+
);
|
|
136
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
137
|
+
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
138
|
+
const dataRows = rows.map(
|
|
139
|
+
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
140
|
+
);
|
|
141
|
+
return [headerRow, divider, ...dataRows].join("\n");
|
|
142
|
+
}
|
|
143
|
+
function formatListTable(rows) {
|
|
144
|
+
if (rows.length === 0) return "No components found.";
|
|
145
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
146
|
+
const tableRows = rows.map((r) => [
|
|
147
|
+
r.name,
|
|
148
|
+
r.file,
|
|
149
|
+
r.complexityClass,
|
|
150
|
+
String(r.hookCount),
|
|
151
|
+
String(r.contextCount)
|
|
152
|
+
]);
|
|
153
|
+
return buildTable(headers, tableRows);
|
|
154
|
+
}
|
|
155
|
+
function formatListJson(rows) {
|
|
156
|
+
return JSON.stringify(rows, null, 2);
|
|
157
|
+
}
|
|
158
|
+
function formatSideEffects(se) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
161
|
+
if (se.timers) parts.push("timers");
|
|
162
|
+
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
163
|
+
if (se.globalListeners) parts.push("globalListeners");
|
|
164
|
+
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
165
|
+
}
|
|
166
|
+
function formatGetTable(name, descriptor) {
|
|
167
|
+
const propNames = Object.keys(descriptor.props);
|
|
168
|
+
const lines = [
|
|
169
|
+
`Component: ${name}`,
|
|
170
|
+
` File: ${descriptor.filePath}`,
|
|
171
|
+
` Export: ${descriptor.exportType}`,
|
|
172
|
+
` Display Name: ${descriptor.displayName}`,
|
|
173
|
+
` Complexity: ${descriptor.complexityClass}`,
|
|
174
|
+
` Memoized: ${descriptor.memoized}`,
|
|
175
|
+
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
176
|
+
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
177
|
+
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
178
|
+
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
179
|
+
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
180
|
+
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
181
|
+
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
182
|
+
"",
|
|
183
|
+
` Props (${propNames.length}):`
|
|
184
|
+
];
|
|
185
|
+
if (propNames.length === 0) {
|
|
186
|
+
lines.push(" (none)");
|
|
187
|
+
} else {
|
|
188
|
+
for (const propName of propNames) {
|
|
189
|
+
const p = descriptor.props[propName];
|
|
190
|
+
if (p === void 0) continue;
|
|
191
|
+
const req = p.required ? "required" : "optional";
|
|
192
|
+
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
193
|
+
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
194
|
+
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
function formatGetJson(name, descriptor) {
|
|
200
|
+
return JSON.stringify({ name, ...descriptor }, null, 2);
|
|
201
|
+
}
|
|
202
|
+
function formatQueryTable(rows, queryDesc) {
|
|
203
|
+
if (rows.length === 0) return `No components match: ${queryDesc}`;
|
|
204
|
+
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
205
|
+
const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
|
|
206
|
+
return `Query: ${queryDesc}
|
|
207
|
+
|
|
208
|
+
${buildTable(headers, tableRows)}`;
|
|
209
|
+
}
|
|
210
|
+
function formatQueryJson(rows) {
|
|
211
|
+
return JSON.stringify(rows, null, 2);
|
|
212
|
+
}
|
|
213
|
+
function matchGlob(pattern, value) {
|
|
214
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
|
|
216
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
217
|
+
return regex.test(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/render-formatter.ts
|
|
221
|
+
function parseViewport(spec) {
|
|
222
|
+
const lower = spec.toLowerCase();
|
|
223
|
+
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
224
|
+
if (!match) {
|
|
225
|
+
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
226
|
+
}
|
|
227
|
+
const width = parseInt(match[1] ?? "0", 10);
|
|
228
|
+
const height = parseInt(match[2] ?? "0", 10);
|
|
229
|
+
if (width <= 0 || height <= 0) {
|
|
230
|
+
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
231
|
+
}
|
|
232
|
+
return { width, height };
|
|
233
|
+
}
|
|
234
|
+
function formatRenderJson(componentName, props, result) {
|
|
235
|
+
return {
|
|
236
|
+
component: componentName,
|
|
237
|
+
props,
|
|
238
|
+
width: result.width,
|
|
239
|
+
height: result.height,
|
|
240
|
+
renderTimeMs: result.renderTimeMs,
|
|
241
|
+
computedStyles: result.computedStyles,
|
|
242
|
+
screenshot: result.screenshot.toString("base64"),
|
|
243
|
+
dom: result.dom,
|
|
244
|
+
console: result.console,
|
|
245
|
+
accessibility: result.accessibility
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function formatMatrixJson(result) {
|
|
249
|
+
return {
|
|
250
|
+
axes: result.axes.map((axis) => ({
|
|
251
|
+
name: axis.name,
|
|
252
|
+
values: axis.values.map((v) => String(v))
|
|
253
|
+
})),
|
|
254
|
+
stats: { ...result.stats },
|
|
255
|
+
cells: result.cells.map((cell) => ({
|
|
256
|
+
index: cell.index,
|
|
257
|
+
axisIndices: cell.axisIndices,
|
|
258
|
+
props: cell.props,
|
|
259
|
+
renderTimeMs: cell.result.renderTimeMs,
|
|
260
|
+
width: cell.result.width,
|
|
261
|
+
height: cell.result.height,
|
|
262
|
+
screenshot: cell.result.screenshot.toString("base64")
|
|
263
|
+
}))
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function formatMatrixHtml(componentName, result) {
|
|
267
|
+
const cellsHtml = result.cells.map((cell) => {
|
|
268
|
+
const b64 = cell.result.screenshot.toString("base64");
|
|
269
|
+
const propLabel = escapeHtml(
|
|
270
|
+
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
271
|
+
);
|
|
272
|
+
return ` <div class="cell">
|
|
273
|
+
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
274
|
+
<div class="label">${propLabel}</div>
|
|
275
|
+
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
276
|
+
</div>`;
|
|
277
|
+
}).join("\n");
|
|
278
|
+
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
279
|
+
return `<!DOCTYPE html>
|
|
280
|
+
<html lang="en">
|
|
281
|
+
<head>
|
|
282
|
+
<meta charset="UTF-8" />
|
|
283
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
284
|
+
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
285
|
+
<style>
|
|
286
|
+
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
287
|
+
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
288
|
+
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
289
|
+
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
290
|
+
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
291
|
+
.cell img { display: block; }
|
|
292
|
+
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
293
|
+
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
294
|
+
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
295
|
+
</style>
|
|
296
|
+
</head>
|
|
297
|
+
<body>
|
|
298
|
+
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
299
|
+
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
300
|
+
<div class="grid">
|
|
301
|
+
${cellsHtml}
|
|
302
|
+
</div>
|
|
303
|
+
<div class="stats">
|
|
304
|
+
${result.stats.totalCells} cells \xB7
|
|
305
|
+
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
306
|
+
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
307
|
+
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
308
|
+
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
309
|
+
</div>
|
|
310
|
+
</body>
|
|
311
|
+
</html>
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function formatMatrixCsv(componentName, result) {
|
|
315
|
+
const axisNames = result.axes.map((a) => a.name);
|
|
316
|
+
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
317
|
+
const rows = result.cells.map((cell) => {
|
|
318
|
+
const axisVals = result.axes.map((_, i) => {
|
|
319
|
+
const axisIdx = cell.axisIndices[i];
|
|
320
|
+
const axis = result.axes[i];
|
|
321
|
+
if (axisIdx === void 0 || axis === void 0) return "";
|
|
322
|
+
const val = axis.values[axisIdx];
|
|
323
|
+
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
324
|
+
});
|
|
325
|
+
return [
|
|
326
|
+
csvEscape(componentName),
|
|
327
|
+
...axisVals,
|
|
328
|
+
cell.result.renderTimeMs.toFixed(3),
|
|
329
|
+
String(cell.result.width),
|
|
330
|
+
String(cell.result.height)
|
|
331
|
+
].join(",");
|
|
332
|
+
});
|
|
333
|
+
return `${[headers.join(","), ...rows].join("\n")}
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
337
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
338
|
+
const empty = barWidth - filled;
|
|
339
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
340
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
341
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
342
|
+
}
|
|
343
|
+
function formatSummaryText(results, outputDir) {
|
|
344
|
+
const total = results.length;
|
|
345
|
+
const passed = results.filter((r) => r.success).length;
|
|
346
|
+
const failed = total - passed;
|
|
347
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
348
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
349
|
+
const lines = [
|
|
350
|
+
"\u2500".repeat(60),
|
|
351
|
+
`Render Summary`,
|
|
352
|
+
"\u2500".repeat(60),
|
|
353
|
+
` Total components : ${total}`,
|
|
354
|
+
` Passed : ${passed}`,
|
|
355
|
+
` Failed : ${failed}`,
|
|
356
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
357
|
+
` Output dir : ${outputDir}`
|
|
358
|
+
];
|
|
359
|
+
if (failed > 0) {
|
|
360
|
+
lines.push("", " Failed components:");
|
|
361
|
+
for (const r of results) {
|
|
362
|
+
if (!r.success) {
|
|
363
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
lines.push("\u2500".repeat(60));
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
370
|
+
function escapeHtml(str) {
|
|
371
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
372
|
+
}
|
|
373
|
+
function csvEscape(value) {
|
|
374
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
375
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
var CONFIG_FILENAMES = [
|
|
380
|
+
".reactscope/config.json",
|
|
381
|
+
".reactscope/config.js",
|
|
382
|
+
".reactscope/config.mjs"
|
|
383
|
+
];
|
|
384
|
+
var STYLE_ENTRY_CANDIDATES = [
|
|
385
|
+
"src/index.css",
|
|
386
|
+
"src/globals.css",
|
|
387
|
+
"app/globals.css",
|
|
388
|
+
"app/index.css",
|
|
389
|
+
"styles/index.css",
|
|
390
|
+
"index.css"
|
|
391
|
+
];
|
|
392
|
+
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
393
|
+
var compilerCache = null;
|
|
394
|
+
function getCachedBuild(cwd) {
|
|
395
|
+
if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
|
|
396
|
+
return compilerCache.build;
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function findStylesEntry(cwd) {
|
|
401
|
+
for (const name of CONFIG_FILENAMES) {
|
|
402
|
+
const p = resolve(cwd, name);
|
|
403
|
+
if (!existsSync(p)) continue;
|
|
404
|
+
try {
|
|
405
|
+
if (name.endsWith(".json")) {
|
|
406
|
+
const raw = readFileSync(p, "utf-8");
|
|
407
|
+
const data = JSON.parse(raw);
|
|
408
|
+
const scope = data.scope;
|
|
409
|
+
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
410
|
+
if (typeof entry === "string") {
|
|
411
|
+
const full = resolve(cwd, entry);
|
|
412
|
+
if (existsSync(full)) return full;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
419
|
+
if (existsSync(pkgPath)) {
|
|
420
|
+
try {
|
|
421
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
422
|
+
const pkg = JSON.parse(raw);
|
|
423
|
+
const entry = pkg.scope?.stylesEntry;
|
|
424
|
+
if (typeof entry === "string") {
|
|
425
|
+
const full = resolve(cwd, entry);
|
|
426
|
+
if (existsSync(full)) return full;
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
432
|
+
const full = resolve(cwd, candidate);
|
|
433
|
+
if (existsSync(full)) {
|
|
434
|
+
try {
|
|
435
|
+
const content = readFileSync(full, "utf-8");
|
|
436
|
+
if (TAILWIND_IMPORT.test(content)) return full;
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
async function getTailwindCompiler(cwd) {
|
|
444
|
+
const cached = getCachedBuild(cwd);
|
|
445
|
+
if (cached !== null) return cached;
|
|
446
|
+
const entryPath = findStylesEntry(cwd);
|
|
447
|
+
if (entryPath === null) return null;
|
|
448
|
+
let compile;
|
|
449
|
+
try {
|
|
450
|
+
const require2 = createRequire(resolve(cwd, "package.json"));
|
|
451
|
+
const tailwind = require2("tailwindcss");
|
|
452
|
+
const fn = tailwind.compile;
|
|
453
|
+
if (typeof fn !== "function") return null;
|
|
454
|
+
compile = fn;
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const entryContent = readFileSync(entryPath, "utf-8");
|
|
459
|
+
const loadStylesheet = async (id, base) => {
|
|
460
|
+
if (id === "tailwindcss") {
|
|
461
|
+
const nodeModules = resolve(cwd, "node_modules");
|
|
462
|
+
const tailwindCssPath = resolve(nodeModules, "tailwindcss", "index.css");
|
|
463
|
+
if (!existsSync(tailwindCssPath)) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const content = readFileSync(tailwindCssPath, "utf-8");
|
|
469
|
+
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
470
|
+
}
|
|
471
|
+
const full = resolve(base, id);
|
|
472
|
+
if (existsSync(full)) {
|
|
473
|
+
const content = readFileSync(full, "utf-8");
|
|
474
|
+
return { path: full, base: resolve(full, ".."), content };
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
477
|
+
};
|
|
478
|
+
const result = await compile(entryContent, {
|
|
479
|
+
base: cwd,
|
|
480
|
+
from: entryPath,
|
|
481
|
+
loadStylesheet
|
|
482
|
+
});
|
|
483
|
+
const build2 = result.build.bind(result);
|
|
484
|
+
compilerCache = { cwd, build: build2 };
|
|
485
|
+
return build2;
|
|
486
|
+
}
|
|
487
|
+
async function getCompiledCssForClasses(cwd, classes) {
|
|
488
|
+
const build2 = await getTailwindCompiler(cwd);
|
|
489
|
+
if (build2 === null) return null;
|
|
490
|
+
const deduped = [...new Set(classes)].filter(Boolean);
|
|
491
|
+
if (deduped.length === 0) return null;
|
|
492
|
+
return build2(deduped);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/ci/commands.ts
|
|
496
|
+
var CI_EXIT = {
|
|
497
|
+
/** All checks passed */
|
|
498
|
+
OK: 0,
|
|
499
|
+
/** Compliance score below threshold */
|
|
500
|
+
COMPLIANCE_BELOW_THRESHOLD: 1,
|
|
501
|
+
/** Accessibility violations found */
|
|
502
|
+
A11Y_VIOLATIONS: 2,
|
|
503
|
+
/** Console errors detected during render */
|
|
504
|
+
CONSOLE_ERRORS: 3,
|
|
505
|
+
/** Visual regression detected against baseline */
|
|
506
|
+
VISUAL_REGRESSION: 4,
|
|
507
|
+
/** One or more components failed to render */
|
|
508
|
+
RENDER_FAILURES: 5
|
|
509
|
+
};
|
|
510
|
+
var ALL_CHECKS = ["compliance", "a11y", "console-errors", "visual-regression"];
|
|
511
|
+
var _pool = null;
|
|
512
|
+
async function getPool(viewportWidth, viewportHeight) {
|
|
513
|
+
if (_pool === null) {
|
|
514
|
+
_pool = new BrowserPool({
|
|
515
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
516
|
+
viewportWidth,
|
|
517
|
+
viewportHeight
|
|
518
|
+
});
|
|
519
|
+
await _pool.init();
|
|
520
|
+
}
|
|
521
|
+
return _pool;
|
|
522
|
+
}
|
|
523
|
+
async function shutdownPool() {
|
|
524
|
+
if (_pool !== null) {
|
|
525
|
+
await _pool.close();
|
|
526
|
+
_pool = null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
530
|
+
const pool = await getPool(viewportWidth, viewportHeight);
|
|
531
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
532
|
+
const slot = await pool.acquire();
|
|
533
|
+
const { page } = slot;
|
|
534
|
+
try {
|
|
535
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
536
|
+
await page.waitForFunction(
|
|
537
|
+
() => {
|
|
538
|
+
const w = window;
|
|
539
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
540
|
+
},
|
|
541
|
+
{ timeout: 15e3 }
|
|
542
|
+
);
|
|
543
|
+
const renderError = await page.evaluate(() => {
|
|
544
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
545
|
+
});
|
|
546
|
+
if (renderError !== null) {
|
|
547
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
548
|
+
}
|
|
549
|
+
const rootDir = process.cwd();
|
|
550
|
+
const classes = await page.evaluate(() => {
|
|
551
|
+
const set = /* @__PURE__ */ new Set();
|
|
552
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
553
|
+
for (const c of el.className.split(/\s+/)) {
|
|
554
|
+
if (c) set.add(c);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return [...set];
|
|
558
|
+
});
|
|
559
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
560
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
561
|
+
await page.addStyleTag({ content: projectCss });
|
|
562
|
+
}
|
|
563
|
+
const startMs = performance.now();
|
|
564
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
565
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
566
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const PAD = 24;
|
|
572
|
+
const MIN_W = 320;
|
|
573
|
+
const MIN_H = 200;
|
|
574
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
575
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
576
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
577
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
578
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
579
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
580
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
581
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
582
|
+
const screenshot = await page.screenshot({
|
|
583
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
584
|
+
type: "png"
|
|
585
|
+
});
|
|
586
|
+
const computedStylesRaw = {};
|
|
587
|
+
const styles = await page.evaluate((sel) => {
|
|
588
|
+
const el = document.querySelector(sel);
|
|
589
|
+
if (el === null) return {};
|
|
590
|
+
const computed = window.getComputedStyle(el);
|
|
591
|
+
const out = {};
|
|
592
|
+
for (const prop of [
|
|
593
|
+
"display",
|
|
594
|
+
"width",
|
|
595
|
+
"height",
|
|
596
|
+
"color",
|
|
597
|
+
"backgroundColor",
|
|
598
|
+
"fontSize",
|
|
599
|
+
"fontFamily",
|
|
600
|
+
"padding",
|
|
601
|
+
"margin"
|
|
602
|
+
]) {
|
|
603
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}, "[data-reactscope-root] > *");
|
|
607
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
608
|
+
const renderTimeMs = performance.now() - startMs;
|
|
609
|
+
return {
|
|
610
|
+
screenshot,
|
|
611
|
+
width: Math.round(safeW),
|
|
612
|
+
height: Math.round(safeH),
|
|
613
|
+
renderTimeMs,
|
|
614
|
+
computedStyles: computedStylesRaw
|
|
615
|
+
};
|
|
616
|
+
} finally {
|
|
617
|
+
pool.release(slot);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
621
|
+
const flat = {};
|
|
622
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
623
|
+
Object.assign(flat, styles);
|
|
624
|
+
}
|
|
625
|
+
const colors = {};
|
|
626
|
+
const spacing = {};
|
|
627
|
+
const typography = {};
|
|
628
|
+
const borders = {};
|
|
629
|
+
const shadows = {};
|
|
630
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
631
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
632
|
+
colors[prop] = value;
|
|
633
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
634
|
+
spacing[prop] = value;
|
|
635
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
636
|
+
typography[prop] = value;
|
|
637
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
638
|
+
borders[prop] = value;
|
|
639
|
+
} else if (prop === "boxShadow") {
|
|
640
|
+
shadows[prop] = value;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { colors, spacing, typography, borders, shadows };
|
|
644
|
+
}
|
|
645
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
646
|
+
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
647
|
+
if (!existsSync(jsonPath)) return null;
|
|
648
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
649
|
+
}
|
|
650
|
+
async function runCi(options = {}) {
|
|
651
|
+
const {
|
|
652
|
+
baselineDir: baselineDirRaw,
|
|
653
|
+
checks = ALL_CHECKS,
|
|
654
|
+
complianceThreshold = 0.9,
|
|
655
|
+
viewportWidth = 375,
|
|
656
|
+
viewportHeight = 812
|
|
657
|
+
} = options;
|
|
658
|
+
const startTime = performance.now();
|
|
659
|
+
const rootDir = process.cwd();
|
|
660
|
+
const checksSet = new Set(checks);
|
|
661
|
+
const baselineDir = baselineDirRaw !== void 0 ? resolve(rootDir, baselineDirRaw) : void 0;
|
|
662
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
663
|
+
const manifest = await generateManifest({ rootDir });
|
|
664
|
+
const componentNames = Object.keys(manifest.components);
|
|
665
|
+
const total = componentNames.length;
|
|
666
|
+
process.stderr.write(`Found ${total} components.
|
|
667
|
+
`);
|
|
668
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
669
|
+
`);
|
|
670
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
671
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
672
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
673
|
+
let completed = 0;
|
|
674
|
+
const CONCURRENCY = 4;
|
|
675
|
+
let nextIdx = 0;
|
|
676
|
+
const renderOne = async (name) => {
|
|
677
|
+
const descriptor = manifest.components[name];
|
|
678
|
+
if (descriptor === void 0) return;
|
|
679
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
680
|
+
const outcome = await safeRender(
|
|
681
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
682
|
+
{
|
|
683
|
+
props: {},
|
|
684
|
+
sourceLocation: {
|
|
685
|
+
file: descriptor.filePath,
|
|
686
|
+
line: descriptor.loc.start,
|
|
687
|
+
column: 0
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
completed++;
|
|
692
|
+
const pct = Math.round(completed / total * 100);
|
|
693
|
+
if (isTTY()) {
|
|
694
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
695
|
+
}
|
|
696
|
+
if (outcome.crashed) {
|
|
697
|
+
renderFailures.add(name);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const result = outcome.result;
|
|
701
|
+
currentRenderMeta.set(name, { width: result.width, height: result.height });
|
|
702
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
703
|
+
};
|
|
704
|
+
if (total > 0) {
|
|
705
|
+
const worker = async () => {
|
|
706
|
+
while (nextIdx < componentNames.length) {
|
|
707
|
+
const i = nextIdx++;
|
|
708
|
+
const name = componentNames[i];
|
|
709
|
+
if (name !== void 0) {
|
|
710
|
+
await renderOne(name);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const workers = [];
|
|
715
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
716
|
+
workers.push(worker());
|
|
717
|
+
}
|
|
718
|
+
await Promise.all(workers);
|
|
719
|
+
}
|
|
720
|
+
await shutdownPool();
|
|
721
|
+
if (isTTY() && total > 0) {
|
|
722
|
+
process.stderr.write("\n");
|
|
723
|
+
}
|
|
724
|
+
const resolver = new TokenResolver([]);
|
|
725
|
+
const engine = new ComplianceEngine(resolver);
|
|
726
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
727
|
+
const complianceScore = batchReport.aggregateCompliance;
|
|
728
|
+
const checkResults = [];
|
|
729
|
+
const renderFailureCount = renderFailures.size;
|
|
730
|
+
const rendersPassed = renderFailureCount === 0;
|
|
731
|
+
if (checksSet.has("compliance")) {
|
|
732
|
+
const compliancePassed = complianceScore >= complianceThreshold;
|
|
733
|
+
checkResults.push({
|
|
734
|
+
check: "compliance",
|
|
735
|
+
passed: compliancePassed,
|
|
736
|
+
message: compliancePassed ? `Compliance ${(complianceScore * 100).toFixed(1)}% >= threshold ${(complianceThreshold * 100).toFixed(1)}%` : `Compliance ${(complianceScore * 100).toFixed(1)}% < threshold ${(complianceThreshold * 100).toFixed(1)}%`,
|
|
737
|
+
value: complianceScore,
|
|
738
|
+
threshold: complianceThreshold
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
if (checksSet.has("a11y")) {
|
|
742
|
+
checkResults.push({
|
|
743
|
+
check: "a11y",
|
|
744
|
+
passed: true,
|
|
745
|
+
message: "Accessibility audit not yet implemented \u2014 skipped"
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (checksSet.has("console-errors")) {
|
|
749
|
+
const consoleErrorsPassed = rendersPassed;
|
|
750
|
+
checkResults.push({
|
|
751
|
+
check: "console-errors",
|
|
752
|
+
passed: consoleErrorsPassed,
|
|
753
|
+
message: consoleErrorsPassed ? "No console errors detected" : `Console errors likely \u2014 ${renderFailureCount} component(s) failed to render`
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
let hasVisualRegression = false;
|
|
757
|
+
if (checksSet.has("visual-regression") && baselineDir !== void 0) {
|
|
758
|
+
if (!existsSync(baselineDir)) {
|
|
759
|
+
process.stderr.write(
|
|
760
|
+
`Warning: baseline directory not found at "${baselineDir}" \u2014 skipping visual regression check.
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
checkResults.push({
|
|
764
|
+
check: "visual-regression",
|
|
765
|
+
passed: true,
|
|
766
|
+
message: `Baseline not found at ${baselineDir} \u2014 skipped`
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
const regressions = [];
|
|
770
|
+
for (const name of componentNames) {
|
|
771
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
772
|
+
const currentMeta = currentRenderMeta.get(name);
|
|
773
|
+
if (baselineMeta !== null && currentMeta !== void 0) {
|
|
774
|
+
const dw = Math.abs(currentMeta.width - baselineMeta.width);
|
|
775
|
+
const dh = Math.abs(currentMeta.height - baselineMeta.height);
|
|
776
|
+
if (dw > 10 || dh > 10) {
|
|
777
|
+
regressions.push(name);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
hasVisualRegression = regressions.length > 0;
|
|
782
|
+
checkResults.push({
|
|
783
|
+
check: "visual-regression",
|
|
784
|
+
passed: !hasVisualRegression,
|
|
785
|
+
message: hasVisualRegression ? `Visual regression detected in ${regressions.length} component(s): ${regressions.slice(0, 5).join(", ")}${regressions.length > 5 ? "..." : ""}` : "No visual regressions detected"
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
let exitCode = CI_EXIT.OK;
|
|
790
|
+
const complianceResult = checkResults.find((r) => r.check === "compliance");
|
|
791
|
+
const a11yResult = checkResults.find((r) => r.check === "a11y");
|
|
792
|
+
const consoleResult = checkResults.find((r) => r.check === "console-errors");
|
|
793
|
+
const visualResult = checkResults.find((r) => r.check === "visual-regression");
|
|
794
|
+
if (complianceResult !== void 0 && !complianceResult.passed) {
|
|
795
|
+
exitCode = CI_EXIT.COMPLIANCE_BELOW_THRESHOLD;
|
|
796
|
+
} else if (a11yResult !== void 0 && !a11yResult.passed) {
|
|
797
|
+
exitCode = CI_EXIT.A11Y_VIOLATIONS;
|
|
798
|
+
} else if (consoleResult !== void 0 && !consoleResult.passed) {
|
|
799
|
+
exitCode = CI_EXIT.CONSOLE_ERRORS;
|
|
800
|
+
} else if (visualResult !== void 0 && !visualResult.passed) {
|
|
801
|
+
exitCode = CI_EXIT.VISUAL_REGRESSION;
|
|
802
|
+
} else if (!rendersPassed) {
|
|
803
|
+
exitCode = CI_EXIT.RENDER_FAILURES;
|
|
804
|
+
}
|
|
805
|
+
const passed = exitCode === CI_EXIT.OK;
|
|
806
|
+
const wallClockMs = performance.now() - startTime;
|
|
807
|
+
return {
|
|
808
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
809
|
+
passed,
|
|
810
|
+
exitCode,
|
|
811
|
+
checks: checkResults,
|
|
812
|
+
components: {
|
|
813
|
+
total,
|
|
814
|
+
rendered: total - renderFailureCount,
|
|
815
|
+
failed: renderFailureCount
|
|
816
|
+
},
|
|
817
|
+
complianceScore,
|
|
818
|
+
complianceThreshold,
|
|
819
|
+
baselineCompared: baselineDir !== void 0 && existsSync(baselineDir),
|
|
820
|
+
wallClockMs
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function formatCiReport(result) {
|
|
824
|
+
const lines = [];
|
|
825
|
+
const title = "Scope CI Report";
|
|
826
|
+
const rule2 = "=".repeat(Math.max(title.length, 40));
|
|
827
|
+
lines.push(title, rule2);
|
|
828
|
+
const { total, rendered, failed } = result.components;
|
|
829
|
+
lines.push(
|
|
830
|
+
`Components: ${total} total ${rendered} rendered${failed > 0 ? ` ${failed} failed` : ""}`
|
|
831
|
+
);
|
|
832
|
+
lines.push("");
|
|
833
|
+
for (const check of result.checks) {
|
|
834
|
+
const icon = check.passed ? "pass" : "FAIL";
|
|
835
|
+
lines.push(` [${icon}] ${check.message}`);
|
|
836
|
+
}
|
|
837
|
+
if (result.components.failed > 0) {
|
|
838
|
+
lines.push(` [FAIL] ${result.components.failed} component(s) failed to render`);
|
|
839
|
+
}
|
|
840
|
+
lines.push("");
|
|
841
|
+
lines.push(rule2);
|
|
842
|
+
if (result.passed) {
|
|
843
|
+
lines.push(
|
|
844
|
+
`CI passed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
lines.push(
|
|
848
|
+
`CI failed in ${(result.wallClockMs / 1e3).toFixed(1)}s (exit code ${result.exitCode})`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return lines.join("\n");
|
|
852
|
+
}
|
|
853
|
+
function parseChecks(raw) {
|
|
854
|
+
if (raw === void 0) return void 0;
|
|
855
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
856
|
+
if (parts.length === 0) return void 0;
|
|
857
|
+
const valid = new Set(ALL_CHECKS);
|
|
858
|
+
const parsed = [];
|
|
859
|
+
for (const part of parts) {
|
|
860
|
+
if (!valid.has(part)) {
|
|
861
|
+
process.stderr.write(
|
|
862
|
+
`Warning: unknown check "${part}" \u2014 valid checks are: ${ALL_CHECKS.join(", ")}
|
|
863
|
+
`
|
|
864
|
+
);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
parsed.push(part);
|
|
868
|
+
}
|
|
869
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
870
|
+
}
|
|
871
|
+
function createCiCommand() {
|
|
872
|
+
return new Command("ci").description(
|
|
873
|
+
"Run a non-interactive CI pipeline (manifest -> render -> compliance -> regression) with exit codes"
|
|
874
|
+
).option(
|
|
875
|
+
"-b, --baseline <dir>",
|
|
876
|
+
"Baseline directory for visual regression comparison (omit to skip)"
|
|
877
|
+
).option(
|
|
878
|
+
"--checks <list>",
|
|
879
|
+
`Comma-separated checks to run (default: all). Valid: ${ALL_CHECKS.join(", ")}`
|
|
880
|
+
).option("--threshold <n>", "Compliance pass threshold (0-1, default: 0.90)", "0.90").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").option("--json", "Emit structured JSON to stdout in addition to the summary", false).option("-o, --output <path>", "Write the CI result JSON to a file").action(
|
|
881
|
+
async (opts) => {
|
|
882
|
+
try {
|
|
883
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
884
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
885
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
886
|
+
const complianceThreshold = Number.parseFloat(opts.threshold);
|
|
887
|
+
const checks = parseChecks(opts.checks);
|
|
888
|
+
const result = await runCi({
|
|
889
|
+
baselineDir: opts.baseline,
|
|
890
|
+
checks,
|
|
891
|
+
complianceThreshold,
|
|
892
|
+
viewportWidth,
|
|
893
|
+
viewportHeight
|
|
894
|
+
});
|
|
895
|
+
if (opts.output !== void 0) {
|
|
896
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
897
|
+
writeFileSync(outPath, JSON.stringify(result, null, 2), "utf-8");
|
|
898
|
+
process.stderr.write(`CI result written to ${opts.output}
|
|
899
|
+
`);
|
|
900
|
+
}
|
|
901
|
+
process.stdout.write(`${formatCiReport(result)}
|
|
902
|
+
`);
|
|
903
|
+
if (opts.json) {
|
|
904
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
905
|
+
`);
|
|
906
|
+
}
|
|
907
|
+
process.exit(result.exitCode);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
910
|
+
`);
|
|
911
|
+
process.exit(CI_EXIT.RENDER_FAILURES);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
}
|
|
14
916
|
function hasConfigFile(dir, stem) {
|
|
15
917
|
if (!existsSync(dir)) return false;
|
|
16
918
|
try {
|
|
@@ -205,9 +1107,9 @@ function createRL() {
|
|
|
205
1107
|
});
|
|
206
1108
|
}
|
|
207
1109
|
async function ask(rl, question) {
|
|
208
|
-
return new Promise((
|
|
1110
|
+
return new Promise((resolve15) => {
|
|
209
1111
|
rl.question(question, (answer) => {
|
|
210
|
-
|
|
1112
|
+
resolve15(answer.trim());
|
|
211
1113
|
});
|
|
212
1114
|
});
|
|
213
1115
|
}
|
|
@@ -285,282 +1187,75 @@ async function runInit(options) {
|
|
|
285
1187
|
`);
|
|
286
1188
|
process.stdout.write(` Output dir : ${config.output.dir}
|
|
287
1189
|
|
|
288
|
-
`);
|
|
289
|
-
} else {
|
|
290
|
-
const rl = createRL();
|
|
291
|
-
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
292
|
-
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
293
|
-
try {
|
|
294
|
-
process.stdout.write(` Detected framework: ${detected.framework}
|
|
295
|
-
`);
|
|
296
|
-
const includeRaw = await askWithDefault(
|
|
297
|
-
rl,
|
|
298
|
-
"Component include patterns (comma-separated)",
|
|
299
|
-
config.components.include.join(", ")
|
|
300
|
-
);
|
|
301
|
-
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
302
|
-
const excludeRaw = await askWithDefault(
|
|
303
|
-
rl,
|
|
304
|
-
"Component exclude patterns (comma-separated)",
|
|
305
|
-
config.components.exclude.join(", ")
|
|
306
|
-
);
|
|
307
|
-
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
308
|
-
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
309
|
-
config.tokens.file = tokenFile;
|
|
310
|
-
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
311
|
-
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
312
|
-
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
313
|
-
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
314
|
-
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
315
|
-
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
316
|
-
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
317
|
-
} finally {
|
|
318
|
-
rl.close();
|
|
319
|
-
}
|
|
320
|
-
process.stdout.write("\n");
|
|
321
|
-
}
|
|
322
|
-
const cfgPath = scaffoldConfig(rootDir, config);
|
|
323
|
-
created.push(cfgPath);
|
|
324
|
-
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
325
|
-
created.push(tokPath);
|
|
326
|
-
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
327
|
-
created.push(outDirPath);
|
|
328
|
-
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
329
|
-
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
330
|
-
process.stdout.write(" Created files:\n");
|
|
331
|
-
for (const p of created) {
|
|
332
|
-
process.stdout.write(` ${p}
|
|
333
|
-
`);
|
|
334
|
-
}
|
|
335
|
-
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
336
|
-
return {
|
|
337
|
-
success: true,
|
|
338
|
-
message: "Project initialised successfully.",
|
|
339
|
-
created,
|
|
340
|
-
skipped: false
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function createInitCommand() {
|
|
344
|
-
return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
345
|
-
try {
|
|
346
|
-
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
347
|
-
if (!result.success && !result.skipped) {
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
350
|
-
} catch (err) {
|
|
351
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
352
|
-
`);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
358
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
359
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
360
|
-
}
|
|
361
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
362
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
363
|
-
const wrapperCode = (
|
|
364
|
-
/* ts */
|
|
365
|
-
`
|
|
366
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
367
|
-
import { createRoot } from "react-dom/client";
|
|
368
|
-
import { createElement } from "react";
|
|
369
|
-
|
|
370
|
-
(function scopeRenderHarness() {
|
|
371
|
-
var Component =
|
|
372
|
-
__scopeMod["default"] ||
|
|
373
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
374
|
-
(Object.values(__scopeMod).find(
|
|
375
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
376
|
-
));
|
|
377
|
-
|
|
378
|
-
if (!Component) {
|
|
379
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
380
|
-
"No renderable component found. Checked: default, " +
|
|
381
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
382
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
383
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
var props = ${propsJson};
|
|
389
|
-
var rootEl = document.getElementById("scope-root");
|
|
390
|
-
if (!rootEl) {
|
|
391
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
392
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
396
|
-
// Use requestAnimationFrame to let React flush the render
|
|
397
|
-
requestAnimationFrame(function() {
|
|
398
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
399
|
-
});
|
|
400
|
-
} catch (err) {
|
|
401
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
402
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
403
|
-
}
|
|
404
|
-
})();
|
|
405
|
-
`
|
|
406
|
-
);
|
|
407
|
-
const result = await esbuild.build({
|
|
408
|
-
stdin: {
|
|
409
|
-
contents: wrapperCode,
|
|
410
|
-
// Resolve relative imports (within the component's dir)
|
|
411
|
-
resolveDir: dirname(filePath),
|
|
412
|
-
loader: "tsx",
|
|
413
|
-
sourcefile: "__scope_harness__.tsx"
|
|
414
|
-
},
|
|
415
|
-
bundle: true,
|
|
416
|
-
format: "iife",
|
|
417
|
-
write: false,
|
|
418
|
-
platform: "browser",
|
|
419
|
-
jsx: "automatic",
|
|
420
|
-
jsxImportSource: "react",
|
|
421
|
-
target: "es2020",
|
|
422
|
-
// Bundle everything — no externals
|
|
423
|
-
external: [],
|
|
424
|
-
define: {
|
|
425
|
-
"process.env.NODE_ENV": '"development"',
|
|
426
|
-
global: "globalThis"
|
|
427
|
-
},
|
|
428
|
-
logLevel: "silent",
|
|
429
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
430
|
-
banner: {
|
|
431
|
-
js: "/* @agent-scope/cli component harness */"
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
if (result.errors.length > 0) {
|
|
435
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
436
|
-
throw new Error(`esbuild failed to bundle component:
|
|
437
|
-
${msg}`);
|
|
438
|
-
}
|
|
439
|
-
const outputFile = result.outputFiles?.[0];
|
|
440
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
441
|
-
throw new Error("esbuild produced no output");
|
|
442
|
-
}
|
|
443
|
-
return outputFile.text;
|
|
444
|
-
}
|
|
445
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
446
|
-
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
447
|
-
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
448
|
-
</style>` : "";
|
|
449
|
-
return `<!DOCTYPE html>
|
|
450
|
-
<html lang="en">
|
|
451
|
-
<head>
|
|
452
|
-
<meta charset="UTF-8" />
|
|
453
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
454
|
-
<style>
|
|
455
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
456
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
457
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
458
|
-
</style>
|
|
459
|
-
${projectStyleBlock}
|
|
460
|
-
</head>
|
|
461
|
-
<body>
|
|
462
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
463
|
-
<script>${bundledScript}</script>
|
|
464
|
-
</body>
|
|
465
|
-
</html>`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// src/manifest-formatter.ts
|
|
469
|
-
function isTTY() {
|
|
470
|
-
return process.stdout.isTTY === true;
|
|
471
|
-
}
|
|
472
|
-
function pad(value, width) {
|
|
473
|
-
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
474
|
-
}
|
|
475
|
-
function buildTable(headers, rows) {
|
|
476
|
-
const colWidths = headers.map(
|
|
477
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
478
|
-
);
|
|
479
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
480
|
-
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
481
|
-
const dataRows = rows.map(
|
|
482
|
-
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
483
|
-
);
|
|
484
|
-
return [headerRow, divider, ...dataRows].join("\n");
|
|
485
|
-
}
|
|
486
|
-
function formatListTable(rows) {
|
|
487
|
-
if (rows.length === 0) return "No components found.";
|
|
488
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
489
|
-
const tableRows = rows.map((r) => [
|
|
490
|
-
r.name,
|
|
491
|
-
r.file,
|
|
492
|
-
r.complexityClass,
|
|
493
|
-
String(r.hookCount),
|
|
494
|
-
String(r.contextCount)
|
|
495
|
-
]);
|
|
496
|
-
return buildTable(headers, tableRows);
|
|
497
|
-
}
|
|
498
|
-
function formatListJson(rows) {
|
|
499
|
-
return JSON.stringify(rows, null, 2);
|
|
500
|
-
}
|
|
501
|
-
function formatSideEffects(se) {
|
|
502
|
-
const parts = [];
|
|
503
|
-
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
504
|
-
if (se.timers) parts.push("timers");
|
|
505
|
-
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
506
|
-
if (se.globalListeners) parts.push("globalListeners");
|
|
507
|
-
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
508
|
-
}
|
|
509
|
-
function formatGetTable(name, descriptor) {
|
|
510
|
-
const propNames = Object.keys(descriptor.props);
|
|
511
|
-
const lines = [
|
|
512
|
-
`Component: ${name}`,
|
|
513
|
-
` File: ${descriptor.filePath}`,
|
|
514
|
-
` Export: ${descriptor.exportType}`,
|
|
515
|
-
` Display Name: ${descriptor.displayName}`,
|
|
516
|
-
` Complexity: ${descriptor.complexityClass}`,
|
|
517
|
-
` Memoized: ${descriptor.memoized}`,
|
|
518
|
-
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
519
|
-
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
520
|
-
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
521
|
-
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
522
|
-
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
523
|
-
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
524
|
-
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
525
|
-
"",
|
|
526
|
-
` Props (${propNames.length}):`
|
|
527
|
-
];
|
|
528
|
-
if (propNames.length === 0) {
|
|
529
|
-
lines.push(" (none)");
|
|
1190
|
+
`);
|
|
530
1191
|
} else {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1192
|
+
const rl = createRL();
|
|
1193
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
1194
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
1195
|
+
try {
|
|
1196
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
1197
|
+
`);
|
|
1198
|
+
const includeRaw = await askWithDefault(
|
|
1199
|
+
rl,
|
|
1200
|
+
"Component include patterns (comma-separated)",
|
|
1201
|
+
config.components.include.join(", ")
|
|
1202
|
+
);
|
|
1203
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1204
|
+
const excludeRaw = await askWithDefault(
|
|
1205
|
+
rl,
|
|
1206
|
+
"Component exclude patterns (comma-separated)",
|
|
1207
|
+
config.components.exclude.join(", ")
|
|
1208
|
+
);
|
|
1209
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1210
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
1211
|
+
config.tokens.file = tokenFile;
|
|
1212
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
1213
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
1214
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
1215
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
1216
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
1217
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1218
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1219
|
+
} finally {
|
|
1220
|
+
rl.close();
|
|
538
1221
|
}
|
|
1222
|
+
process.stdout.write("\n");
|
|
539
1223
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return
|
|
1224
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1225
|
+
created.push(cfgPath);
|
|
1226
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
1227
|
+
created.push(tokPath);
|
|
1228
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1229
|
+
created.push(outDirPath);
|
|
1230
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
1231
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
1232
|
+
process.stdout.write(" Created files:\n");
|
|
1233
|
+
for (const p of created) {
|
|
1234
|
+
process.stdout.write(` ${p}
|
|
1235
|
+
`);
|
|
1236
|
+
}
|
|
1237
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
1238
|
+
return {
|
|
1239
|
+
success: true,
|
|
1240
|
+
message: "Project initialised successfully.",
|
|
1241
|
+
created,
|
|
1242
|
+
skipped: false
|
|
1243
|
+
};
|
|
555
1244
|
}
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1245
|
+
function createInitCommand() {
|
|
1246
|
+
return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
1247
|
+
try {
|
|
1248
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1249
|
+
if (!result.success && !result.skipped) {
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1254
|
+
`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
561
1258
|
}
|
|
562
|
-
|
|
563
|
-
// src/manifest-commands.ts
|
|
564
1259
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
565
1260
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
566
1261
|
const absPath = resolve(process.cwd(), manifestPath);
|
|
@@ -713,183 +1408,23 @@ function registerGenerate(manifestCmd) {
|
|
|
713
1408
|
process.stderr.write(`Manifest written to ${outputPath}
|
|
714
1409
|
`);
|
|
715
1410
|
process.stdout.write(`${outputPath}
|
|
716
|
-
`);
|
|
717
|
-
} catch (err) {
|
|
718
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
719
|
-
`);
|
|
720
|
-
process.exit(1);
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
function createManifestCommand() {
|
|
725
|
-
const manifestCmd = new Command("manifest").description(
|
|
726
|
-
"Query and explore the component manifest"
|
|
727
|
-
);
|
|
728
|
-
registerList(manifestCmd);
|
|
729
|
-
registerGet(manifestCmd);
|
|
730
|
-
registerQuery(manifestCmd);
|
|
731
|
-
registerGenerate(manifestCmd);
|
|
732
|
-
return manifestCmd;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// src/render-formatter.ts
|
|
736
|
-
function parseViewport(spec) {
|
|
737
|
-
const lower = spec.toLowerCase();
|
|
738
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
739
|
-
if (!match) {
|
|
740
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
741
|
-
}
|
|
742
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
743
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
744
|
-
if (width <= 0 || height <= 0) {
|
|
745
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
746
|
-
}
|
|
747
|
-
return { width, height };
|
|
748
|
-
}
|
|
749
|
-
function formatRenderJson(componentName, props, result) {
|
|
750
|
-
return {
|
|
751
|
-
component: componentName,
|
|
752
|
-
props,
|
|
753
|
-
width: result.width,
|
|
754
|
-
height: result.height,
|
|
755
|
-
renderTimeMs: result.renderTimeMs,
|
|
756
|
-
computedStyles: result.computedStyles,
|
|
757
|
-
screenshot: result.screenshot.toString("base64"),
|
|
758
|
-
dom: result.dom,
|
|
759
|
-
console: result.console,
|
|
760
|
-
accessibility: result.accessibility
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
function formatMatrixJson(result) {
|
|
764
|
-
return {
|
|
765
|
-
axes: result.axes.map((axis) => ({
|
|
766
|
-
name: axis.name,
|
|
767
|
-
values: axis.values.map((v) => String(v))
|
|
768
|
-
})),
|
|
769
|
-
stats: { ...result.stats },
|
|
770
|
-
cells: result.cells.map((cell) => ({
|
|
771
|
-
index: cell.index,
|
|
772
|
-
axisIndices: cell.axisIndices,
|
|
773
|
-
props: cell.props,
|
|
774
|
-
renderTimeMs: cell.result.renderTimeMs,
|
|
775
|
-
width: cell.result.width,
|
|
776
|
-
height: cell.result.height,
|
|
777
|
-
screenshot: cell.result.screenshot.toString("base64")
|
|
778
|
-
}))
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
function formatMatrixHtml(componentName, result) {
|
|
782
|
-
const cellsHtml = result.cells.map((cell) => {
|
|
783
|
-
const b64 = cell.result.screenshot.toString("base64");
|
|
784
|
-
const propLabel = escapeHtml(
|
|
785
|
-
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
786
|
-
);
|
|
787
|
-
return ` <div class="cell">
|
|
788
|
-
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
789
|
-
<div class="label">${propLabel}</div>
|
|
790
|
-
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
791
|
-
</div>`;
|
|
792
|
-
}).join("\n");
|
|
793
|
-
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
794
|
-
return `<!DOCTYPE html>
|
|
795
|
-
<html lang="en">
|
|
796
|
-
<head>
|
|
797
|
-
<meta charset="UTF-8" />
|
|
798
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
799
|
-
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
800
|
-
<style>
|
|
801
|
-
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
802
|
-
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
803
|
-
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
804
|
-
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
805
|
-
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
806
|
-
.cell img { display: block; }
|
|
807
|
-
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
808
|
-
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
809
|
-
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
810
|
-
</style>
|
|
811
|
-
</head>
|
|
812
|
-
<body>
|
|
813
|
-
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
814
|
-
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
815
|
-
<div class="grid">
|
|
816
|
-
${cellsHtml}
|
|
817
|
-
</div>
|
|
818
|
-
<div class="stats">
|
|
819
|
-
${result.stats.totalCells} cells \xB7
|
|
820
|
-
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
821
|
-
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
822
|
-
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
823
|
-
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
824
|
-
</div>
|
|
825
|
-
</body>
|
|
826
|
-
</html>
|
|
827
|
-
`;
|
|
828
|
-
}
|
|
829
|
-
function formatMatrixCsv(componentName, result) {
|
|
830
|
-
const axisNames = result.axes.map((a) => a.name);
|
|
831
|
-
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
832
|
-
const rows = result.cells.map((cell) => {
|
|
833
|
-
const axisVals = result.axes.map((_, i) => {
|
|
834
|
-
const axisIdx = cell.axisIndices[i];
|
|
835
|
-
const axis = result.axes[i];
|
|
836
|
-
if (axisIdx === void 0 || axis === void 0) return "";
|
|
837
|
-
const val = axis.values[axisIdx];
|
|
838
|
-
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
839
|
-
});
|
|
840
|
-
return [
|
|
841
|
-
csvEscape(componentName),
|
|
842
|
-
...axisVals,
|
|
843
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
844
|
-
String(cell.result.width),
|
|
845
|
-
String(cell.result.height)
|
|
846
|
-
].join(",");
|
|
847
|
-
});
|
|
848
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
849
|
-
`;
|
|
850
|
-
}
|
|
851
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
852
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
853
|
-
const empty = barWidth - filled;
|
|
854
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
855
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
856
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
857
|
-
}
|
|
858
|
-
function formatSummaryText(results, outputDir) {
|
|
859
|
-
const total = results.length;
|
|
860
|
-
const passed = results.filter((r) => r.success).length;
|
|
861
|
-
const failed = total - passed;
|
|
862
|
-
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
863
|
-
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
864
|
-
const lines = [
|
|
865
|
-
"\u2500".repeat(60),
|
|
866
|
-
`Render Summary`,
|
|
867
|
-
"\u2500".repeat(60),
|
|
868
|
-
` Total components : ${total}`,
|
|
869
|
-
` Passed : ${passed}`,
|
|
870
|
-
` Failed : ${failed}`,
|
|
871
|
-
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
872
|
-
` Output dir : ${outputDir}`
|
|
873
|
-
];
|
|
874
|
-
if (failed > 0) {
|
|
875
|
-
lines.push("", " Failed components:");
|
|
876
|
-
for (const r of results) {
|
|
877
|
-
if (!r.success) {
|
|
878
|
-
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
879
|
-
}
|
|
1411
|
+
`);
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1414
|
+
`);
|
|
1415
|
+
process.exit(1);
|
|
880
1416
|
}
|
|
881
|
-
}
|
|
882
|
-
lines.push("\u2500".repeat(60));
|
|
883
|
-
return lines.join("\n");
|
|
884
|
-
}
|
|
885
|
-
function escapeHtml(str) {
|
|
886
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1417
|
+
});
|
|
887
1418
|
}
|
|
888
|
-
function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1419
|
+
function createManifestCommand() {
|
|
1420
|
+
const manifestCmd = new Command("manifest").description(
|
|
1421
|
+
"Query and explore the component manifest"
|
|
1422
|
+
);
|
|
1423
|
+
registerList(manifestCmd);
|
|
1424
|
+
registerGet(manifestCmd);
|
|
1425
|
+
registerQuery(manifestCmd);
|
|
1426
|
+
registerGenerate(manifestCmd);
|
|
1427
|
+
return manifestCmd;
|
|
893
1428
|
}
|
|
894
1429
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
895
1430
|
function buildHookInstrumentationScript() {
|
|
@@ -1529,22 +2064,22 @@ Available: ${available}`
|
|
|
1529
2064
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1530
2065
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1531
2066
|
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1532
|
-
var
|
|
1533
|
-
async function
|
|
1534
|
-
if (
|
|
1535
|
-
|
|
2067
|
+
var _pool2 = null;
|
|
2068
|
+
async function getPool2() {
|
|
2069
|
+
if (_pool2 === null) {
|
|
2070
|
+
_pool2 = new BrowserPool({
|
|
1536
2071
|
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1537
2072
|
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1538
2073
|
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1539
2074
|
});
|
|
1540
|
-
await
|
|
2075
|
+
await _pool2.init();
|
|
1541
2076
|
}
|
|
1542
|
-
return
|
|
2077
|
+
return _pool2;
|
|
1543
2078
|
}
|
|
1544
|
-
async function
|
|
1545
|
-
if (
|
|
1546
|
-
await
|
|
1547
|
-
|
|
2079
|
+
async function shutdownPool2() {
|
|
2080
|
+
if (_pool2 !== null) {
|
|
2081
|
+
await _pool2.close();
|
|
2082
|
+
_pool2 = null;
|
|
1548
2083
|
}
|
|
1549
2084
|
}
|
|
1550
2085
|
function mapNodeType(node) {
|
|
@@ -1756,7 +2291,7 @@ function formatInstrumentTree(root, showProviderDepth = false) {
|
|
|
1756
2291
|
}
|
|
1757
2292
|
async function runInstrumentTree(options) {
|
|
1758
2293
|
const { componentName, filePath } = options;
|
|
1759
|
-
const pool = await
|
|
2294
|
+
const pool = await getPool2();
|
|
1760
2295
|
const slot = await pool.acquire();
|
|
1761
2296
|
const { page } = slot;
|
|
1762
2297
|
try {
|
|
@@ -1853,7 +2388,7 @@ Available: ${available}`
|
|
|
1853
2388
|
providerDepth: opts.providerDepth,
|
|
1854
2389
|
wastedRenders: opts.wastedRenders
|
|
1855
2390
|
});
|
|
1856
|
-
await
|
|
2391
|
+
await shutdownPool2();
|
|
1857
2392
|
const fmt = resolveFormat2(opts.format);
|
|
1858
2393
|
if (fmt === "json") {
|
|
1859
2394
|
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
@@ -1864,7 +2399,7 @@ Available: ${available}`
|
|
|
1864
2399
|
`);
|
|
1865
2400
|
}
|
|
1866
2401
|
} catch (err) {
|
|
1867
|
-
await
|
|
2402
|
+
await shutdownPool2();
|
|
1868
2403
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1869
2404
|
`);
|
|
1870
2405
|
process.exit(1);
|
|
@@ -2182,22 +2717,22 @@ async function replayInteraction2(page, steps) {
|
|
|
2182
2717
|
}
|
|
2183
2718
|
}
|
|
2184
2719
|
}
|
|
2185
|
-
var
|
|
2186
|
-
async function
|
|
2187
|
-
if (
|
|
2188
|
-
|
|
2720
|
+
var _pool3 = null;
|
|
2721
|
+
async function getPool3() {
|
|
2722
|
+
if (_pool3 === null) {
|
|
2723
|
+
_pool3 = new BrowserPool({
|
|
2189
2724
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
2190
2725
|
viewportWidth: 1280,
|
|
2191
2726
|
viewportHeight: 800
|
|
2192
2727
|
});
|
|
2193
|
-
await
|
|
2728
|
+
await _pool3.init();
|
|
2194
2729
|
}
|
|
2195
|
-
return
|
|
2730
|
+
return _pool3;
|
|
2196
2731
|
}
|
|
2197
|
-
async function
|
|
2198
|
-
if (
|
|
2199
|
-
await
|
|
2200
|
-
|
|
2732
|
+
async function shutdownPool3() {
|
|
2733
|
+
if (_pool3 !== null) {
|
|
2734
|
+
await _pool3.close();
|
|
2735
|
+
_pool3 = null;
|
|
2201
2736
|
}
|
|
2202
2737
|
}
|
|
2203
2738
|
async function analyzeRenders(options) {
|
|
@@ -2214,7 +2749,7 @@ Available: ${available}`
|
|
|
2214
2749
|
const rootDir = process.cwd();
|
|
2215
2750
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
2216
2751
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
2217
|
-
const pool = await
|
|
2752
|
+
const pool = await getPool3();
|
|
2218
2753
|
const slot = await pool.acquire();
|
|
2219
2754
|
const { page } = slot;
|
|
2220
2755
|
const startMs = performance.now();
|
|
@@ -2322,7 +2857,7 @@ function createInstrumentRendersCommand() {
|
|
|
2322
2857
|
interaction,
|
|
2323
2858
|
manifestPath: opts.manifest
|
|
2324
2859
|
});
|
|
2325
|
-
await
|
|
2860
|
+
await shutdownPool3();
|
|
2326
2861
|
if (opts.json || !isTTY()) {
|
|
2327
2862
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2328
2863
|
`);
|
|
@@ -2331,7 +2866,7 @@ function createInstrumentRendersCommand() {
|
|
|
2331
2866
|
`);
|
|
2332
2867
|
}
|
|
2333
2868
|
} catch (err) {
|
|
2334
|
-
await
|
|
2869
|
+
await shutdownPool3();
|
|
2335
2870
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2336
2871
|
`);
|
|
2337
2872
|
process.exit(1);
|
|
@@ -2389,141 +2924,24 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
2389
2924
|
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2390
2925
|
writeFileSync(outputPath, json, "utf-8");
|
|
2391
2926
|
}
|
|
2392
|
-
var CONFIG_FILENAMES = [
|
|
2393
|
-
".reactscope/config.json",
|
|
2394
|
-
".reactscope/config.js",
|
|
2395
|
-
".reactscope/config.mjs"
|
|
2396
|
-
];
|
|
2397
|
-
var STYLE_ENTRY_CANDIDATES = [
|
|
2398
|
-
"src/index.css",
|
|
2399
|
-
"src/globals.css",
|
|
2400
|
-
"app/globals.css",
|
|
2401
|
-
"app/index.css",
|
|
2402
|
-
"styles/index.css",
|
|
2403
|
-
"index.css"
|
|
2404
|
-
];
|
|
2405
|
-
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2406
|
-
var compilerCache = null;
|
|
2407
|
-
function getCachedBuild(cwd) {
|
|
2408
|
-
if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
|
|
2409
|
-
return compilerCache.build;
|
|
2410
|
-
}
|
|
2411
|
-
return null;
|
|
2412
|
-
}
|
|
2413
|
-
function findStylesEntry(cwd) {
|
|
2414
|
-
for (const name of CONFIG_FILENAMES) {
|
|
2415
|
-
const p = resolve(cwd, name);
|
|
2416
|
-
if (!existsSync(p)) continue;
|
|
2417
|
-
try {
|
|
2418
|
-
if (name.endsWith(".json")) {
|
|
2419
|
-
const raw = readFileSync(p, "utf-8");
|
|
2420
|
-
const data = JSON.parse(raw);
|
|
2421
|
-
const scope = data.scope;
|
|
2422
|
-
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2423
|
-
if (typeof entry === "string") {
|
|
2424
|
-
const full = resolve(cwd, entry);
|
|
2425
|
-
if (existsSync(full)) return full;
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
} catch {
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
const pkgPath = resolve(cwd, "package.json");
|
|
2432
|
-
if (existsSync(pkgPath)) {
|
|
2433
|
-
try {
|
|
2434
|
-
const raw = readFileSync(pkgPath, "utf-8");
|
|
2435
|
-
const pkg = JSON.parse(raw);
|
|
2436
|
-
const entry = pkg.scope?.stylesEntry;
|
|
2437
|
-
if (typeof entry === "string") {
|
|
2438
|
-
const full = resolve(cwd, entry);
|
|
2439
|
-
if (existsSync(full)) return full;
|
|
2440
|
-
}
|
|
2441
|
-
} catch {
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2445
|
-
const full = resolve(cwd, candidate);
|
|
2446
|
-
if (existsSync(full)) {
|
|
2447
|
-
try {
|
|
2448
|
-
const content = readFileSync(full, "utf-8");
|
|
2449
|
-
if (TAILWIND_IMPORT.test(content)) return full;
|
|
2450
|
-
} catch {
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
return null;
|
|
2455
|
-
}
|
|
2456
|
-
async function getTailwindCompiler(cwd) {
|
|
2457
|
-
const cached = getCachedBuild(cwd);
|
|
2458
|
-
if (cached !== null) return cached;
|
|
2459
|
-
const entryPath = findStylesEntry(cwd);
|
|
2460
|
-
if (entryPath === null) return null;
|
|
2461
|
-
let compile;
|
|
2462
|
-
try {
|
|
2463
|
-
const require2 = createRequire(resolve(cwd, "package.json"));
|
|
2464
|
-
const tailwind = require2("tailwindcss");
|
|
2465
|
-
const fn = tailwind.compile;
|
|
2466
|
-
if (typeof fn !== "function") return null;
|
|
2467
|
-
compile = fn;
|
|
2468
|
-
} catch {
|
|
2469
|
-
return null;
|
|
2470
|
-
}
|
|
2471
|
-
const entryContent = readFileSync(entryPath, "utf-8");
|
|
2472
|
-
const loadStylesheet = async (id, base) => {
|
|
2473
|
-
if (id === "tailwindcss") {
|
|
2474
|
-
const nodeModules = resolve(cwd, "node_modules");
|
|
2475
|
-
const tailwindCssPath = resolve(nodeModules, "tailwindcss", "index.css");
|
|
2476
|
-
if (!existsSync(tailwindCssPath)) {
|
|
2477
|
-
throw new Error(
|
|
2478
|
-
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
2479
|
-
);
|
|
2480
|
-
}
|
|
2481
|
-
const content = readFileSync(tailwindCssPath, "utf-8");
|
|
2482
|
-
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2483
|
-
}
|
|
2484
|
-
const full = resolve(base, id);
|
|
2485
|
-
if (existsSync(full)) {
|
|
2486
|
-
const content = readFileSync(full, "utf-8");
|
|
2487
|
-
return { path: full, base: resolve(full, ".."), content };
|
|
2488
|
-
}
|
|
2489
|
-
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2490
|
-
};
|
|
2491
|
-
const result = await compile(entryContent, {
|
|
2492
|
-
base: cwd,
|
|
2493
|
-
from: entryPath,
|
|
2494
|
-
loadStylesheet
|
|
2495
|
-
});
|
|
2496
|
-
const build2 = result.build.bind(result);
|
|
2497
|
-
compilerCache = { cwd, build: build2 };
|
|
2498
|
-
return build2;
|
|
2499
|
-
}
|
|
2500
|
-
async function getCompiledCssForClasses(cwd, classes) {
|
|
2501
|
-
const build2 = await getTailwindCompiler(cwd);
|
|
2502
|
-
if (build2 === null) return null;
|
|
2503
|
-
const deduped = [...new Set(classes)].filter(Boolean);
|
|
2504
|
-
if (deduped.length === 0) return null;
|
|
2505
|
-
return build2(deduped);
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
// src/render-commands.ts
|
|
2509
2927
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2510
2928
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2511
|
-
var
|
|
2512
|
-
async function
|
|
2513
|
-
if (
|
|
2514
|
-
|
|
2929
|
+
var _pool4 = null;
|
|
2930
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
2931
|
+
if (_pool4 === null) {
|
|
2932
|
+
_pool4 = new BrowserPool({
|
|
2515
2933
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2516
2934
|
viewportWidth,
|
|
2517
2935
|
viewportHeight
|
|
2518
2936
|
});
|
|
2519
|
-
await
|
|
2937
|
+
await _pool4.init();
|
|
2520
2938
|
}
|
|
2521
|
-
return
|
|
2939
|
+
return _pool4;
|
|
2522
2940
|
}
|
|
2523
|
-
async function
|
|
2524
|
-
if (
|
|
2525
|
-
await
|
|
2526
|
-
|
|
2941
|
+
async function shutdownPool4() {
|
|
2942
|
+
if (_pool4 !== null) {
|
|
2943
|
+
await _pool4.close();
|
|
2944
|
+
_pool4 = null;
|
|
2527
2945
|
}
|
|
2528
2946
|
}
|
|
2529
2947
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2534,7 +2952,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2534
2952
|
_satori: satori,
|
|
2535
2953
|
async renderCell(props, _complexityClass) {
|
|
2536
2954
|
const startMs = performance.now();
|
|
2537
|
-
const pool = await
|
|
2955
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2538
2956
|
const htmlHarness = await buildComponentHarness(
|
|
2539
2957
|
filePath,
|
|
2540
2958
|
componentName,
|
|
@@ -2670,7 +3088,7 @@ Available: ${available}`
|
|
|
2670
3088
|
}
|
|
2671
3089
|
}
|
|
2672
3090
|
);
|
|
2673
|
-
await
|
|
3091
|
+
await shutdownPool4();
|
|
2674
3092
|
if (outcome.crashed) {
|
|
2675
3093
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2676
3094
|
`);
|
|
@@ -2718,7 +3136,7 @@ Available: ${available}`
|
|
|
2718
3136
|
);
|
|
2719
3137
|
}
|
|
2720
3138
|
} catch (err) {
|
|
2721
|
-
await
|
|
3139
|
+
await shutdownPool4();
|
|
2722
3140
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2723
3141
|
`);
|
|
2724
3142
|
process.exit(1);
|
|
@@ -2803,7 +3221,7 @@ Available: ${available}`
|
|
|
2803
3221
|
concurrency
|
|
2804
3222
|
});
|
|
2805
3223
|
const result = await matrix.render();
|
|
2806
|
-
await
|
|
3224
|
+
await shutdownPool4();
|
|
2807
3225
|
process.stderr.write(
|
|
2808
3226
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2809
3227
|
`
|
|
@@ -2848,7 +3266,7 @@ Available: ${available}`
|
|
|
2848
3266
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2849
3267
|
}
|
|
2850
3268
|
} catch (err) {
|
|
2851
|
-
await
|
|
3269
|
+
await shutdownPool4();
|
|
2852
3270
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2853
3271
|
`);
|
|
2854
3272
|
process.exit(1);
|
|
@@ -2945,13 +3363,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2945
3363
|
workers.push(worker());
|
|
2946
3364
|
}
|
|
2947
3365
|
await Promise.all(workers);
|
|
2948
|
-
await
|
|
3366
|
+
await shutdownPool4();
|
|
2949
3367
|
process.stderr.write("\n");
|
|
2950
3368
|
const summary = formatSummaryText(results, outputDir);
|
|
2951
3369
|
process.stderr.write(`${summary}
|
|
2952
3370
|
`);
|
|
2953
3371
|
} catch (err) {
|
|
2954
|
-
await
|
|
3372
|
+
await shutdownPool4();
|
|
2955
3373
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2956
3374
|
`);
|
|
2957
3375
|
process.exit(1);
|
|
@@ -2993,26 +3411,26 @@ function createRenderCommand() {
|
|
|
2993
3411
|
return renderCmd;
|
|
2994
3412
|
}
|
|
2995
3413
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2996
|
-
var
|
|
2997
|
-
async function
|
|
2998
|
-
if (
|
|
2999
|
-
|
|
3414
|
+
var _pool5 = null;
|
|
3415
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3416
|
+
if (_pool5 === null) {
|
|
3417
|
+
_pool5 = new BrowserPool({
|
|
3000
3418
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3001
3419
|
viewportWidth,
|
|
3002
3420
|
viewportHeight
|
|
3003
3421
|
});
|
|
3004
|
-
await
|
|
3422
|
+
await _pool5.init();
|
|
3005
3423
|
}
|
|
3006
|
-
return
|
|
3424
|
+
return _pool5;
|
|
3007
3425
|
}
|
|
3008
|
-
async function
|
|
3009
|
-
if (
|
|
3010
|
-
await
|
|
3011
|
-
|
|
3426
|
+
async function shutdownPool5() {
|
|
3427
|
+
if (_pool5 !== null) {
|
|
3428
|
+
await _pool5.close();
|
|
3429
|
+
_pool5 = null;
|
|
3012
3430
|
}
|
|
3013
3431
|
}
|
|
3014
|
-
async function
|
|
3015
|
-
const pool = await
|
|
3432
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3433
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3016
3434
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3017
3435
|
const slot = await pool.acquire();
|
|
3018
3436
|
const { page } = slot;
|
|
@@ -3102,7 +3520,7 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
|
|
|
3102
3520
|
pool.release(slot);
|
|
3103
3521
|
}
|
|
3104
3522
|
}
|
|
3105
|
-
function
|
|
3523
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3106
3524
|
const flat = {};
|
|
3107
3525
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3108
3526
|
Object.assign(flat, styles);
|
|
@@ -3145,12 +3563,12 @@ async function runBaseline(options = {}) {
|
|
|
3145
3563
|
mkdirSync(rendersDir, { recursive: true });
|
|
3146
3564
|
let manifest;
|
|
3147
3565
|
if (manifestPath !== void 0) {
|
|
3148
|
-
const { readFileSync:
|
|
3566
|
+
const { readFileSync: readFileSync11 } = await import('fs');
|
|
3149
3567
|
const absPath = resolve(rootDir, manifestPath);
|
|
3150
3568
|
if (!existsSync(absPath)) {
|
|
3151
3569
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3152
3570
|
}
|
|
3153
|
-
manifest = JSON.parse(
|
|
3571
|
+
manifest = JSON.parse(readFileSync11(absPath, "utf-8"));
|
|
3154
3572
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3155
3573
|
`);
|
|
3156
3574
|
} else {
|
|
@@ -3204,7 +3622,7 @@ async function runBaseline(options = {}) {
|
|
|
3204
3622
|
if (descriptor === void 0) return;
|
|
3205
3623
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3206
3624
|
const outcome = await safeRender(
|
|
3207
|
-
() =>
|
|
3625
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3208
3626
|
{
|
|
3209
3627
|
props: {},
|
|
3210
3628
|
sourceLocation: {
|
|
@@ -3246,7 +3664,7 @@ async function runBaseline(options = {}) {
|
|
|
3246
3664
|
JSON.stringify(jsonOutput, null, 2),
|
|
3247
3665
|
"utf-8"
|
|
3248
3666
|
);
|
|
3249
|
-
computedStylesMap.set(name,
|
|
3667
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3250
3668
|
};
|
|
3251
3669
|
const worker = async () => {
|
|
3252
3670
|
while (nextIdx < componentNames.length) {
|
|
@@ -3262,7 +3680,7 @@ async function runBaseline(options = {}) {
|
|
|
3262
3680
|
workers.push(worker());
|
|
3263
3681
|
}
|
|
3264
3682
|
await Promise.all(workers);
|
|
3265
|
-
await
|
|
3683
|
+
await shutdownPool5();
|
|
3266
3684
|
if (isTTY()) {
|
|
3267
3685
|
process.stderr.write("\n");
|
|
3268
3686
|
}
|
|
@@ -3318,31 +3736,31 @@ function loadBaselineCompliance(baselineDir) {
|
|
|
3318
3736
|
const raw = JSON.parse(readFileSync(compliancePath, "utf-8"));
|
|
3319
3737
|
return raw;
|
|
3320
3738
|
}
|
|
3321
|
-
function
|
|
3739
|
+
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3322
3740
|
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3323
3741
|
if (!existsSync(jsonPath)) return null;
|
|
3324
3742
|
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
3325
3743
|
}
|
|
3326
|
-
var
|
|
3327
|
-
async function
|
|
3328
|
-
if (
|
|
3329
|
-
|
|
3744
|
+
var _pool6 = null;
|
|
3745
|
+
async function getPool6(viewportWidth, viewportHeight) {
|
|
3746
|
+
if (_pool6 === null) {
|
|
3747
|
+
_pool6 = new BrowserPool({
|
|
3330
3748
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3331
3749
|
viewportWidth,
|
|
3332
3750
|
viewportHeight
|
|
3333
3751
|
});
|
|
3334
|
-
await
|
|
3752
|
+
await _pool6.init();
|
|
3335
3753
|
}
|
|
3336
|
-
return
|
|
3754
|
+
return _pool6;
|
|
3337
3755
|
}
|
|
3338
|
-
async function
|
|
3339
|
-
if (
|
|
3340
|
-
await
|
|
3341
|
-
|
|
3756
|
+
async function shutdownPool6() {
|
|
3757
|
+
if (_pool6 !== null) {
|
|
3758
|
+
await _pool6.close();
|
|
3759
|
+
_pool6 = null;
|
|
3342
3760
|
}
|
|
3343
3761
|
}
|
|
3344
|
-
async function
|
|
3345
|
-
const pool = await
|
|
3762
|
+
async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3763
|
+
const pool = await getPool6(viewportWidth, viewportHeight);
|
|
3346
3764
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3347
3765
|
const slot = await pool.acquire();
|
|
3348
3766
|
const { page } = slot;
|
|
@@ -3432,7 +3850,7 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
|
|
|
3432
3850
|
pool.release(slot);
|
|
3433
3851
|
}
|
|
3434
3852
|
}
|
|
3435
|
-
function
|
|
3853
|
+
function extractComputedStyles3(computedStylesRaw) {
|
|
3436
3854
|
const flat = {};
|
|
3437
3855
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3438
3856
|
Object.assign(flat, styles);
|
|
@@ -3548,7 +3966,7 @@ async function runDiff(options = {}) {
|
|
|
3548
3966
|
if (descriptor === void 0) return;
|
|
3549
3967
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3550
3968
|
const outcome = await safeRender(
|
|
3551
|
-
() =>
|
|
3969
|
+
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3552
3970
|
{
|
|
3553
3971
|
props: {},
|
|
3554
3972
|
sourceLocation: {
|
|
@@ -3573,7 +3991,7 @@ async function runDiff(options = {}) {
|
|
|
3573
3991
|
height: result.height,
|
|
3574
3992
|
renderTimeMs: result.renderTimeMs
|
|
3575
3993
|
});
|
|
3576
|
-
computedStylesMap.set(name,
|
|
3994
|
+
computedStylesMap.set(name, extractComputedStyles3(result.computedStyles));
|
|
3577
3995
|
};
|
|
3578
3996
|
if (total > 0) {
|
|
3579
3997
|
const worker = async () => {
|
|
@@ -3591,7 +4009,7 @@ async function runDiff(options = {}) {
|
|
|
3591
4009
|
}
|
|
3592
4010
|
await Promise.all(workers);
|
|
3593
4011
|
}
|
|
3594
|
-
await
|
|
4012
|
+
await shutdownPool6();
|
|
3595
4013
|
if (isTTY() && total > 0) {
|
|
3596
4014
|
process.stderr.write("\n");
|
|
3597
4015
|
}
|
|
@@ -3602,7 +4020,7 @@ async function runDiff(options = {}) {
|
|
|
3602
4020
|
for (const name of componentNames) {
|
|
3603
4021
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3604
4022
|
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3605
|
-
const baselineMeta =
|
|
4023
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3606
4024
|
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3607
4025
|
const failed = renderFailures.has(name);
|
|
3608
4026
|
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
@@ -3622,7 +4040,7 @@ async function runDiff(options = {}) {
|
|
|
3622
4040
|
}
|
|
3623
4041
|
for (const name of removedNames) {
|
|
3624
4042
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3625
|
-
const baselineMeta =
|
|
4043
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3626
4044
|
entries.push({
|
|
3627
4045
|
name,
|
|
3628
4046
|
status: "removed",
|
|
@@ -5074,6 +5492,7 @@ function createProgram(options = {}) {
|
|
|
5074
5492
|
program.addCommand(createTokensCommand());
|
|
5075
5493
|
program.addCommand(createInstrumentCommand());
|
|
5076
5494
|
program.addCommand(createInitCommand());
|
|
5495
|
+
program.addCommand(createCiCommand());
|
|
5077
5496
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
5078
5497
|
if (existingReportCmd !== void 0) {
|
|
5079
5498
|
registerBaselineSubCommand(existingReportCmd);
|
|
@@ -5082,6 +5501,6 @@ function createProgram(options = {}) {
|
|
|
5082
5501
|
return program;
|
|
5083
5502
|
}
|
|
5084
5503
|
|
|
5085
|
-
export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runInit };
|
|
5504
|
+
export { CI_EXIT, createCiCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
|
|
5086
5505
|
//# sourceMappingURL=index.js.map
|
|
5087
5506
|
//# sourceMappingURL=index.js.map
|