@agent-scope/cli 1.14.0 → 1.16.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 +1480 -919
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1193 -640
- 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 +1184 -635
- 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((resolve16) => {
|
|
232
1134
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
1135
|
+
resolve16(answer.trim());
|
|
234
1136
|
});
|
|
235
1137
|
});
|
|
236
1138
|
}
|
|
@@ -324,266 +1226,59 @@ async function runInit(options) {
|
|
|
324
1226
|
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
325
1227
|
const excludeRaw = await askWithDefault(
|
|
326
1228
|
rl,
|
|
327
|
-
"Component exclude patterns (comma-separated)",
|
|
328
|
-
config.components.exclude.join(", ")
|
|
329
|
-
);
|
|
330
|
-
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
331
|
-
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
332
|
-
config.tokens.file = tokenFile;
|
|
333
|
-
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
334
|
-
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
335
|
-
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
336
|
-
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
337
|
-
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
338
|
-
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
339
|
-
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
340
|
-
} finally {
|
|
341
|
-
rl.close();
|
|
342
|
-
}
|
|
343
|
-
process.stdout.write("\n");
|
|
344
|
-
}
|
|
345
|
-
const cfgPath = scaffoldConfig(rootDir, config);
|
|
346
|
-
created.push(cfgPath);
|
|
347
|
-
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
348
|
-
created.push(tokPath);
|
|
349
|
-
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
350
|
-
created.push(outDirPath);
|
|
351
|
-
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
352
|
-
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
353
|
-
process.stdout.write(" Created files:\n");
|
|
354
|
-
for (const p of created) {
|
|
355
|
-
process.stdout.write(` ${p}
|
|
356
|
-
`);
|
|
357
|
-
}
|
|
358
|
-
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
359
|
-
return {
|
|
360
|
-
success: true,
|
|
361
|
-
message: "Project initialised successfully.",
|
|
362
|
-
created,
|
|
363
|
-
skipped: false
|
|
364
|
-
};
|
|
365
|
-
}
|
|
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, projectCss);
|
|
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 = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
470
|
-
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
471
|
-
</style>` : "";
|
|
472
|
-
return `<!DOCTYPE html>
|
|
473
|
-
<html lang="en">
|
|
474
|
-
<head>
|
|
475
|
-
<meta charset="UTF-8" />
|
|
476
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
477
|
-
<style>
|
|
478
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
479
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
480
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
481
|
-
</style>
|
|
482
|
-
${projectStyleBlock}
|
|
483
|
-
</head>
|
|
484
|
-
<body>
|
|
485
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
486
|
-
<script>${bundledScript}</script>
|
|
487
|
-
</body>
|
|
488
|
-
</html>`;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// src/manifest-formatter.ts
|
|
492
|
-
function isTTY() {
|
|
493
|
-
return process.stdout.isTTY === true;
|
|
494
|
-
}
|
|
495
|
-
function pad(value, width) {
|
|
496
|
-
return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
|
|
497
|
-
}
|
|
498
|
-
function buildTable(headers, rows) {
|
|
499
|
-
const colWidths = headers.map(
|
|
500
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
501
|
-
);
|
|
502
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
503
|
-
const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
|
|
504
|
-
const dataRows = rows.map(
|
|
505
|
-
(row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
|
|
506
|
-
);
|
|
507
|
-
return [headerRow, divider, ...dataRows].join("\n");
|
|
508
|
-
}
|
|
509
|
-
function formatListTable(rows) {
|
|
510
|
-
if (rows.length === 0) return "No components found.";
|
|
511
|
-
const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
|
|
512
|
-
const tableRows = rows.map((r) => [
|
|
513
|
-
r.name,
|
|
514
|
-
r.file,
|
|
515
|
-
r.complexityClass,
|
|
516
|
-
String(r.hookCount),
|
|
517
|
-
String(r.contextCount)
|
|
518
|
-
]);
|
|
519
|
-
return buildTable(headers, tableRows);
|
|
520
|
-
}
|
|
521
|
-
function formatListJson(rows) {
|
|
522
|
-
return JSON.stringify(rows, null, 2);
|
|
523
|
-
}
|
|
524
|
-
function formatSideEffects(se) {
|
|
525
|
-
const parts = [];
|
|
526
|
-
if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
|
|
527
|
-
if (se.timers) parts.push("timers");
|
|
528
|
-
if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
|
|
529
|
-
if (se.globalListeners) parts.push("globalListeners");
|
|
530
|
-
return parts.length > 0 ? parts.join(" | ") : "none";
|
|
531
|
-
}
|
|
532
|
-
function formatGetTable(name, descriptor) {
|
|
533
|
-
const propNames = Object.keys(descriptor.props);
|
|
534
|
-
const lines = [
|
|
535
|
-
`Component: ${name}`,
|
|
536
|
-
` File: ${descriptor.filePath}`,
|
|
537
|
-
` Export: ${descriptor.exportType}`,
|
|
538
|
-
` Display Name: ${descriptor.displayName}`,
|
|
539
|
-
` Complexity: ${descriptor.complexityClass}`,
|
|
540
|
-
` Memoized: ${descriptor.memoized}`,
|
|
541
|
-
` Forwarded Ref: ${descriptor.forwardedRef}`,
|
|
542
|
-
` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
|
|
543
|
-
` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
|
|
544
|
-
` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
|
|
545
|
-
` Composes: ${descriptor.composes.join(", ") || "none"}`,
|
|
546
|
-
` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
|
|
547
|
-
` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
|
|
548
|
-
"",
|
|
549
|
-
` Props (${propNames.length}):`
|
|
550
|
-
];
|
|
551
|
-
if (propNames.length === 0) {
|
|
552
|
-
lines.push(" (none)");
|
|
553
|
-
} else {
|
|
554
|
-
for (const propName of propNames) {
|
|
555
|
-
const p = descriptor.props[propName];
|
|
556
|
-
if (p === void 0) continue;
|
|
557
|
-
const req = p.required ? "required" : "optional";
|
|
558
|
-
const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
|
|
559
|
-
const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
|
|
560
|
-
lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
|
|
1229
|
+
"Component exclude patterns (comma-separated)",
|
|
1230
|
+
config.components.exclude.join(", ")
|
|
1231
|
+
);
|
|
1232
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1233
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
1234
|
+
config.tokens.file = tokenFile;
|
|
1235
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
1236
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
1237
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
1238
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
1239
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
1240
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1241
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1242
|
+
} finally {
|
|
1243
|
+
rl.close();
|
|
561
1244
|
}
|
|
1245
|
+
process.stdout.write("\n");
|
|
562
1246
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return
|
|
1247
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1248
|
+
created.push(cfgPath);
|
|
1249
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
1250
|
+
created.push(tokPath);
|
|
1251
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1252
|
+
created.push(outDirPath);
|
|
1253
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
1254
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
1255
|
+
process.stdout.write(" Created files:\n");
|
|
1256
|
+
for (const p of created) {
|
|
1257
|
+
process.stdout.write(` ${p}
|
|
1258
|
+
`);
|
|
1259
|
+
}
|
|
1260
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
1261
|
+
return {
|
|
1262
|
+
success: true,
|
|
1263
|
+
message: "Project initialised successfully.",
|
|
1264
|
+
created,
|
|
1265
|
+
skipped: false
|
|
1266
|
+
};
|
|
578
1267
|
}
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
});
|
|
584
1281
|
}
|
|
585
|
-
|
|
586
|
-
// src/manifest-commands.ts
|
|
587
1282
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
588
1283
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
589
1284
|
const absPath = path.resolve(process.cwd(), manifestPath);
|
|
@@ -754,166 +1449,6 @@ function createManifestCommand() {
|
|
|
754
1449
|
registerGenerate(manifestCmd);
|
|
755
1450
|
return manifestCmd;
|
|
756
1451
|
}
|
|
757
|
-
|
|
758
|
-
// src/render-formatter.ts
|
|
759
|
-
function parseViewport(spec) {
|
|
760
|
-
const lower = spec.toLowerCase();
|
|
761
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
762
|
-
if (!match) {
|
|
763
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
764
|
-
}
|
|
765
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
766
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
767
|
-
if (width <= 0 || height <= 0) {
|
|
768
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
769
|
-
}
|
|
770
|
-
return { width, height };
|
|
771
|
-
}
|
|
772
|
-
function formatRenderJson(componentName, props, result) {
|
|
773
|
-
return {
|
|
774
|
-
component: componentName,
|
|
775
|
-
props,
|
|
776
|
-
width: result.width,
|
|
777
|
-
height: result.height,
|
|
778
|
-
renderTimeMs: result.renderTimeMs,
|
|
779
|
-
computedStyles: result.computedStyles,
|
|
780
|
-
screenshot: result.screenshot.toString("base64"),
|
|
781
|
-
dom: result.dom,
|
|
782
|
-
console: result.console,
|
|
783
|
-
accessibility: result.accessibility
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
function formatMatrixJson(result) {
|
|
787
|
-
return {
|
|
788
|
-
axes: result.axes.map((axis) => ({
|
|
789
|
-
name: axis.name,
|
|
790
|
-
values: axis.values.map((v) => String(v))
|
|
791
|
-
})),
|
|
792
|
-
stats: { ...result.stats },
|
|
793
|
-
cells: result.cells.map((cell) => ({
|
|
794
|
-
index: cell.index,
|
|
795
|
-
axisIndices: cell.axisIndices,
|
|
796
|
-
props: cell.props,
|
|
797
|
-
renderTimeMs: cell.result.renderTimeMs,
|
|
798
|
-
width: cell.result.width,
|
|
799
|
-
height: cell.result.height,
|
|
800
|
-
screenshot: cell.result.screenshot.toString("base64")
|
|
801
|
-
}))
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
function formatMatrixHtml(componentName, result) {
|
|
805
|
-
const cellsHtml = result.cells.map((cell) => {
|
|
806
|
-
const b64 = cell.result.screenshot.toString("base64");
|
|
807
|
-
const propLabel = escapeHtml(
|
|
808
|
-
Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
|
|
809
|
-
);
|
|
810
|
-
return ` <div class="cell">
|
|
811
|
-
<img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
|
|
812
|
-
<div class="label">${propLabel}</div>
|
|
813
|
-
<div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
|
|
814
|
-
</div>`;
|
|
815
|
-
}).join("\n");
|
|
816
|
-
const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
|
|
817
|
-
return `<!DOCTYPE html>
|
|
818
|
-
<html lang="en">
|
|
819
|
-
<head>
|
|
820
|
-
<meta charset="UTF-8" />
|
|
821
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
822
|
-
<title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
|
|
823
|
-
<style>
|
|
824
|
-
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
|
|
825
|
-
h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
|
|
826
|
-
.axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
|
|
827
|
-
.grid { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
828
|
-
.cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
|
|
829
|
-
.cell img { display: block; }
|
|
830
|
-
.label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
|
|
831
|
-
.meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
|
|
832
|
-
.stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
|
|
833
|
-
</style>
|
|
834
|
-
</head>
|
|
835
|
-
<body>
|
|
836
|
-
<h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
|
|
837
|
-
<div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
|
|
838
|
-
<div class="grid">
|
|
839
|
-
${cellsHtml}
|
|
840
|
-
</div>
|
|
841
|
-
<div class="stats">
|
|
842
|
-
${result.stats.totalCells} cells \xB7
|
|
843
|
-
avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
|
|
844
|
-
min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
|
|
845
|
-
max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
|
|
846
|
-
wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
|
|
847
|
-
</div>
|
|
848
|
-
</body>
|
|
849
|
-
</html>
|
|
850
|
-
`;
|
|
851
|
-
}
|
|
852
|
-
function formatMatrixCsv(componentName, result) {
|
|
853
|
-
const axisNames = result.axes.map((a) => a.name);
|
|
854
|
-
const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
|
|
855
|
-
const rows = result.cells.map((cell) => {
|
|
856
|
-
const axisVals = result.axes.map((_, i) => {
|
|
857
|
-
const axisIdx = cell.axisIndices[i];
|
|
858
|
-
const axis = result.axes[i];
|
|
859
|
-
if (axisIdx === void 0 || axis === void 0) return "";
|
|
860
|
-
const val = axis.values[axisIdx];
|
|
861
|
-
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
862
|
-
});
|
|
863
|
-
return [
|
|
864
|
-
csvEscape(componentName),
|
|
865
|
-
...axisVals,
|
|
866
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
867
|
-
String(cell.result.width),
|
|
868
|
-
String(cell.result.height)
|
|
869
|
-
].join(",");
|
|
870
|
-
});
|
|
871
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
872
|
-
`;
|
|
873
|
-
}
|
|
874
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
875
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
876
|
-
const empty = barWidth - filled;
|
|
877
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
878
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
879
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
880
|
-
}
|
|
881
|
-
function formatSummaryText(results, outputDir) {
|
|
882
|
-
const total = results.length;
|
|
883
|
-
const passed = results.filter((r) => r.success).length;
|
|
884
|
-
const failed = total - passed;
|
|
885
|
-
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
886
|
-
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
887
|
-
const lines = [
|
|
888
|
-
"\u2500".repeat(60),
|
|
889
|
-
`Render Summary`,
|
|
890
|
-
"\u2500".repeat(60),
|
|
891
|
-
` Total components : ${total}`,
|
|
892
|
-
` Passed : ${passed}`,
|
|
893
|
-
` Failed : ${failed}`,
|
|
894
|
-
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
895
|
-
` Output dir : ${outputDir}`
|
|
896
|
-
];
|
|
897
|
-
if (failed > 0) {
|
|
898
|
-
lines.push("", " Failed components:");
|
|
899
|
-
for (const r of results) {
|
|
900
|
-
if (!r.success) {
|
|
901
|
-
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
lines.push("\u2500".repeat(60));
|
|
906
|
-
return lines.join("\n");
|
|
907
|
-
}
|
|
908
|
-
function escapeHtml(str) {
|
|
909
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
910
|
-
}
|
|
911
|
-
function csvEscape(value) {
|
|
912
|
-
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
913
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
914
|
-
}
|
|
915
|
-
return value;
|
|
916
|
-
}
|
|
917
1452
|
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
918
1453
|
function buildHookInstrumentationScript() {
|
|
919
1454
|
return `
|
|
@@ -1552,22 +2087,22 @@ Available: ${available}`
|
|
|
1552
2087
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1553
2088
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1554
2089
|
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1555
|
-
var
|
|
1556
|
-
async function
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
2090
|
+
var _pool2 = null;
|
|
2091
|
+
async function getPool2() {
|
|
2092
|
+
if (_pool2 === null) {
|
|
2093
|
+
_pool2 = new render.BrowserPool({
|
|
1559
2094
|
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1560
2095
|
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1561
2096
|
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1562
2097
|
});
|
|
1563
|
-
await
|
|
2098
|
+
await _pool2.init();
|
|
1564
2099
|
}
|
|
1565
|
-
return
|
|
2100
|
+
return _pool2;
|
|
1566
2101
|
}
|
|
1567
|
-
async function
|
|
1568
|
-
if (
|
|
1569
|
-
await
|
|
1570
|
-
|
|
2102
|
+
async function shutdownPool2() {
|
|
2103
|
+
if (_pool2 !== null) {
|
|
2104
|
+
await _pool2.close();
|
|
2105
|
+
_pool2 = null;
|
|
1571
2106
|
}
|
|
1572
2107
|
}
|
|
1573
2108
|
function mapNodeType(node) {
|
|
@@ -1779,7 +2314,7 @@ function formatInstrumentTree(root, showProviderDepth = false) {
|
|
|
1779
2314
|
}
|
|
1780
2315
|
async function runInstrumentTree(options) {
|
|
1781
2316
|
const { componentName, filePath } = options;
|
|
1782
|
-
const pool = await
|
|
2317
|
+
const pool = await getPool2();
|
|
1783
2318
|
const slot = await pool.acquire();
|
|
1784
2319
|
const { page } = slot;
|
|
1785
2320
|
try {
|
|
@@ -1876,9 +2411,9 @@ Available: ${available}`
|
|
|
1876
2411
|
providerDepth: opts.providerDepth,
|
|
1877
2412
|
wastedRenders: opts.wastedRenders
|
|
1878
2413
|
});
|
|
1879
|
-
await
|
|
1880
|
-
const
|
|
1881
|
-
if (
|
|
2414
|
+
await shutdownPool2();
|
|
2415
|
+
const fmt2 = resolveFormat2(opts.format);
|
|
2416
|
+
if (fmt2 === "json") {
|
|
1882
2417
|
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1883
2418
|
`);
|
|
1884
2419
|
} else {
|
|
@@ -1887,7 +2422,7 @@ Available: ${available}`
|
|
|
1887
2422
|
`);
|
|
1888
2423
|
}
|
|
1889
2424
|
} catch (err) {
|
|
1890
|
-
await
|
|
2425
|
+
await shutdownPool2();
|
|
1891
2426
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1892
2427
|
`);
|
|
1893
2428
|
process.exit(1);
|
|
@@ -2205,22 +2740,22 @@ async function replayInteraction2(page, steps) {
|
|
|
2205
2740
|
}
|
|
2206
2741
|
}
|
|
2207
2742
|
}
|
|
2208
|
-
var
|
|
2209
|
-
async function
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2743
|
+
var _pool3 = null;
|
|
2744
|
+
async function getPool3() {
|
|
2745
|
+
if (_pool3 === null) {
|
|
2746
|
+
_pool3 = new render.BrowserPool({
|
|
2212
2747
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
2213
2748
|
viewportWidth: 1280,
|
|
2214
2749
|
viewportHeight: 800
|
|
2215
2750
|
});
|
|
2216
|
-
await
|
|
2751
|
+
await _pool3.init();
|
|
2217
2752
|
}
|
|
2218
|
-
return
|
|
2753
|
+
return _pool3;
|
|
2219
2754
|
}
|
|
2220
|
-
async function
|
|
2221
|
-
if (
|
|
2222
|
-
await
|
|
2223
|
-
|
|
2755
|
+
async function shutdownPool3() {
|
|
2756
|
+
if (_pool3 !== null) {
|
|
2757
|
+
await _pool3.close();
|
|
2758
|
+
_pool3 = null;
|
|
2224
2759
|
}
|
|
2225
2760
|
}
|
|
2226
2761
|
async function analyzeRenders(options) {
|
|
@@ -2237,7 +2772,7 @@ Available: ${available}`
|
|
|
2237
2772
|
const rootDir = process.cwd();
|
|
2238
2773
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
2239
2774
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
2240
|
-
const pool = await
|
|
2775
|
+
const pool = await getPool3();
|
|
2241
2776
|
const slot = await pool.acquire();
|
|
2242
2777
|
const { page } = slot;
|
|
2243
2778
|
const startMs = performance.now();
|
|
@@ -2345,7 +2880,7 @@ function createInstrumentRendersCommand() {
|
|
|
2345
2880
|
interaction,
|
|
2346
2881
|
manifestPath: opts.manifest
|
|
2347
2882
|
});
|
|
2348
|
-
await
|
|
2883
|
+
await shutdownPool3();
|
|
2349
2884
|
if (opts.json || !isTTY()) {
|
|
2350
2885
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2351
2886
|
`);
|
|
@@ -2354,7 +2889,7 @@ function createInstrumentRendersCommand() {
|
|
|
2354
2889
|
`);
|
|
2355
2890
|
}
|
|
2356
2891
|
} catch (err) {
|
|
2357
|
-
await
|
|
2892
|
+
await shutdownPool3();
|
|
2358
2893
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2359
2894
|
`);
|
|
2360
2895
|
process.exit(1);
|
|
@@ -2412,141 +2947,24 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
2412
2947
|
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2413
2948
|
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2414
2949
|
}
|
|
2415
|
-
var CONFIG_FILENAMES = [
|
|
2416
|
-
".reactscope/config.json",
|
|
2417
|
-
".reactscope/config.js",
|
|
2418
|
-
".reactscope/config.mjs"
|
|
2419
|
-
];
|
|
2420
|
-
var STYLE_ENTRY_CANDIDATES = [
|
|
2421
|
-
"src/index.css",
|
|
2422
|
-
"src/globals.css",
|
|
2423
|
-
"app/globals.css",
|
|
2424
|
-
"app/index.css",
|
|
2425
|
-
"styles/index.css",
|
|
2426
|
-
"index.css"
|
|
2427
|
-
];
|
|
2428
|
-
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2429
|
-
var compilerCache = null;
|
|
2430
|
-
function getCachedBuild(cwd) {
|
|
2431
|
-
if (compilerCache !== null && path.resolve(compilerCache.cwd) === path.resolve(cwd)) {
|
|
2432
|
-
return compilerCache.build;
|
|
2433
|
-
}
|
|
2434
|
-
return null;
|
|
2435
|
-
}
|
|
2436
|
-
function findStylesEntry(cwd) {
|
|
2437
|
-
for (const name of CONFIG_FILENAMES) {
|
|
2438
|
-
const p = path.resolve(cwd, name);
|
|
2439
|
-
if (!fs.existsSync(p)) continue;
|
|
2440
|
-
try {
|
|
2441
|
-
if (name.endsWith(".json")) {
|
|
2442
|
-
const raw = fs.readFileSync(p, "utf-8");
|
|
2443
|
-
const data = JSON.parse(raw);
|
|
2444
|
-
const scope = data.scope;
|
|
2445
|
-
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2446
|
-
if (typeof entry === "string") {
|
|
2447
|
-
const full = path.resolve(cwd, entry);
|
|
2448
|
-
if (fs.existsSync(full)) return full;
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
} catch {
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
const pkgPath = path.resolve(cwd, "package.json");
|
|
2455
|
-
if (fs.existsSync(pkgPath)) {
|
|
2456
|
-
try {
|
|
2457
|
-
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
2458
|
-
const pkg = JSON.parse(raw);
|
|
2459
|
-
const entry = pkg.scope?.stylesEntry;
|
|
2460
|
-
if (typeof entry === "string") {
|
|
2461
|
-
const full = path.resolve(cwd, entry);
|
|
2462
|
-
if (fs.existsSync(full)) return full;
|
|
2463
|
-
}
|
|
2464
|
-
} catch {
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2468
|
-
const full = path.resolve(cwd, candidate);
|
|
2469
|
-
if (fs.existsSync(full)) {
|
|
2470
|
-
try {
|
|
2471
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
2472
|
-
if (TAILWIND_IMPORT.test(content)) return full;
|
|
2473
|
-
} catch {
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
return null;
|
|
2478
|
-
}
|
|
2479
|
-
async function getTailwindCompiler(cwd) {
|
|
2480
|
-
const cached = getCachedBuild(cwd);
|
|
2481
|
-
if (cached !== null) return cached;
|
|
2482
|
-
const entryPath = findStylesEntry(cwd);
|
|
2483
|
-
if (entryPath === null) return null;
|
|
2484
|
-
let compile;
|
|
2485
|
-
try {
|
|
2486
|
-
const require2 = module$1.createRequire(path.resolve(cwd, "package.json"));
|
|
2487
|
-
const tailwind = require2("tailwindcss");
|
|
2488
|
-
const fn = tailwind.compile;
|
|
2489
|
-
if (typeof fn !== "function") return null;
|
|
2490
|
-
compile = fn;
|
|
2491
|
-
} catch {
|
|
2492
|
-
return null;
|
|
2493
|
-
}
|
|
2494
|
-
const entryContent = fs.readFileSync(entryPath, "utf-8");
|
|
2495
|
-
const loadStylesheet = async (id, base) => {
|
|
2496
|
-
if (id === "tailwindcss") {
|
|
2497
|
-
const nodeModules = path.resolve(cwd, "node_modules");
|
|
2498
|
-
const tailwindCssPath = path.resolve(nodeModules, "tailwindcss", "index.css");
|
|
2499
|
-
if (!fs.existsSync(tailwindCssPath)) {
|
|
2500
|
-
throw new Error(
|
|
2501
|
-
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
2502
|
-
);
|
|
2503
|
-
}
|
|
2504
|
-
const content = fs.readFileSync(tailwindCssPath, "utf-8");
|
|
2505
|
-
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2506
|
-
}
|
|
2507
|
-
const full = path.resolve(base, id);
|
|
2508
|
-
if (fs.existsSync(full)) {
|
|
2509
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
2510
|
-
return { path: full, base: path.resolve(full, ".."), content };
|
|
2511
|
-
}
|
|
2512
|
-
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2513
|
-
};
|
|
2514
|
-
const result = await compile(entryContent, {
|
|
2515
|
-
base: cwd,
|
|
2516
|
-
from: entryPath,
|
|
2517
|
-
loadStylesheet
|
|
2518
|
-
});
|
|
2519
|
-
const build2 = result.build.bind(result);
|
|
2520
|
-
compilerCache = { cwd, build: build2 };
|
|
2521
|
-
return build2;
|
|
2522
|
-
}
|
|
2523
|
-
async function getCompiledCssForClasses(cwd, classes) {
|
|
2524
|
-
const build2 = await getTailwindCompiler(cwd);
|
|
2525
|
-
if (build2 === null) return null;
|
|
2526
|
-
const deduped = [...new Set(classes)].filter(Boolean);
|
|
2527
|
-
if (deduped.length === 0) return null;
|
|
2528
|
-
return build2(deduped);
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
// src/render-commands.ts
|
|
2532
2950
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2533
2951
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2534
|
-
var
|
|
2535
|
-
async function
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2952
|
+
var _pool4 = null;
|
|
2953
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
2954
|
+
if (_pool4 === null) {
|
|
2955
|
+
_pool4 = new render.BrowserPool({
|
|
2538
2956
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2539
2957
|
viewportWidth,
|
|
2540
2958
|
viewportHeight
|
|
2541
2959
|
});
|
|
2542
|
-
await
|
|
2960
|
+
await _pool4.init();
|
|
2543
2961
|
}
|
|
2544
|
-
return
|
|
2962
|
+
return _pool4;
|
|
2545
2963
|
}
|
|
2546
|
-
async function
|
|
2547
|
-
if (
|
|
2548
|
-
await
|
|
2549
|
-
|
|
2964
|
+
async function shutdownPool4() {
|
|
2965
|
+
if (_pool4 !== null) {
|
|
2966
|
+
await _pool4.close();
|
|
2967
|
+
_pool4 = null;
|
|
2550
2968
|
}
|
|
2551
2969
|
}
|
|
2552
2970
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2557,7 +2975,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2557
2975
|
_satori: satori,
|
|
2558
2976
|
async renderCell(props, _complexityClass) {
|
|
2559
2977
|
const startMs = performance.now();
|
|
2560
|
-
const pool = await
|
|
2978
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2561
2979
|
const htmlHarness = await buildComponentHarness(
|
|
2562
2980
|
filePath,
|
|
2563
2981
|
componentName,
|
|
@@ -2693,7 +3111,7 @@ Available: ${available}`
|
|
|
2693
3111
|
}
|
|
2694
3112
|
}
|
|
2695
3113
|
);
|
|
2696
|
-
await
|
|
3114
|
+
await shutdownPool4();
|
|
2697
3115
|
if (outcome.crashed) {
|
|
2698
3116
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2699
3117
|
`);
|
|
@@ -2714,12 +3132,12 @@ Available: ${available}`
|
|
|
2714
3132
|
);
|
|
2715
3133
|
return;
|
|
2716
3134
|
}
|
|
2717
|
-
const
|
|
2718
|
-
if (
|
|
3135
|
+
const fmt2 = resolveSingleFormat(opts.format);
|
|
3136
|
+
if (fmt2 === "json") {
|
|
2719
3137
|
const json = formatRenderJson(componentName, props, result);
|
|
2720
3138
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
2721
3139
|
`);
|
|
2722
|
-
} else if (
|
|
3140
|
+
} else if (fmt2 === "file") {
|
|
2723
3141
|
const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2724
3142
|
fs.mkdirSync(dir, { recursive: true });
|
|
2725
3143
|
const outPath = path.resolve(dir, `${componentName}.png`);
|
|
@@ -2741,7 +3159,7 @@ Available: ${available}`
|
|
|
2741
3159
|
);
|
|
2742
3160
|
}
|
|
2743
3161
|
} catch (err) {
|
|
2744
|
-
await
|
|
3162
|
+
await shutdownPool4();
|
|
2745
3163
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2746
3164
|
`);
|
|
2747
3165
|
process.exit(1);
|
|
@@ -2826,7 +3244,7 @@ Available: ${available}`
|
|
|
2826
3244
|
concurrency
|
|
2827
3245
|
});
|
|
2828
3246
|
const result = await matrix.render();
|
|
2829
|
-
await
|
|
3247
|
+
await shutdownPool4();
|
|
2830
3248
|
process.stderr.write(
|
|
2831
3249
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2832
3250
|
`
|
|
@@ -2840,8 +3258,8 @@ Available: ${available}`
|
|
|
2840
3258
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
2841
3259
|
`);
|
|
2842
3260
|
}
|
|
2843
|
-
const
|
|
2844
|
-
if (
|
|
3261
|
+
const fmt2 = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
|
|
3262
|
+
if (fmt2 === "file") {
|
|
2845
3263
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
2846
3264
|
const gen = new SpriteSheetGenerator2();
|
|
2847
3265
|
const sheet = await gen.generate(result);
|
|
@@ -2854,10 +3272,10 @@ Available: ${available}`
|
|
|
2854
3272
|
`\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
|
|
2855
3273
|
`
|
|
2856
3274
|
);
|
|
2857
|
-
} else if (
|
|
3275
|
+
} else if (fmt2 === "json") {
|
|
2858
3276
|
process.stdout.write(`${JSON.stringify(formatMatrixJson(result), null, 2)}
|
|
2859
3277
|
`);
|
|
2860
|
-
} else if (
|
|
3278
|
+
} else if (fmt2 === "png") {
|
|
2861
3279
|
if (opts.sprite !== void 0) {
|
|
2862
3280
|
} else {
|
|
2863
3281
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
|
|
@@ -2865,13 +3283,13 @@ Available: ${available}`
|
|
|
2865
3283
|
const sheet = await gen.generate(result);
|
|
2866
3284
|
process.stdout.write(sheet.png);
|
|
2867
3285
|
}
|
|
2868
|
-
} else if (
|
|
3286
|
+
} else if (fmt2 === "html") {
|
|
2869
3287
|
process.stdout.write(formatMatrixHtml(componentName, result));
|
|
2870
|
-
} else if (
|
|
3288
|
+
} else if (fmt2 === "csv") {
|
|
2871
3289
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2872
3290
|
}
|
|
2873
3291
|
} catch (err) {
|
|
2874
|
-
await
|
|
3292
|
+
await shutdownPool4();
|
|
2875
3293
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2876
3294
|
`);
|
|
2877
3295
|
process.exit(1);
|
|
@@ -2968,13 +3386,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2968
3386
|
workers.push(worker());
|
|
2969
3387
|
}
|
|
2970
3388
|
await Promise.all(workers);
|
|
2971
|
-
await
|
|
3389
|
+
await shutdownPool4();
|
|
2972
3390
|
process.stderr.write("\n");
|
|
2973
3391
|
const summary = formatSummaryText(results, outputDir);
|
|
2974
3392
|
process.stderr.write(`${summary}
|
|
2975
3393
|
`);
|
|
2976
3394
|
} catch (err) {
|
|
2977
|
-
await
|
|
3395
|
+
await shutdownPool4();
|
|
2978
3396
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2979
3397
|
`);
|
|
2980
3398
|
process.exit(1);
|
|
@@ -3016,26 +3434,26 @@ function createRenderCommand() {
|
|
|
3016
3434
|
return renderCmd;
|
|
3017
3435
|
}
|
|
3018
3436
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
3019
|
-
var
|
|
3020
|
-
async function
|
|
3021
|
-
if (
|
|
3022
|
-
|
|
3437
|
+
var _pool5 = null;
|
|
3438
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3439
|
+
if (_pool5 === null) {
|
|
3440
|
+
_pool5 = new render.BrowserPool({
|
|
3023
3441
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3024
3442
|
viewportWidth,
|
|
3025
3443
|
viewportHeight
|
|
3026
3444
|
});
|
|
3027
|
-
await
|
|
3445
|
+
await _pool5.init();
|
|
3028
3446
|
}
|
|
3029
|
-
return
|
|
3447
|
+
return _pool5;
|
|
3030
3448
|
}
|
|
3031
|
-
async function
|
|
3032
|
-
if (
|
|
3033
|
-
await
|
|
3034
|
-
|
|
3449
|
+
async function shutdownPool5() {
|
|
3450
|
+
if (_pool5 !== null) {
|
|
3451
|
+
await _pool5.close();
|
|
3452
|
+
_pool5 = null;
|
|
3035
3453
|
}
|
|
3036
3454
|
}
|
|
3037
|
-
async function
|
|
3038
|
-
const pool = await
|
|
3455
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3456
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3039
3457
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3040
3458
|
const slot = await pool.acquire();
|
|
3041
3459
|
const { page } = slot;
|
|
@@ -3125,7 +3543,7 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
|
|
|
3125
3543
|
pool.release(slot);
|
|
3126
3544
|
}
|
|
3127
3545
|
}
|
|
3128
|
-
function
|
|
3546
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3129
3547
|
const flat = {};
|
|
3130
3548
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3131
3549
|
Object.assign(flat, styles);
|
|
@@ -3168,12 +3586,12 @@ async function runBaseline(options = {}) {
|
|
|
3168
3586
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3169
3587
|
let manifest$1;
|
|
3170
3588
|
if (manifestPath !== void 0) {
|
|
3171
|
-
const { readFileSync:
|
|
3589
|
+
const { readFileSync: readFileSync12 } = await import('fs');
|
|
3172
3590
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3173
3591
|
if (!fs.existsSync(absPath)) {
|
|
3174
3592
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3175
3593
|
}
|
|
3176
|
-
manifest$1 = JSON.parse(
|
|
3594
|
+
manifest$1 = JSON.parse(readFileSync12(absPath, "utf-8"));
|
|
3177
3595
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3178
3596
|
`);
|
|
3179
3597
|
} else {
|
|
@@ -3227,7 +3645,7 @@ async function runBaseline(options = {}) {
|
|
|
3227
3645
|
if (descriptor === void 0) return;
|
|
3228
3646
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3229
3647
|
const outcome = await render.safeRender(
|
|
3230
|
-
() =>
|
|
3648
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3231
3649
|
{
|
|
3232
3650
|
props: {},
|
|
3233
3651
|
sourceLocation: {
|
|
@@ -3269,7 +3687,7 @@ async function runBaseline(options = {}) {
|
|
|
3269
3687
|
JSON.stringify(jsonOutput, null, 2),
|
|
3270
3688
|
"utf-8"
|
|
3271
3689
|
);
|
|
3272
|
-
computedStylesMap.set(name,
|
|
3690
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3273
3691
|
};
|
|
3274
3692
|
const worker = async () => {
|
|
3275
3693
|
while (nextIdx < componentNames.length) {
|
|
@@ -3285,7 +3703,7 @@ async function runBaseline(options = {}) {
|
|
|
3285
3703
|
workers.push(worker());
|
|
3286
3704
|
}
|
|
3287
3705
|
await Promise.all(workers);
|
|
3288
|
-
await
|
|
3706
|
+
await shutdownPool5();
|
|
3289
3707
|
if (isTTY()) {
|
|
3290
3708
|
process.stderr.write("\n");
|
|
3291
3709
|
}
|
|
@@ -3341,31 +3759,31 @@ function loadBaselineCompliance(baselineDir) {
|
|
|
3341
3759
|
const raw = JSON.parse(fs.readFileSync(compliancePath, "utf-8"));
|
|
3342
3760
|
return raw;
|
|
3343
3761
|
}
|
|
3344
|
-
function
|
|
3762
|
+
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3345
3763
|
const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3346
3764
|
if (!fs.existsSync(jsonPath)) return null;
|
|
3347
3765
|
return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
3348
3766
|
}
|
|
3349
|
-
var
|
|
3350
|
-
async function
|
|
3351
|
-
if (
|
|
3352
|
-
|
|
3767
|
+
var _pool6 = null;
|
|
3768
|
+
async function getPool6(viewportWidth, viewportHeight) {
|
|
3769
|
+
if (_pool6 === null) {
|
|
3770
|
+
_pool6 = new render.BrowserPool({
|
|
3353
3771
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3354
3772
|
viewportWidth,
|
|
3355
3773
|
viewportHeight
|
|
3356
3774
|
});
|
|
3357
|
-
await
|
|
3775
|
+
await _pool6.init();
|
|
3358
3776
|
}
|
|
3359
|
-
return
|
|
3777
|
+
return _pool6;
|
|
3360
3778
|
}
|
|
3361
|
-
async function
|
|
3362
|
-
if (
|
|
3363
|
-
await
|
|
3364
|
-
|
|
3779
|
+
async function shutdownPool6() {
|
|
3780
|
+
if (_pool6 !== null) {
|
|
3781
|
+
await _pool6.close();
|
|
3782
|
+
_pool6 = null;
|
|
3365
3783
|
}
|
|
3366
3784
|
}
|
|
3367
|
-
async function
|
|
3368
|
-
const pool = await
|
|
3785
|
+
async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3786
|
+
const pool = await getPool6(viewportWidth, viewportHeight);
|
|
3369
3787
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3370
3788
|
const slot = await pool.acquire();
|
|
3371
3789
|
const { page } = slot;
|
|
@@ -3455,7 +3873,7 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
|
|
|
3455
3873
|
pool.release(slot);
|
|
3456
3874
|
}
|
|
3457
3875
|
}
|
|
3458
|
-
function
|
|
3876
|
+
function extractComputedStyles3(computedStylesRaw) {
|
|
3459
3877
|
const flat = {};
|
|
3460
3878
|
for (const styles of Object.values(computedStylesRaw)) {
|
|
3461
3879
|
Object.assign(flat, styles);
|
|
@@ -3571,7 +3989,7 @@ async function runDiff(options = {}) {
|
|
|
3571
3989
|
if (descriptor === void 0) return;
|
|
3572
3990
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3573
3991
|
const outcome = await render.safeRender(
|
|
3574
|
-
() =>
|
|
3992
|
+
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3575
3993
|
{
|
|
3576
3994
|
props: {},
|
|
3577
3995
|
sourceLocation: {
|
|
@@ -3596,7 +4014,7 @@ async function runDiff(options = {}) {
|
|
|
3596
4014
|
height: result.height,
|
|
3597
4015
|
renderTimeMs: result.renderTimeMs
|
|
3598
4016
|
});
|
|
3599
|
-
computedStylesMap.set(name,
|
|
4017
|
+
computedStylesMap.set(name, extractComputedStyles3(result.computedStyles));
|
|
3600
4018
|
};
|
|
3601
4019
|
if (total > 0) {
|
|
3602
4020
|
const worker = async () => {
|
|
@@ -3614,7 +4032,7 @@ async function runDiff(options = {}) {
|
|
|
3614
4032
|
}
|
|
3615
4033
|
await Promise.all(workers);
|
|
3616
4034
|
}
|
|
3617
|
-
await
|
|
4035
|
+
await shutdownPool6();
|
|
3618
4036
|
if (isTTY() && total > 0) {
|
|
3619
4037
|
process.stderr.write("\n");
|
|
3620
4038
|
}
|
|
@@ -3625,7 +4043,7 @@ async function runDiff(options = {}) {
|
|
|
3625
4043
|
for (const name of componentNames) {
|
|
3626
4044
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3627
4045
|
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3628
|
-
const baselineMeta =
|
|
4046
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3629
4047
|
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3630
4048
|
const failed = renderFailures.has(name);
|
|
3631
4049
|
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
@@ -3645,7 +4063,7 @@ async function runDiff(options = {}) {
|
|
|
3645
4063
|
}
|
|
3646
4064
|
for (const name of removedNames) {
|
|
3647
4065
|
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3648
|
-
const baselineMeta =
|
|
4066
|
+
const baselineMeta = loadBaselineRenderJson2(baselineDir, name);
|
|
3649
4067
|
entries.push({
|
|
3650
4068
|
name,
|
|
3651
4069
|
status: "removed",
|
|
@@ -3807,6 +4225,135 @@ function registerDiffSubCommand(reportCmd) {
|
|
|
3807
4225
|
}
|
|
3808
4226
|
);
|
|
3809
4227
|
}
|
|
4228
|
+
var STATUS_BADGE = {
|
|
4229
|
+
added: "\u2705 added",
|
|
4230
|
+
removed: "\u{1F5D1}\uFE0F removed",
|
|
4231
|
+
unchanged: "\u2014 unchanged",
|
|
4232
|
+
compliance_regressed: "\u274C regressed",
|
|
4233
|
+
compliance_improved: "\u{1F4C8} improved",
|
|
4234
|
+
size_changed: "\u{1F4D0} resized"
|
|
4235
|
+
};
|
|
4236
|
+
function fmt(n) {
|
|
4237
|
+
return `${(n * 100).toFixed(1)}%`;
|
|
4238
|
+
}
|
|
4239
|
+
function fmtDelta(delta) {
|
|
4240
|
+
if (delta === null) return "\u2014";
|
|
4241
|
+
const sign = delta >= 0 ? "+" : "";
|
|
4242
|
+
return `${sign}${(delta * 100).toFixed(1)}%`;
|
|
4243
|
+
}
|
|
4244
|
+
function fmtDimensions(d) {
|
|
4245
|
+
if (d === null) return "\u2014";
|
|
4246
|
+
return `${d.width} \xD7 ${d.height}`;
|
|
4247
|
+
}
|
|
4248
|
+
function complianceDeltaArrow(diff) {
|
|
4249
|
+
const delta = diff.currentAggregateCompliance - diff.baselineAggregateCompliance;
|
|
4250
|
+
if (Math.abs(delta) < 5e-4) return `\u2192 ${fmt(diff.currentAggregateCompliance)} (no change)`;
|
|
4251
|
+
const arrow = delta > 0 ? "\u2191" : "\u2193";
|
|
4252
|
+
return `${arrow} ${fmtDelta(delta)}`;
|
|
4253
|
+
}
|
|
4254
|
+
function formatPrComment(diff) {
|
|
4255
|
+
const { summary, components } = diff;
|
|
4256
|
+
const lines = [];
|
|
4257
|
+
const hasRegressions = diff.hasRegressions;
|
|
4258
|
+
const headerEmoji = hasRegressions ? "\u26A0\uFE0F" : "\u2705";
|
|
4259
|
+
lines.push(`## ${headerEmoji} Scope Report`);
|
|
4260
|
+
lines.push("");
|
|
4261
|
+
lines.push("| Metric | Value |");
|
|
4262
|
+
lines.push("|---|---|");
|
|
4263
|
+
lines.push(`| Baseline compliance | ${fmt(diff.baselineAggregateCompliance)} |`);
|
|
4264
|
+
lines.push(`| Current compliance | ${fmt(diff.currentAggregateCompliance)} |`);
|
|
4265
|
+
lines.push(`| Delta | ${complianceDeltaArrow(diff)} |`);
|
|
4266
|
+
lines.push(
|
|
4267
|
+
`| Components | ${summary.total} total \xB7 ${summary.added} added \xB7 ${summary.removed} removed \xB7 ${summary.complianceRegressed} regressed |`
|
|
4268
|
+
);
|
|
4269
|
+
if (summary.renderFailed > 0) {
|
|
4270
|
+
lines.push(`| Render failures | ${summary.renderFailed} |`);
|
|
4271
|
+
}
|
|
4272
|
+
lines.push("");
|
|
4273
|
+
const changed = components.filter((c) => c.status !== "unchanged");
|
|
4274
|
+
const unchanged = components.filter((c) => c.status === "unchanged");
|
|
4275
|
+
if (changed.length > 0) {
|
|
4276
|
+
lines.push("### Changes");
|
|
4277
|
+
lines.push("");
|
|
4278
|
+
lines.push("| Component | Status | Compliance \u0394 | Dimensions |");
|
|
4279
|
+
lines.push("|---|---|---|---|");
|
|
4280
|
+
for (const c of changed) {
|
|
4281
|
+
const badge = STATUS_BADGE[c.status];
|
|
4282
|
+
const delta = fmtDelta(c.complianceDelta);
|
|
4283
|
+
const dims = fmtDimensions(c.currentDimensions ?? c.baselineDimensions);
|
|
4284
|
+
lines.push(`| \`${c.name}\` | ${badge} | ${delta} | ${dims} |`);
|
|
4285
|
+
}
|
|
4286
|
+
lines.push("");
|
|
4287
|
+
}
|
|
4288
|
+
if (unchanged.length > 0) {
|
|
4289
|
+
lines.push(
|
|
4290
|
+
`<details><summary>${unchanged.length} unchanged component${unchanged.length === 1 ? "" : "s"}</summary>`
|
|
4291
|
+
);
|
|
4292
|
+
lines.push("");
|
|
4293
|
+
lines.push("| Component | Compliance |");
|
|
4294
|
+
lines.push("|---|---|");
|
|
4295
|
+
for (const c of unchanged) {
|
|
4296
|
+
lines.push(
|
|
4297
|
+
`| \`${c.name}\` | ${c.currentCompliance !== null ? fmt(c.currentCompliance) : "\u2014"} |`
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
lines.push("");
|
|
4301
|
+
lines.push("</details>");
|
|
4302
|
+
lines.push("");
|
|
4303
|
+
}
|
|
4304
|
+
lines.push(
|
|
4305
|
+
`> Generated by [Scope](https://github.com/FlatFilers/Scope) \xB7 diffed at ${diff.diffedAt}`
|
|
4306
|
+
);
|
|
4307
|
+
return lines.join("\n");
|
|
4308
|
+
}
|
|
4309
|
+
function loadDiffResult(filePath) {
|
|
4310
|
+
const abs = path.resolve(filePath);
|
|
4311
|
+
if (!fs.existsSync(abs)) {
|
|
4312
|
+
throw new Error(`DiffResult file not found: ${abs}`);
|
|
4313
|
+
}
|
|
4314
|
+
let raw;
|
|
4315
|
+
try {
|
|
4316
|
+
raw = fs.readFileSync(abs, "utf-8");
|
|
4317
|
+
} catch (err) {
|
|
4318
|
+
throw new Error(
|
|
4319
|
+
`Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
let parsed;
|
|
4323
|
+
try {
|
|
4324
|
+
parsed = JSON.parse(raw);
|
|
4325
|
+
} catch {
|
|
4326
|
+
throw new Error(`DiffResult file is not valid JSON: ${abs}`);
|
|
4327
|
+
}
|
|
4328
|
+
if (typeof parsed !== "object" || parsed === null || !("diffedAt" in parsed) || !("components" in parsed) || !("summary" in parsed)) {
|
|
4329
|
+
throw new Error(
|
|
4330
|
+
`DiffResult file does not match expected shape (missing diffedAt/components/summary): ${abs}`
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
return parsed;
|
|
4334
|
+
}
|
|
4335
|
+
function registerPrCommentSubCommand(reportCmd) {
|
|
4336
|
+
reportCmd.command("pr-comment").description(
|
|
4337
|
+
"Format a DiffResult JSON file as a GitHub PR comment (Markdown, written to stdout)"
|
|
4338
|
+
).requiredOption("-i, --input <path>", "Path to DiffResult JSON (from scope report diff --json)").option("-o, --output <path>", "Write comment to file instead of stdout").action(async (opts) => {
|
|
4339
|
+
try {
|
|
4340
|
+
const diff = loadDiffResult(opts.input);
|
|
4341
|
+
const comment = formatPrComment(diff);
|
|
4342
|
+
if (opts.output !== void 0) {
|
|
4343
|
+
fs.writeFileSync(path.resolve(opts.output), comment, "utf-8");
|
|
4344
|
+
process.stderr.write(`PR comment written to ${opts.output}
|
|
4345
|
+
`);
|
|
4346
|
+
} else {
|
|
4347
|
+
process.stdout.write(`${comment}
|
|
4348
|
+
`);
|
|
4349
|
+
}
|
|
4350
|
+
} catch (err) {
|
|
4351
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
4352
|
+
`);
|
|
4353
|
+
process.exit(1);
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
3810
4357
|
|
|
3811
4358
|
// src/tree-formatter.ts
|
|
3812
4359
|
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
@@ -5097,23 +5644,29 @@ function createProgram(options = {}) {
|
|
|
5097
5644
|
program.addCommand(createTokensCommand());
|
|
5098
5645
|
program.addCommand(createInstrumentCommand());
|
|
5099
5646
|
program.addCommand(createInitCommand());
|
|
5647
|
+
program.addCommand(createCiCommand());
|
|
5100
5648
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
5101
5649
|
if (existingReportCmd !== void 0) {
|
|
5102
5650
|
registerBaselineSubCommand(existingReportCmd);
|
|
5103
5651
|
registerDiffSubCommand(existingReportCmd);
|
|
5652
|
+
registerPrCommentSubCommand(existingReportCmd);
|
|
5104
5653
|
}
|
|
5105
5654
|
return program;
|
|
5106
5655
|
}
|
|
5107
5656
|
|
|
5657
|
+
exports.CI_EXIT = CI_EXIT;
|
|
5658
|
+
exports.createCiCommand = createCiCommand;
|
|
5108
5659
|
exports.createInitCommand = createInitCommand;
|
|
5109
5660
|
exports.createInstrumentCommand = createInstrumentCommand;
|
|
5110
5661
|
exports.createManifestCommand = createManifestCommand;
|
|
5111
5662
|
exports.createProgram = createProgram;
|
|
5112
5663
|
exports.createTokensCommand = createTokensCommand;
|
|
5113
5664
|
exports.createTokensExportCommand = createTokensExportCommand;
|
|
5665
|
+
exports.formatCiReport = formatCiReport;
|
|
5114
5666
|
exports.isTTY = isTTY;
|
|
5115
5667
|
exports.matchGlob = matchGlob;
|
|
5116
5668
|
exports.resolveTokenFilePath = resolveTokenFilePath2;
|
|
5669
|
+
exports.runCi = runCi;
|
|
5117
5670
|
exports.runInit = runInit;
|
|
5118
5671
|
//# sourceMappingURL=index.cjs.map
|
|
5119
5672
|
//# sourceMappingURL=index.cjs.map
|