@agent-scope/cli 1.7.0 → 1.9.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 +1095 -266
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +837 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +837 -26
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command6 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -50,11 +50,485 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
50
50
|
writeFileSync(outputPath, json, "utf-8");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// src/init/index.ts
|
|
54
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
55
|
+
import { join as join2 } from "path";
|
|
56
|
+
import * as readline from "readline";
|
|
57
|
+
|
|
58
|
+
// src/init/detect.ts
|
|
59
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
60
|
+
import { join } from "path";
|
|
61
|
+
function hasConfigFile(dir, stem) {
|
|
62
|
+
if (!existsSync(dir)) return false;
|
|
63
|
+
try {
|
|
64
|
+
const entries = readdirSync(dir);
|
|
65
|
+
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function readSafe(path) {
|
|
71
|
+
try {
|
|
72
|
+
return readFileSync(path, "utf-8");
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function detectFramework(rootDir, packageDeps) {
|
|
78
|
+
if (hasConfigFile(rootDir, "next.config")) return "next";
|
|
79
|
+
if (hasConfigFile(rootDir, "vite.config")) return "vite";
|
|
80
|
+
if (hasConfigFile(rootDir, "remix.config")) return "remix";
|
|
81
|
+
if ("react-scripts" in packageDeps) return "cra";
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
function detectPackageManager(rootDir) {
|
|
85
|
+
if (existsSync(join(rootDir, "bun.lock"))) return "bun";
|
|
86
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
87
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
88
|
+
if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
|
|
89
|
+
return "npm";
|
|
90
|
+
}
|
|
91
|
+
function detectTypeScript(rootDir) {
|
|
92
|
+
const candidate = join(rootDir, "tsconfig.json");
|
|
93
|
+
if (existsSync(candidate)) {
|
|
94
|
+
return { typescript: true, tsconfigPath: candidate };
|
|
95
|
+
}
|
|
96
|
+
return { typescript: false, tsconfigPath: null };
|
|
97
|
+
}
|
|
98
|
+
var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
|
|
99
|
+
var COMPONENT_EXTS = [".tsx", ".jsx"];
|
|
100
|
+
function detectComponentPatterns(rootDir, typescript) {
|
|
101
|
+
const patterns = [];
|
|
102
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
103
|
+
const altExt = typescript ? "jsx" : "jsx";
|
|
104
|
+
for (const dir of COMPONENT_DIRS) {
|
|
105
|
+
const absDir = join(rootDir, dir);
|
|
106
|
+
if (!existsSync(absDir)) continue;
|
|
107
|
+
let hasComponents = false;
|
|
108
|
+
try {
|
|
109
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
110
|
+
hasComponents = entries.some(
|
|
111
|
+
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
112
|
+
);
|
|
113
|
+
if (!hasComponents) {
|
|
114
|
+
hasComponents = entries.some(
|
|
115
|
+
(e) => e.isDirectory() && (() => {
|
|
116
|
+
try {
|
|
117
|
+
return readdirSync(join(absDir, e.name)).some(
|
|
118
|
+
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
})()
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (hasComponents) {
|
|
130
|
+
patterns.push(`${dir}/**/*.${ext}`);
|
|
131
|
+
if (altExt !== ext) {
|
|
132
|
+
patterns.push(`${dir}/**/*.${altExt}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const unique = [...new Set(patterns)];
|
|
137
|
+
if (unique.length === 0) {
|
|
138
|
+
return [`**/*.${ext}`];
|
|
139
|
+
}
|
|
140
|
+
return unique;
|
|
141
|
+
}
|
|
142
|
+
var TAILWIND_STEMS = ["tailwind.config"];
|
|
143
|
+
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
144
|
+
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
145
|
+
var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
|
|
146
|
+
function detectTokenSources(rootDir) {
|
|
147
|
+
const sources = [];
|
|
148
|
+
for (const stem of TAILWIND_STEMS) {
|
|
149
|
+
if (hasConfigFile(rootDir, stem)) {
|
|
150
|
+
try {
|
|
151
|
+
const entries = readdirSync(rootDir);
|
|
152
|
+
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
153
|
+
if (match) {
|
|
154
|
+
sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const srcDir = join(rootDir, "src");
|
|
161
|
+
const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
|
|
162
|
+
for (const scanDir of dirsToScan) {
|
|
163
|
+
try {
|
|
164
|
+
const entries = readdirSync(scanDir, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
167
|
+
const filePath = join(scanDir, entry.name);
|
|
168
|
+
const content = readSafe(filePath);
|
|
169
|
+
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
170
|
+
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (existsSync(srcDir)) {
|
|
178
|
+
try {
|
|
179
|
+
const entries = readdirSync(srcDir);
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
182
|
+
sources.push({ kind: "theme-file", path: join(srcDir, entry) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return sources;
|
|
189
|
+
}
|
|
190
|
+
function detectProject(rootDir) {
|
|
191
|
+
const pkgPath = join(rootDir, "package.json");
|
|
192
|
+
let packageDeps = {};
|
|
193
|
+
const pkgContent = readSafe(pkgPath);
|
|
194
|
+
if (pkgContent !== null) {
|
|
195
|
+
try {
|
|
196
|
+
const pkg = JSON.parse(pkgContent);
|
|
197
|
+
packageDeps = {
|
|
198
|
+
...pkg.dependencies,
|
|
199
|
+
...pkg.devDependencies
|
|
200
|
+
};
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const framework = detectFramework(rootDir, packageDeps);
|
|
205
|
+
const { typescript, tsconfigPath } = detectTypeScript(rootDir);
|
|
206
|
+
const packageManager = detectPackageManager(rootDir);
|
|
207
|
+
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
208
|
+
const tokenSources = detectTokenSources(rootDir);
|
|
209
|
+
return {
|
|
210
|
+
framework,
|
|
211
|
+
typescript,
|
|
212
|
+
tsconfigPath,
|
|
213
|
+
componentPatterns,
|
|
214
|
+
tokenSources,
|
|
215
|
+
packageManager
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/init/index.ts
|
|
220
|
+
import { Command } from "commander";
|
|
221
|
+
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
222
|
+
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
223
|
+
return {
|
|
224
|
+
components: {
|
|
225
|
+
include,
|
|
226
|
+
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
227
|
+
wrappers: { providers: [], globalCSS: [] }
|
|
228
|
+
},
|
|
229
|
+
render: {
|
|
230
|
+
viewport: { default: { width: 1280, height: 800 } },
|
|
231
|
+
theme: "light",
|
|
232
|
+
warmBrowser: true
|
|
233
|
+
},
|
|
234
|
+
tokens: {
|
|
235
|
+
file: tokenFile,
|
|
236
|
+
compliance: { threshold: 90 }
|
|
237
|
+
},
|
|
238
|
+
output: {
|
|
239
|
+
dir: outputDir,
|
|
240
|
+
sprites: { format: "png", cellPadding: 8, labelAxes: true },
|
|
241
|
+
json: { pretty: true }
|
|
242
|
+
},
|
|
243
|
+
ci: {
|
|
244
|
+
complianceThreshold: 90,
|
|
245
|
+
failOnA11yViolations: true,
|
|
246
|
+
failOnConsoleErrors: false,
|
|
247
|
+
baselinePath: `${outputDir}baseline/`
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function createRL() {
|
|
252
|
+
return readline.createInterface({
|
|
253
|
+
input: process.stdin,
|
|
254
|
+
output: process.stdout
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async function ask(rl, question) {
|
|
258
|
+
return new Promise((resolve6) => {
|
|
259
|
+
rl.question(question, (answer) => {
|
|
260
|
+
resolve6(answer.trim());
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async function askWithDefault(rl, label, defaultValue) {
|
|
265
|
+
const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
|
|
266
|
+
return answer.length > 0 ? answer : defaultValue;
|
|
267
|
+
}
|
|
268
|
+
function ensureGitignoreEntry(rootDir, entry) {
|
|
269
|
+
const gitignorePath = join2(rootDir, ".gitignore");
|
|
270
|
+
if (existsSync2(gitignorePath)) {
|
|
271
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
272
|
+
const normalised = entry.replace(/\/$/, "");
|
|
273
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
274
|
+
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
278
|
+
appendFileSync(gitignorePath, `${suffix}${entry}
|
|
279
|
+
`);
|
|
280
|
+
} else {
|
|
281
|
+
writeFileSync2(gitignorePath, `${entry}
|
|
282
|
+
`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function scaffoldConfig(rootDir, config) {
|
|
286
|
+
const path = join2(rootDir, "reactscope.config.json");
|
|
287
|
+
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
288
|
+
`);
|
|
289
|
+
return path;
|
|
290
|
+
}
|
|
291
|
+
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
292
|
+
const path = join2(rootDir, tokenFile);
|
|
293
|
+
if (!existsSync2(path)) {
|
|
294
|
+
const stub = {
|
|
295
|
+
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
296
|
+
tokens: {}
|
|
297
|
+
};
|
|
298
|
+
writeFileSync2(path, `${JSON.stringify(stub, null, 2)}
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
return path;
|
|
302
|
+
}
|
|
303
|
+
function scaffoldOutputDir(rootDir, outputDir) {
|
|
304
|
+
const dirPath = join2(rootDir, outputDir);
|
|
305
|
+
mkdirSync(dirPath, { recursive: true });
|
|
306
|
+
const keepPath = join2(dirPath, ".gitkeep");
|
|
307
|
+
if (!existsSync2(keepPath)) {
|
|
308
|
+
writeFileSync2(keepPath, "");
|
|
309
|
+
}
|
|
310
|
+
return dirPath;
|
|
311
|
+
}
|
|
312
|
+
async function runInit(options) {
|
|
313
|
+
const rootDir = options.cwd ?? process.cwd();
|
|
314
|
+
const configPath = join2(rootDir, "reactscope.config.json");
|
|
315
|
+
const created = [];
|
|
316
|
+
if (existsSync2(configPath) && !options.force) {
|
|
317
|
+
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
318
|
+
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
319
|
+
`);
|
|
320
|
+
return { success: false, message: msg, created: [], skipped: true };
|
|
321
|
+
}
|
|
322
|
+
const detected = detectProject(rootDir);
|
|
323
|
+
const defaultTokenFile = "reactscope.tokens.json";
|
|
324
|
+
const defaultOutputDir = ".reactscope/";
|
|
325
|
+
let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
|
|
326
|
+
if (options.yes) {
|
|
327
|
+
process.stdout.write("\n\u{1F50D} Detected project settings:\n");
|
|
328
|
+
process.stdout.write(` Framework : ${detected.framework}
|
|
329
|
+
`);
|
|
330
|
+
process.stdout.write(` TypeScript : ${detected.typescript}
|
|
331
|
+
`);
|
|
332
|
+
process.stdout.write(` Include globs : ${config.components.include.join(", ")}
|
|
333
|
+
`);
|
|
334
|
+
process.stdout.write(` Token file : ${config.tokens.file}
|
|
335
|
+
`);
|
|
336
|
+
process.stdout.write(` Output dir : ${config.output.dir}
|
|
337
|
+
|
|
338
|
+
`);
|
|
339
|
+
} else {
|
|
340
|
+
const rl = createRL();
|
|
341
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
342
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
343
|
+
try {
|
|
344
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
345
|
+
`);
|
|
346
|
+
const includeRaw = await askWithDefault(
|
|
347
|
+
rl,
|
|
348
|
+
"Component include patterns (comma-separated)",
|
|
349
|
+
config.components.include.join(", ")
|
|
350
|
+
);
|
|
351
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
352
|
+
const excludeRaw = await askWithDefault(
|
|
353
|
+
rl,
|
|
354
|
+
"Component exclude patterns (comma-separated)",
|
|
355
|
+
config.components.exclude.join(", ")
|
|
356
|
+
);
|
|
357
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
358
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
359
|
+
config.tokens.file = tokenFile;
|
|
360
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
361
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
362
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
363
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
364
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
365
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
366
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
367
|
+
} finally {
|
|
368
|
+
rl.close();
|
|
369
|
+
}
|
|
370
|
+
process.stdout.write("\n");
|
|
371
|
+
}
|
|
372
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
373
|
+
created.push(cfgPath);
|
|
374
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
375
|
+
created.push(tokPath);
|
|
376
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
377
|
+
created.push(outDirPath);
|
|
378
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
379
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
380
|
+
process.stdout.write(" Created files:\n");
|
|
381
|
+
for (const p of created) {
|
|
382
|
+
process.stdout.write(` ${p}
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
message: "Project initialised successfully.",
|
|
389
|
+
created,
|
|
390
|
+
skipped: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function createInitCommand() {
|
|
394
|
+
return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
395
|
+
try {
|
|
396
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
397
|
+
if (!result.success && !result.skipped) {
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
402
|
+
`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/instrument/renders.ts
|
|
409
|
+
import { resolve as resolve2 } from "path";
|
|
410
|
+
import { BrowserPool } from "@agent-scope/render";
|
|
411
|
+
import { Command as Command3 } from "commander";
|
|
412
|
+
|
|
413
|
+
// src/component-bundler.ts
|
|
414
|
+
import { dirname } from "path";
|
|
415
|
+
import * as esbuild from "esbuild";
|
|
416
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
417
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
418
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
419
|
+
}
|
|
420
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
421
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
422
|
+
const wrapperCode = (
|
|
423
|
+
/* ts */
|
|
424
|
+
`
|
|
425
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
426
|
+
import { createRoot } from "react-dom/client";
|
|
427
|
+
import { createElement } from "react";
|
|
428
|
+
|
|
429
|
+
(function scopeRenderHarness() {
|
|
430
|
+
var Component =
|
|
431
|
+
__scopeMod["default"] ||
|
|
432
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
433
|
+
(Object.values(__scopeMod).find(
|
|
434
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
435
|
+
));
|
|
436
|
+
|
|
437
|
+
if (!Component) {
|
|
438
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
439
|
+
"No renderable component found. Checked: default, " +
|
|
440
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
441
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
442
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
var props = ${propsJson};
|
|
448
|
+
var rootEl = document.getElementById("scope-root");
|
|
449
|
+
if (!rootEl) {
|
|
450
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
451
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
455
|
+
// Use requestAnimationFrame to let React flush the render
|
|
456
|
+
requestAnimationFrame(function() {
|
|
457
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
461
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
462
|
+
}
|
|
463
|
+
})();
|
|
464
|
+
`
|
|
465
|
+
);
|
|
466
|
+
const result = await esbuild.build({
|
|
467
|
+
stdin: {
|
|
468
|
+
contents: wrapperCode,
|
|
469
|
+
// Resolve relative imports (within the component's dir)
|
|
470
|
+
resolveDir: dirname(filePath),
|
|
471
|
+
loader: "tsx",
|
|
472
|
+
sourcefile: "__scope_harness__.tsx"
|
|
473
|
+
},
|
|
474
|
+
bundle: true,
|
|
475
|
+
format: "iife",
|
|
476
|
+
write: false,
|
|
477
|
+
platform: "browser",
|
|
478
|
+
jsx: "automatic",
|
|
479
|
+
jsxImportSource: "react",
|
|
480
|
+
target: "es2020",
|
|
481
|
+
// Bundle everything — no externals
|
|
482
|
+
external: [],
|
|
483
|
+
define: {
|
|
484
|
+
"process.env.NODE_ENV": '"development"',
|
|
485
|
+
global: "globalThis"
|
|
486
|
+
},
|
|
487
|
+
logLevel: "silent",
|
|
488
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
489
|
+
banner: {
|
|
490
|
+
js: "/* @agent-scope/cli component harness */"
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
if (result.errors.length > 0) {
|
|
494
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
495
|
+
throw new Error(`esbuild failed to bundle component:
|
|
496
|
+
${msg}`);
|
|
497
|
+
}
|
|
498
|
+
const outputFile = result.outputFiles?.[0];
|
|
499
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
500
|
+
throw new Error("esbuild produced no output");
|
|
501
|
+
}
|
|
502
|
+
return outputFile.text;
|
|
503
|
+
}
|
|
504
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
505
|
+
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
506
|
+
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
507
|
+
</style>` : "";
|
|
508
|
+
return `<!DOCTYPE html>
|
|
509
|
+
<html lang="en">
|
|
510
|
+
<head>
|
|
511
|
+
<meta charset="UTF-8" />
|
|
512
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
513
|
+
<style>
|
|
514
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
515
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
516
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
517
|
+
</style>
|
|
518
|
+
${projectStyleBlock}
|
|
519
|
+
</head>
|
|
520
|
+
<body>
|
|
521
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
522
|
+
<script>${bundledScript}</script>
|
|
523
|
+
</body>
|
|
524
|
+
</html>`;
|
|
525
|
+
}
|
|
526
|
+
|
|
53
527
|
// src/manifest-commands.ts
|
|
54
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync as
|
|
528
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
55
529
|
import { resolve } from "path";
|
|
56
530
|
import { generateManifest } from "@agent-scope/manifest";
|
|
57
|
-
import { Command } from "commander";
|
|
531
|
+
import { Command as Command2 } from "commander";
|
|
58
532
|
|
|
59
533
|
// src/manifest-formatter.ts
|
|
60
534
|
function isTTY() {
|
|
@@ -155,11 +629,11 @@ function matchGlob(pattern, value) {
|
|
|
155
629
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
156
630
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
157
631
|
const absPath = resolve(process.cwd(), manifestPath);
|
|
158
|
-
if (!
|
|
632
|
+
if (!existsSync3(absPath)) {
|
|
159
633
|
throw new Error(`Manifest not found at ${absPath}.
|
|
160
634
|
Run \`scope manifest generate\` first.`);
|
|
161
635
|
}
|
|
162
|
-
const raw =
|
|
636
|
+
const raw = readFileSync3(absPath, "utf-8");
|
|
163
637
|
return JSON.parse(raw);
|
|
164
638
|
}
|
|
165
639
|
function resolveFormat(formatFlag) {
|
|
@@ -278,178 +752,49 @@ function registerQuery(manifestCmd) {
|
|
|
278
752
|
);
|
|
279
753
|
}
|
|
280
754
|
function registerGenerate(manifestCmd) {
|
|
281
|
-
manifestCmd.command("generate").description(
|
|
282
|
-
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
283
|
-
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
284
|
-
try {
|
|
285
|
-
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
286
|
-
const outputPath = resolve(process.cwd(), opts.output);
|
|
287
|
-
const include = opts.include?.split(",").map((s) => s.trim());
|
|
288
|
-
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
289
|
-
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
290
|
-
`);
|
|
291
|
-
const manifest = await generateManifest({
|
|
292
|
-
rootDir,
|
|
293
|
-
...include !== void 0 && { include },
|
|
294
|
-
...exclude !== void 0 && { exclude }
|
|
295
|
-
});
|
|
296
|
-
const componentCount = Object.keys(manifest.components).length;
|
|
297
|
-
process.stderr.write(`Found ${componentCount} components.
|
|
298
|
-
`);
|
|
299
|
-
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
300
|
-
if (!
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
process.stderr.write(`Manifest written to ${outputPath}
|
|
305
|
-
`);
|
|
306
|
-
process.stdout.write(`${outputPath}
|
|
307
|
-
`);
|
|
308
|
-
} catch (err) {
|
|
309
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
310
|
-
`);
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
function createManifestCommand() {
|
|
316
|
-
const manifestCmd = new Command("manifest").description(
|
|
317
|
-
"Query and explore the component manifest"
|
|
318
|
-
);
|
|
319
|
-
registerList(manifestCmd);
|
|
320
|
-
registerGet(manifestCmd);
|
|
321
|
-
registerQuery(manifestCmd);
|
|
322
|
-
registerGenerate(manifestCmd);
|
|
323
|
-
return manifestCmd;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/render-commands.ts
|
|
327
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
328
|
-
import { resolve as resolve3 } from "path";
|
|
329
|
-
import {
|
|
330
|
-
ALL_CONTEXT_IDS,
|
|
331
|
-
ALL_STRESS_IDS,
|
|
332
|
-
BrowserPool,
|
|
333
|
-
contextAxis,
|
|
334
|
-
RenderMatrix,
|
|
335
|
-
SatoriRenderer,
|
|
336
|
-
safeRender,
|
|
337
|
-
stressAxis
|
|
338
|
-
} from "@agent-scope/render";
|
|
339
|
-
import { Command as Command2 } from "commander";
|
|
340
|
-
|
|
341
|
-
// src/component-bundler.ts
|
|
342
|
-
import { dirname } from "path";
|
|
343
|
-
import * as esbuild from "esbuild";
|
|
344
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
345
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
346
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss);
|
|
347
|
-
}
|
|
348
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
349
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
350
|
-
const wrapperCode = (
|
|
351
|
-
/* ts */
|
|
352
|
-
`
|
|
353
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
354
|
-
import { createRoot } from "react-dom/client";
|
|
355
|
-
import { createElement } from "react";
|
|
356
|
-
|
|
357
|
-
(function scopeRenderHarness() {
|
|
358
|
-
var Component =
|
|
359
|
-
__scopeMod["default"] ||
|
|
360
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
361
|
-
(Object.values(__scopeMod).find(
|
|
362
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
363
|
-
));
|
|
364
|
-
|
|
365
|
-
if (!Component) {
|
|
366
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
367
|
-
"No renderable component found. Checked: default, " +
|
|
368
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
369
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
370
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
var props = ${propsJson};
|
|
376
|
-
var rootEl = document.getElementById("scope-root");
|
|
377
|
-
if (!rootEl) {
|
|
378
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
379
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
383
|
-
// Use requestAnimationFrame to let React flush the render
|
|
384
|
-
requestAnimationFrame(function() {
|
|
385
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
386
|
-
});
|
|
387
|
-
} catch (err) {
|
|
388
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
389
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
390
|
-
}
|
|
391
|
-
})();
|
|
392
|
-
`
|
|
393
|
-
);
|
|
394
|
-
const result = await esbuild.build({
|
|
395
|
-
stdin: {
|
|
396
|
-
contents: wrapperCode,
|
|
397
|
-
// Resolve relative imports (within the component's dir)
|
|
398
|
-
resolveDir: dirname(filePath),
|
|
399
|
-
loader: "tsx",
|
|
400
|
-
sourcefile: "__scope_harness__.tsx"
|
|
401
|
-
},
|
|
402
|
-
bundle: true,
|
|
403
|
-
format: "iife",
|
|
404
|
-
write: false,
|
|
405
|
-
platform: "browser",
|
|
406
|
-
jsx: "automatic",
|
|
407
|
-
jsxImportSource: "react",
|
|
408
|
-
target: "es2020",
|
|
409
|
-
// Bundle everything — no externals
|
|
410
|
-
external: [],
|
|
411
|
-
define: {
|
|
412
|
-
"process.env.NODE_ENV": '"development"',
|
|
413
|
-
global: "globalThis"
|
|
414
|
-
},
|
|
415
|
-
logLevel: "silent",
|
|
416
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
417
|
-
banner: {
|
|
418
|
-
js: "/* @agent-scope/cli component harness */"
|
|
755
|
+
manifestCmd.command("generate").description(
|
|
756
|
+
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
757
|
+
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
758
|
+
try {
|
|
759
|
+
const rootDir = resolve(process.cwd(), opts.root ?? ".");
|
|
760
|
+
const outputPath = resolve(process.cwd(), opts.output);
|
|
761
|
+
const include = opts.include?.split(",").map((s) => s.trim());
|
|
762
|
+
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
763
|
+
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
764
|
+
`);
|
|
765
|
+
const manifest = await generateManifest({
|
|
766
|
+
rootDir,
|
|
767
|
+
...include !== void 0 && { include },
|
|
768
|
+
...exclude !== void 0 && { exclude }
|
|
769
|
+
});
|
|
770
|
+
const componentCount = Object.keys(manifest.components).length;
|
|
771
|
+
process.stderr.write(`Found ${componentCount} components.
|
|
772
|
+
`);
|
|
773
|
+
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
774
|
+
if (!existsSync3(outputDir)) {
|
|
775
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
776
|
+
}
|
|
777
|
+
writeFileSync3(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
778
|
+
process.stderr.write(`Manifest written to ${outputPath}
|
|
779
|
+
`);
|
|
780
|
+
process.stdout.write(`${outputPath}
|
|
781
|
+
`);
|
|
782
|
+
} catch (err) {
|
|
783
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
784
|
+
`);
|
|
785
|
+
process.exit(1);
|
|
419
786
|
}
|
|
420
787
|
});
|
|
421
|
-
if (result.errors.length > 0) {
|
|
422
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
423
|
-
throw new Error(`esbuild failed to bundle component:
|
|
424
|
-
${msg}`);
|
|
425
|
-
}
|
|
426
|
-
const outputFile = result.outputFiles?.[0];
|
|
427
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
428
|
-
throw new Error("esbuild produced no output");
|
|
429
|
-
}
|
|
430
|
-
return outputFile.text;
|
|
431
788
|
}
|
|
432
|
-
function
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
<style>
|
|
442
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
443
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
444
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
445
|
-
</style>
|
|
446
|
-
${projectStyleBlock}
|
|
447
|
-
</head>
|
|
448
|
-
<body>
|
|
449
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
450
|
-
<script>${bundledScript}</script>
|
|
451
|
-
</body>
|
|
452
|
-
</html>`;
|
|
789
|
+
function createManifestCommand() {
|
|
790
|
+
const manifestCmd = new Command2("manifest").description(
|
|
791
|
+
"Query and explore the component manifest"
|
|
792
|
+
);
|
|
793
|
+
registerList(manifestCmd);
|
|
794
|
+
registerGet(manifestCmd);
|
|
795
|
+
registerQuery(manifestCmd);
|
|
796
|
+
registerGenerate(manifestCmd);
|
|
797
|
+
return manifestCmd;
|
|
453
798
|
}
|
|
454
799
|
|
|
455
800
|
// src/render-formatter.ts
|
|
@@ -612,10 +957,492 @@ function csvEscape(value) {
|
|
|
612
957
|
return value;
|
|
613
958
|
}
|
|
614
959
|
|
|
960
|
+
// src/instrument/renders.ts
|
|
961
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
962
|
+
function determineTrigger(event) {
|
|
963
|
+
if (event.forceUpdate) return "force_update";
|
|
964
|
+
if (event.stateChanged) return "state_change";
|
|
965
|
+
if (event.propsChanged) return "props_change";
|
|
966
|
+
if (event.contextChanged) return "context_change";
|
|
967
|
+
if (event.hookDepsChanged) return "hook_dependency";
|
|
968
|
+
return "parent_rerender";
|
|
969
|
+
}
|
|
970
|
+
function isWastedRender(event) {
|
|
971
|
+
return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
|
|
972
|
+
}
|
|
973
|
+
function buildCausalityChains(rawEvents) {
|
|
974
|
+
const result = [];
|
|
975
|
+
const componentLastRender = /* @__PURE__ */ new Map();
|
|
976
|
+
for (const raw of rawEvents) {
|
|
977
|
+
const trigger = determineTrigger(raw);
|
|
978
|
+
const wasted = isWastedRender(raw);
|
|
979
|
+
const chain = [];
|
|
980
|
+
let current = raw;
|
|
981
|
+
const visited = /* @__PURE__ */ new Set();
|
|
982
|
+
while (true) {
|
|
983
|
+
if (visited.has(current.component)) break;
|
|
984
|
+
visited.add(current.component);
|
|
985
|
+
const currentTrigger = determineTrigger(current);
|
|
986
|
+
chain.unshift({
|
|
987
|
+
component: current.component,
|
|
988
|
+
trigger: currentTrigger,
|
|
989
|
+
propsChanged: current.propsChanged,
|
|
990
|
+
stateChanged: current.stateChanged,
|
|
991
|
+
contextChanged: current.contextChanged
|
|
992
|
+
});
|
|
993
|
+
if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
const parentEvent = componentLastRender.get(current.parentComponent);
|
|
997
|
+
if (parentEvent === void 0) break;
|
|
998
|
+
current = parentEvent;
|
|
999
|
+
}
|
|
1000
|
+
const rootCause = chain[0];
|
|
1001
|
+
const cascadeRenders = rawEvents.filter((e) => {
|
|
1002
|
+
if (rootCause === void 0) return false;
|
|
1003
|
+
return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
|
|
1004
|
+
});
|
|
1005
|
+
result.push({
|
|
1006
|
+
component: raw.component,
|
|
1007
|
+
renderIndex: raw.renderIndex,
|
|
1008
|
+
trigger,
|
|
1009
|
+
propsChanged: raw.propsChanged,
|
|
1010
|
+
stateChanged: raw.stateChanged,
|
|
1011
|
+
contextChanged: raw.contextChanged,
|
|
1012
|
+
memoized: raw.memoized,
|
|
1013
|
+
wasted,
|
|
1014
|
+
chain,
|
|
1015
|
+
cascade: {
|
|
1016
|
+
totalRendersTriggered: cascadeRenders.length,
|
|
1017
|
+
uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
|
|
1018
|
+
unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
componentLastRender.set(raw.component, raw);
|
|
1022
|
+
}
|
|
1023
|
+
return result;
|
|
1024
|
+
}
|
|
1025
|
+
function applyHeuristicFlags(renders) {
|
|
1026
|
+
const flags = [];
|
|
1027
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
1028
|
+
for (const r of renders) {
|
|
1029
|
+
if (!byComponent.has(r.component)) byComponent.set(r.component, []);
|
|
1030
|
+
byComponent.get(r.component).push(r);
|
|
1031
|
+
}
|
|
1032
|
+
for (const [component, events] of byComponent) {
|
|
1033
|
+
const wastedCount = events.filter((e) => e.wasted).length;
|
|
1034
|
+
const totalCount = events.length;
|
|
1035
|
+
if (wastedCount > 0) {
|
|
1036
|
+
flags.push({
|
|
1037
|
+
id: "WASTED_RENDER",
|
|
1038
|
+
severity: "warning",
|
|
1039
|
+
component,
|
|
1040
|
+
detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
|
|
1041
|
+
data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
for (const event of events) {
|
|
1045
|
+
if (event.cascade.totalRendersTriggered > 50) {
|
|
1046
|
+
flags.push({
|
|
1047
|
+
id: "RENDER_CASCADE",
|
|
1048
|
+
severity: "warning",
|
|
1049
|
+
component,
|
|
1050
|
+
detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
|
|
1051
|
+
data: {
|
|
1052
|
+
totalRendersTriggered: event.cascade.totalRendersTriggered,
|
|
1053
|
+
uniqueComponents: event.cascade.uniqueComponents,
|
|
1054
|
+
unchangedPropRenders: event.cascade.unchangedPropRenders
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return flags;
|
|
1062
|
+
}
|
|
1063
|
+
function buildInstrumentationScript() {
|
|
1064
|
+
return (
|
|
1065
|
+
/* js */
|
|
1066
|
+
`
|
|
1067
|
+
(function installScopeRenderInstrumentation() {
|
|
1068
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1069
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1070
|
+
|
|
1071
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1072
|
+
if (!hook) return;
|
|
1073
|
+
|
|
1074
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
1075
|
+
var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
|
|
1076
|
+
|
|
1077
|
+
function extractName(fiber) {
|
|
1078
|
+
if (!fiber) return 'Unknown';
|
|
1079
|
+
var type = fiber.type;
|
|
1080
|
+
if (!type) return 'Unknown';
|
|
1081
|
+
if (typeof type === 'string') return type; // host element
|
|
1082
|
+
if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
|
|
1083
|
+
if (type.displayName) return type.displayName;
|
|
1084
|
+
if (type.render && typeof type.render === 'function') {
|
|
1085
|
+
return type.render.displayName || type.render.name || 'Anonymous';
|
|
1086
|
+
}
|
|
1087
|
+
return 'Anonymous';
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function isMemoized(fiber) {
|
|
1091
|
+
// MemoComponent = 14, SimpleMemoComponent = 15
|
|
1092
|
+
return fiber.tag === 14 || fiber.tag === 15;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function isComponent(fiber) {
|
|
1096
|
+
// FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
|
|
1097
|
+
return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function shallowEqual(a, b) {
|
|
1101
|
+
if (a === b) return true;
|
|
1102
|
+
if (!a || !b) return a === b;
|
|
1103
|
+
var keysA = Object.keys(a);
|
|
1104
|
+
var keysB = Object.keys(b);
|
|
1105
|
+
if (keysA.length !== keysB.length) return false;
|
|
1106
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
1107
|
+
var k = keysA[i];
|
|
1108
|
+
if (k === 'children') continue; // ignore children prop
|
|
1109
|
+
if (a[k] !== b[k]) return false;
|
|
1110
|
+
}
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function getParentComponentName(fiber) {
|
|
1115
|
+
var parent = fiber.return;
|
|
1116
|
+
while (parent) {
|
|
1117
|
+
if (isComponent(parent)) {
|
|
1118
|
+
var name = extractName(parent);
|
|
1119
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
|
|
1120
|
+
}
|
|
1121
|
+
parent = parent.return;
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function walkCommit(fiber) {
|
|
1127
|
+
if (!fiber) return;
|
|
1128
|
+
|
|
1129
|
+
if (isComponent(fiber)) {
|
|
1130
|
+
var name = extractName(fiber);
|
|
1131
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
|
|
1132
|
+
var memoized = isMemoized(fiber);
|
|
1133
|
+
var currentProps = fiber.memoizedProps || {};
|
|
1134
|
+
var prev = renderedComponents.get(name);
|
|
1135
|
+
|
|
1136
|
+
var propsChanged = true;
|
|
1137
|
+
var stateChanged = false;
|
|
1138
|
+
var contextChanged = false;
|
|
1139
|
+
var hookDepsChanged = false;
|
|
1140
|
+
var forceUpdate = false;
|
|
1141
|
+
|
|
1142
|
+
if (prev) {
|
|
1143
|
+
propsChanged = !shallowEqual(prev.lastProps, currentProps);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// State: check memoizedState chain
|
|
1147
|
+
var memoizedState = fiber.memoizedState;
|
|
1148
|
+
if (prev && prev.lastStateSerialized !== undefined) {
|
|
1149
|
+
try {
|
|
1150
|
+
var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1151
|
+
stateChanged = stateSig !== prev.lastStateSerialized;
|
|
1152
|
+
} catch (_) {
|
|
1153
|
+
stateChanged = false;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Context: use _debugHookTypes or check dependencies
|
|
1158
|
+
var deps = fiber.dependencies;
|
|
1159
|
+
if (deps && deps.firstContext) {
|
|
1160
|
+
contextChanged = true; // conservative: context dep present = may have changed
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
var stateSig;
|
|
1164
|
+
try {
|
|
1165
|
+
stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1166
|
+
} catch (_) {
|
|
1167
|
+
stateSig = null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
renderedComponents.set(name, {
|
|
1171
|
+
lastProps: currentProps,
|
|
1172
|
+
lastStateSerialized: stateSig,
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
var parentName = getParentComponentName(fiber);
|
|
1176
|
+
|
|
1177
|
+
window.__SCOPE_RENDER_EVENTS__.push({
|
|
1178
|
+
component: name,
|
|
1179
|
+
renderIndex: window.__SCOPE_RENDER_INDEX__++,
|
|
1180
|
+
propsChanged: prev ? propsChanged : false,
|
|
1181
|
+
stateChanged: stateChanged,
|
|
1182
|
+
contextChanged: contextChanged,
|
|
1183
|
+
memoized: memoized,
|
|
1184
|
+
parentComponent: parentName,
|
|
1185
|
+
hookDepsChanged: hookDepsChanged,
|
|
1186
|
+
forceUpdate: forceUpdate,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
walkCommit(fiber.child);
|
|
1192
|
+
walkCommit(fiber.sibling);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1196
|
+
if (typeof originalOnCommit === 'function') {
|
|
1197
|
+
originalOnCommit.call(hook, rendererID, root, priorityLevel);
|
|
1198
|
+
}
|
|
1199
|
+
var wipRoot = root && root.current && root.current.alternate;
|
|
1200
|
+
if (wipRoot) walkCommit(wipRoot);
|
|
1201
|
+
};
|
|
1202
|
+
})();
|
|
1203
|
+
`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
async function replayInteraction(page, steps) {
|
|
1207
|
+
for (const step of steps) {
|
|
1208
|
+
switch (step.action) {
|
|
1209
|
+
case "click":
|
|
1210
|
+
if (step.target !== void 0) {
|
|
1211
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
case "type":
|
|
1216
|
+
if (step.target !== void 0 && step.text !== void 0) {
|
|
1217
|
+
await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
|
|
1218
|
+
await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
break;
|
|
1223
|
+
case "hover":
|
|
1224
|
+
if (step.target !== void 0) {
|
|
1225
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
break;
|
|
1229
|
+
case "blur":
|
|
1230
|
+
if (step.target !== void 0) {
|
|
1231
|
+
await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
break;
|
|
1235
|
+
case "focus":
|
|
1236
|
+
if (step.target !== void 0) {
|
|
1237
|
+
await page.focus(step.target, { timeout: 5e3 }).catch(() => {
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
break;
|
|
1241
|
+
case "scroll":
|
|
1242
|
+
if (step.target !== void 0) {
|
|
1243
|
+
await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
break;
|
|
1247
|
+
case "wait": {
|
|
1248
|
+
const timeout = step.timeout ?? 1e3;
|
|
1249
|
+
if (step.condition === "idle") {
|
|
1250
|
+
await page.waitForLoadState("networkidle", { timeout }).catch(() => {
|
|
1251
|
+
});
|
|
1252
|
+
} else {
|
|
1253
|
+
await page.waitForTimeout(timeout);
|
|
1254
|
+
}
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
default:
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
var _pool = null;
|
|
1263
|
+
async function getPool() {
|
|
1264
|
+
if (_pool === null) {
|
|
1265
|
+
_pool = new BrowserPool({
|
|
1266
|
+
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1267
|
+
viewportWidth: 1280,
|
|
1268
|
+
viewportHeight: 800
|
|
1269
|
+
});
|
|
1270
|
+
await _pool.init();
|
|
1271
|
+
}
|
|
1272
|
+
return _pool;
|
|
1273
|
+
}
|
|
1274
|
+
async function shutdownPool() {
|
|
1275
|
+
if (_pool !== null) {
|
|
1276
|
+
await _pool.close();
|
|
1277
|
+
_pool = null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function analyzeRenders(options) {
|
|
1281
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
|
|
1282
|
+
const manifest = loadManifest(manifestPath);
|
|
1283
|
+
const descriptor = manifest.components[options.componentName];
|
|
1284
|
+
if (descriptor === void 0) {
|
|
1285
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1286
|
+
throw new Error(
|
|
1287
|
+
`Component "${options.componentName}" not found in manifest.
|
|
1288
|
+
Available: ${available}`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
const rootDir = process.cwd();
|
|
1292
|
+
const filePath = resolve2(rootDir, descriptor.filePath);
|
|
1293
|
+
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1294
|
+
const pool = await getPool();
|
|
1295
|
+
const slot = await pool.acquire();
|
|
1296
|
+
const { page } = slot;
|
|
1297
|
+
const startMs = performance.now();
|
|
1298
|
+
try {
|
|
1299
|
+
await page.addInitScript(buildInstrumentationScript());
|
|
1300
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1301
|
+
await page.waitForFunction(
|
|
1302
|
+
() => window.__SCOPE_RENDER_COMPLETE__ === true,
|
|
1303
|
+
{ timeout: 15e3 }
|
|
1304
|
+
);
|
|
1305
|
+
await page.waitForTimeout(100);
|
|
1306
|
+
await page.evaluate(() => {
|
|
1307
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1308
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1309
|
+
});
|
|
1310
|
+
await replayInteraction(page, options.interaction);
|
|
1311
|
+
await page.waitForTimeout(200);
|
|
1312
|
+
const interactionDurationMs = performance.now() - startMs;
|
|
1313
|
+
const rawEvents = await page.evaluate(() => {
|
|
1314
|
+
return window.__SCOPE_RENDER_EVENTS__ ?? [];
|
|
1315
|
+
});
|
|
1316
|
+
const renders = buildCausalityChains(rawEvents);
|
|
1317
|
+
const flags = applyHeuristicFlags(renders);
|
|
1318
|
+
const uniqueComponents = new Set(renders.map((r) => r.component)).size;
|
|
1319
|
+
const wastedRenders = renders.filter((r) => r.wasted).length;
|
|
1320
|
+
return {
|
|
1321
|
+
component: options.componentName,
|
|
1322
|
+
interaction: options.interaction,
|
|
1323
|
+
summary: {
|
|
1324
|
+
totalRenders: renders.length,
|
|
1325
|
+
uniqueComponents,
|
|
1326
|
+
wastedRenders,
|
|
1327
|
+
interactionDurationMs: Math.round(interactionDurationMs)
|
|
1328
|
+
},
|
|
1329
|
+
renders,
|
|
1330
|
+
flags
|
|
1331
|
+
};
|
|
1332
|
+
} finally {
|
|
1333
|
+
pool.release(slot);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function formatRendersTable(result) {
|
|
1337
|
+
const lines = [];
|
|
1338
|
+
lines.push(`
|
|
1339
|
+
\u{1F50D} Re-render Analysis: ${result.component}`);
|
|
1340
|
+
lines.push(`${"\u2500".repeat(60)}`);
|
|
1341
|
+
lines.push(`Total renders: ${result.summary.totalRenders}`);
|
|
1342
|
+
lines.push(`Unique components: ${result.summary.uniqueComponents}`);
|
|
1343
|
+
lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
|
|
1344
|
+
lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
|
|
1345
|
+
lines.push("");
|
|
1346
|
+
if (result.renders.length === 0) {
|
|
1347
|
+
lines.push("No re-renders captured during interaction.");
|
|
1348
|
+
} else {
|
|
1349
|
+
lines.push("Re-renders:");
|
|
1350
|
+
lines.push(
|
|
1351
|
+
`${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
|
|
1352
|
+
);
|
|
1353
|
+
lines.push("\u2500".repeat(80));
|
|
1354
|
+
for (const r of result.renders) {
|
|
1355
|
+
const wasted = r.wasted ? "\u26A0 yes" : "no";
|
|
1356
|
+
const idx = String(r.renderIndex).padEnd(4);
|
|
1357
|
+
const comp = r.component.slice(0, 29).padEnd(30);
|
|
1358
|
+
const trig = r.trigger.padEnd(18);
|
|
1359
|
+
const w = wasted.padEnd(7);
|
|
1360
|
+
const depth = r.chain.length;
|
|
1361
|
+
lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (result.flags.length > 0) {
|
|
1365
|
+
lines.push("");
|
|
1366
|
+
lines.push("Flags:");
|
|
1367
|
+
for (const flag of result.flags) {
|
|
1368
|
+
const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1369
|
+
lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return lines.join("\n");
|
|
1373
|
+
}
|
|
1374
|
+
function createInstrumentRendersCommand() {
|
|
1375
|
+
return new Command3("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
1376
|
+
"--interaction <json>",
|
|
1377
|
+
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1378
|
+
"[]"
|
|
1379
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
|
|
1380
|
+
async (componentName, opts) => {
|
|
1381
|
+
let interaction = [];
|
|
1382
|
+
try {
|
|
1383
|
+
interaction = JSON.parse(opts.interaction);
|
|
1384
|
+
if (!Array.isArray(interaction)) {
|
|
1385
|
+
throw new Error("Interaction must be a JSON array");
|
|
1386
|
+
}
|
|
1387
|
+
} catch {
|
|
1388
|
+
process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
|
|
1389
|
+
`);
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
process.stderr.write(
|
|
1394
|
+
`Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
|
|
1395
|
+
`
|
|
1396
|
+
);
|
|
1397
|
+
const result = await analyzeRenders({
|
|
1398
|
+
componentName,
|
|
1399
|
+
interaction,
|
|
1400
|
+
manifestPath: opts.manifest
|
|
1401
|
+
});
|
|
1402
|
+
await shutdownPool();
|
|
1403
|
+
if (opts.json || !isTTY()) {
|
|
1404
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1405
|
+
`);
|
|
1406
|
+
} else {
|
|
1407
|
+
process.stdout.write(`${formatRendersTable(result)}
|
|
1408
|
+
`);
|
|
1409
|
+
}
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
await shutdownPool();
|
|
1412
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1413
|
+
`);
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
function createInstrumentCommand() {
|
|
1420
|
+
const instrumentCmd = new Command3("instrument").description(
|
|
1421
|
+
"Structured instrumentation commands for React component analysis"
|
|
1422
|
+
);
|
|
1423
|
+
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1424
|
+
return instrumentCmd;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/render-commands.ts
|
|
1428
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1429
|
+
import { resolve as resolve4 } from "path";
|
|
1430
|
+
import {
|
|
1431
|
+
ALL_CONTEXT_IDS,
|
|
1432
|
+
ALL_STRESS_IDS,
|
|
1433
|
+
BrowserPool as BrowserPool2,
|
|
1434
|
+
contextAxis,
|
|
1435
|
+
RenderMatrix,
|
|
1436
|
+
SatoriRenderer,
|
|
1437
|
+
safeRender,
|
|
1438
|
+
stressAxis
|
|
1439
|
+
} from "@agent-scope/render";
|
|
1440
|
+
import { Command as Command4 } from "commander";
|
|
1441
|
+
|
|
615
1442
|
// src/tailwind-css.ts
|
|
616
|
-
import { existsSync as
|
|
1443
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
617
1444
|
import { createRequire } from "module";
|
|
618
|
-
import { resolve as
|
|
1445
|
+
import { resolve as resolve3 } from "path";
|
|
619
1446
|
var CONFIG_FILENAMES = [
|
|
620
1447
|
".reactscope/config.json",
|
|
621
1448
|
".reactscope/config.js",
|
|
@@ -632,47 +1459,47 @@ var STYLE_ENTRY_CANDIDATES = [
|
|
|
632
1459
|
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
633
1460
|
var compilerCache = null;
|
|
634
1461
|
function getCachedBuild(cwd) {
|
|
635
|
-
if (compilerCache !== null &&
|
|
1462
|
+
if (compilerCache !== null && resolve3(compilerCache.cwd) === resolve3(cwd)) {
|
|
636
1463
|
return compilerCache.build;
|
|
637
1464
|
}
|
|
638
1465
|
return null;
|
|
639
1466
|
}
|
|
640
1467
|
function findStylesEntry(cwd) {
|
|
641
1468
|
for (const name of CONFIG_FILENAMES) {
|
|
642
|
-
const p =
|
|
643
|
-
if (!
|
|
1469
|
+
const p = resolve3(cwd, name);
|
|
1470
|
+
if (!existsSync4(p)) continue;
|
|
644
1471
|
try {
|
|
645
1472
|
if (name.endsWith(".json")) {
|
|
646
|
-
const raw =
|
|
1473
|
+
const raw = readFileSync4(p, "utf-8");
|
|
647
1474
|
const data = JSON.parse(raw);
|
|
648
1475
|
const scope = data.scope;
|
|
649
1476
|
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
650
1477
|
if (typeof entry === "string") {
|
|
651
|
-
const full =
|
|
652
|
-
if (
|
|
1478
|
+
const full = resolve3(cwd, entry);
|
|
1479
|
+
if (existsSync4(full)) return full;
|
|
653
1480
|
}
|
|
654
1481
|
}
|
|
655
1482
|
} catch {
|
|
656
1483
|
}
|
|
657
1484
|
}
|
|
658
|
-
const pkgPath =
|
|
659
|
-
if (
|
|
1485
|
+
const pkgPath = resolve3(cwd, "package.json");
|
|
1486
|
+
if (existsSync4(pkgPath)) {
|
|
660
1487
|
try {
|
|
661
|
-
const raw =
|
|
1488
|
+
const raw = readFileSync4(pkgPath, "utf-8");
|
|
662
1489
|
const pkg = JSON.parse(raw);
|
|
663
1490
|
const entry = pkg.scope?.stylesEntry;
|
|
664
1491
|
if (typeof entry === "string") {
|
|
665
|
-
const full =
|
|
666
|
-
if (
|
|
1492
|
+
const full = resolve3(cwd, entry);
|
|
1493
|
+
if (existsSync4(full)) return full;
|
|
667
1494
|
}
|
|
668
1495
|
} catch {
|
|
669
1496
|
}
|
|
670
1497
|
}
|
|
671
1498
|
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
672
|
-
const full =
|
|
673
|
-
if (
|
|
1499
|
+
const full = resolve3(cwd, candidate);
|
|
1500
|
+
if (existsSync4(full)) {
|
|
674
1501
|
try {
|
|
675
|
-
const content =
|
|
1502
|
+
const content = readFileSync4(full, "utf-8");
|
|
676
1503
|
if (TAILWIND_IMPORT.test(content)) return full;
|
|
677
1504
|
} catch {
|
|
678
1505
|
}
|
|
@@ -687,7 +1514,7 @@ async function getTailwindCompiler(cwd) {
|
|
|
687
1514
|
if (entryPath === null) return null;
|
|
688
1515
|
let compile;
|
|
689
1516
|
try {
|
|
690
|
-
const require2 = createRequire(
|
|
1517
|
+
const require2 = createRequire(resolve3(cwd, "package.json"));
|
|
691
1518
|
const tailwind = require2("tailwindcss");
|
|
692
1519
|
const fn = tailwind.compile;
|
|
693
1520
|
if (typeof fn !== "function") return null;
|
|
@@ -695,23 +1522,23 @@ async function getTailwindCompiler(cwd) {
|
|
|
695
1522
|
} catch {
|
|
696
1523
|
return null;
|
|
697
1524
|
}
|
|
698
|
-
const entryContent =
|
|
1525
|
+
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
699
1526
|
const loadStylesheet = async (id, base) => {
|
|
700
1527
|
if (id === "tailwindcss") {
|
|
701
|
-
const nodeModules =
|
|
702
|
-
const tailwindCssPath =
|
|
703
|
-
if (!
|
|
1528
|
+
const nodeModules = resolve3(cwd, "node_modules");
|
|
1529
|
+
const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
|
|
1530
|
+
if (!existsSync4(tailwindCssPath)) {
|
|
704
1531
|
throw new Error(
|
|
705
1532
|
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
706
1533
|
);
|
|
707
1534
|
}
|
|
708
|
-
const content =
|
|
1535
|
+
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
709
1536
|
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
710
1537
|
}
|
|
711
|
-
const full =
|
|
712
|
-
if (
|
|
713
|
-
const content =
|
|
714
|
-
return { path: full, base:
|
|
1538
|
+
const full = resolve3(base, id);
|
|
1539
|
+
if (existsSync4(full)) {
|
|
1540
|
+
const content = readFileSync4(full, "utf-8");
|
|
1541
|
+
return { path: full, base: resolve3(full, ".."), content };
|
|
715
1542
|
}
|
|
716
1543
|
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
717
1544
|
};
|
|
@@ -733,24 +1560,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
733
1560
|
}
|
|
734
1561
|
|
|
735
1562
|
// src/render-commands.ts
|
|
736
|
-
var
|
|
1563
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
737
1564
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
738
|
-
var
|
|
739
|
-
async function
|
|
740
|
-
if (
|
|
741
|
-
|
|
1565
|
+
var _pool2 = null;
|
|
1566
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1567
|
+
if (_pool2 === null) {
|
|
1568
|
+
_pool2 = new BrowserPool2({
|
|
742
1569
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
743
1570
|
viewportWidth,
|
|
744
1571
|
viewportHeight
|
|
745
1572
|
});
|
|
746
|
-
await
|
|
1573
|
+
await _pool2.init();
|
|
747
1574
|
}
|
|
748
|
-
return
|
|
1575
|
+
return _pool2;
|
|
749
1576
|
}
|
|
750
|
-
async function
|
|
751
|
-
if (
|
|
752
|
-
await
|
|
753
|
-
|
|
1577
|
+
async function shutdownPool2() {
|
|
1578
|
+
if (_pool2 !== null) {
|
|
1579
|
+
await _pool2.close();
|
|
1580
|
+
_pool2 = null;
|
|
754
1581
|
}
|
|
755
1582
|
}
|
|
756
1583
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -761,7 +1588,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
761
1588
|
_satori: satori,
|
|
762
1589
|
async renderCell(props, _complexityClass) {
|
|
763
1590
|
const startMs = performance.now();
|
|
764
|
-
const pool = await
|
|
1591
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
765
1592
|
const htmlHarness = await buildComponentHarness(
|
|
766
1593
|
filePath,
|
|
767
1594
|
componentName,
|
|
@@ -858,7 +1685,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
858
1685
|
};
|
|
859
1686
|
}
|
|
860
1687
|
function registerRenderSingle(renderCmd) {
|
|
861
|
-
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json",
|
|
1688
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
|
|
862
1689
|
async (componentName, opts) => {
|
|
863
1690
|
try {
|
|
864
1691
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -880,7 +1707,7 @@ Available: ${available}`
|
|
|
880
1707
|
}
|
|
881
1708
|
const { width, height } = parseViewport(opts.viewport);
|
|
882
1709
|
const rootDir = process.cwd();
|
|
883
|
-
const filePath =
|
|
1710
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
884
1711
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
885
1712
|
process.stderr.write(
|
|
886
1713
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
@@ -897,7 +1724,7 @@ Available: ${available}`
|
|
|
897
1724
|
}
|
|
898
1725
|
}
|
|
899
1726
|
);
|
|
900
|
-
await
|
|
1727
|
+
await shutdownPool2();
|
|
901
1728
|
if (outcome.crashed) {
|
|
902
1729
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
903
1730
|
`);
|
|
@@ -910,8 +1737,8 @@ Available: ${available}`
|
|
|
910
1737
|
}
|
|
911
1738
|
const result = outcome.result;
|
|
912
1739
|
if (opts.output !== void 0) {
|
|
913
|
-
const outPath =
|
|
914
|
-
|
|
1740
|
+
const outPath = resolve4(process.cwd(), opts.output);
|
|
1741
|
+
writeFileSync4(outPath, result.screenshot);
|
|
915
1742
|
process.stdout.write(
|
|
916
1743
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
917
1744
|
`
|
|
@@ -924,20 +1751,20 @@ Available: ${available}`
|
|
|
924
1751
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
925
1752
|
`);
|
|
926
1753
|
} else if (fmt === "file") {
|
|
927
|
-
const dir =
|
|
928
|
-
|
|
929
|
-
const outPath =
|
|
930
|
-
|
|
1754
|
+
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1755
|
+
mkdirSync3(dir, { recursive: true });
|
|
1756
|
+
const outPath = resolve4(dir, `${componentName}.png`);
|
|
1757
|
+
writeFileSync4(outPath, result.screenshot);
|
|
931
1758
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
932
1759
|
process.stdout.write(
|
|
933
1760
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
934
1761
|
`
|
|
935
1762
|
);
|
|
936
1763
|
} else {
|
|
937
|
-
const dir =
|
|
938
|
-
|
|
939
|
-
const outPath =
|
|
940
|
-
|
|
1764
|
+
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1765
|
+
mkdirSync3(dir, { recursive: true });
|
|
1766
|
+
const outPath = resolve4(dir, `${componentName}.png`);
|
|
1767
|
+
writeFileSync4(outPath, result.screenshot);
|
|
941
1768
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
942
1769
|
process.stdout.write(
|
|
943
1770
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -945,7 +1772,7 @@ Available: ${available}`
|
|
|
945
1772
|
);
|
|
946
1773
|
}
|
|
947
1774
|
} catch (err) {
|
|
948
|
-
await
|
|
1775
|
+
await shutdownPool2();
|
|
949
1776
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
950
1777
|
`);
|
|
951
1778
|
process.exit(1);
|
|
@@ -957,7 +1784,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
957
1784
|
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
|
|
958
1785
|
"--contexts <ids>",
|
|
959
1786
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
960
|
-
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json",
|
|
1787
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
|
|
961
1788
|
async (componentName, opts) => {
|
|
962
1789
|
try {
|
|
963
1790
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -972,7 +1799,7 @@ Available: ${available}`
|
|
|
972
1799
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
973
1800
|
const { width, height } = { width: 375, height: 812 };
|
|
974
1801
|
const rootDir = process.cwd();
|
|
975
|
-
const filePath =
|
|
1802
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
976
1803
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
977
1804
|
const axes = [];
|
|
978
1805
|
if (opts.axes !== void 0) {
|
|
@@ -1030,7 +1857,7 @@ Available: ${available}`
|
|
|
1030
1857
|
concurrency
|
|
1031
1858
|
});
|
|
1032
1859
|
const result = await matrix.render();
|
|
1033
|
-
await
|
|
1860
|
+
await shutdownPool2();
|
|
1034
1861
|
process.stderr.write(
|
|
1035
1862
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1036
1863
|
`
|
|
@@ -1039,8 +1866,8 @@ Available: ${available}`
|
|
|
1039
1866
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1040
1867
|
const gen = new SpriteSheetGenerator();
|
|
1041
1868
|
const sheet = await gen.generate(result);
|
|
1042
|
-
const spritePath =
|
|
1043
|
-
|
|
1869
|
+
const spritePath = resolve4(process.cwd(), opts.sprite);
|
|
1870
|
+
writeFileSync4(spritePath, sheet.png);
|
|
1044
1871
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
1045
1872
|
`);
|
|
1046
1873
|
}
|
|
@@ -1049,10 +1876,10 @@ Available: ${available}`
|
|
|
1049
1876
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1050
1877
|
const gen = new SpriteSheetGenerator();
|
|
1051
1878
|
const sheet = await gen.generate(result);
|
|
1052
|
-
const dir =
|
|
1053
|
-
|
|
1054
|
-
const outPath =
|
|
1055
|
-
|
|
1879
|
+
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1880
|
+
mkdirSync3(dir, { recursive: true });
|
|
1881
|
+
const outPath = resolve4(dir, `${componentName}-matrix.png`);
|
|
1882
|
+
writeFileSync4(outPath, sheet.png);
|
|
1056
1883
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
1057
1884
|
process.stdout.write(
|
|
1058
1885
|
`\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
|
|
@@ -1075,7 +1902,7 @@ Available: ${available}`
|
|
|
1075
1902
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1076
1903
|
}
|
|
1077
1904
|
} catch (err) {
|
|
1078
|
-
await
|
|
1905
|
+
await shutdownPool2();
|
|
1079
1906
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1080
1907
|
`);
|
|
1081
1908
|
process.exit(1);
|
|
@@ -1084,7 +1911,7 @@ Available: ${available}`
|
|
|
1084
1911
|
);
|
|
1085
1912
|
}
|
|
1086
1913
|
function registerRenderAll(renderCmd) {
|
|
1087
|
-
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json",
|
|
1914
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1088
1915
|
async (opts) => {
|
|
1089
1916
|
try {
|
|
1090
1917
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1095,8 +1922,8 @@ function registerRenderAll(renderCmd) {
|
|
|
1095
1922
|
return;
|
|
1096
1923
|
}
|
|
1097
1924
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
1098
|
-
const outputDir =
|
|
1099
|
-
|
|
1925
|
+
const outputDir = resolve4(process.cwd(), opts.outputDir);
|
|
1926
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
1100
1927
|
const rootDir = process.cwd();
|
|
1101
1928
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
1102
1929
|
`);
|
|
@@ -1105,7 +1932,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1105
1932
|
const renderOne = async (name) => {
|
|
1106
1933
|
const descriptor = manifest.components[name];
|
|
1107
1934
|
if (descriptor === void 0) return;
|
|
1108
|
-
const filePath =
|
|
1935
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
1109
1936
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
1110
1937
|
const outcome = await safeRender(
|
|
1111
1938
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
@@ -1128,8 +1955,8 @@ function registerRenderAll(renderCmd) {
|
|
|
1128
1955
|
success: false,
|
|
1129
1956
|
errorMessage: outcome.error.message
|
|
1130
1957
|
});
|
|
1131
|
-
const errPath =
|
|
1132
|
-
|
|
1958
|
+
const errPath = resolve4(outputDir, `${name}.error.json`);
|
|
1959
|
+
writeFileSync4(
|
|
1133
1960
|
errPath,
|
|
1134
1961
|
JSON.stringify(
|
|
1135
1962
|
{
|
|
@@ -1146,10 +1973,10 @@ function registerRenderAll(renderCmd) {
|
|
|
1146
1973
|
}
|
|
1147
1974
|
const result = outcome.result;
|
|
1148
1975
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
1149
|
-
const pngPath =
|
|
1150
|
-
|
|
1151
|
-
const jsonPath =
|
|
1152
|
-
|
|
1976
|
+
const pngPath = resolve4(outputDir, `${name}.png`);
|
|
1977
|
+
writeFileSync4(pngPath, result.screenshot);
|
|
1978
|
+
const jsonPath = resolve4(outputDir, `${name}.json`);
|
|
1979
|
+
writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
1153
1980
|
if (isTTY()) {
|
|
1154
1981
|
process.stdout.write(
|
|
1155
1982
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1172,13 +1999,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1172
1999
|
workers.push(worker());
|
|
1173
2000
|
}
|
|
1174
2001
|
await Promise.all(workers);
|
|
1175
|
-
await
|
|
2002
|
+
await shutdownPool2();
|
|
1176
2003
|
process.stderr.write("\n");
|
|
1177
2004
|
const summary = formatSummaryText(results, outputDir);
|
|
1178
2005
|
process.stderr.write(`${summary}
|
|
1179
2006
|
`);
|
|
1180
2007
|
} catch (err) {
|
|
1181
|
-
await
|
|
2008
|
+
await shutdownPool2();
|
|
1182
2009
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1183
2010
|
`);
|
|
1184
2011
|
process.exit(1);
|
|
@@ -1211,7 +2038,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
1211
2038
|
return "json";
|
|
1212
2039
|
}
|
|
1213
2040
|
function createRenderCommand() {
|
|
1214
|
-
const renderCmd = new
|
|
2041
|
+
const renderCmd = new Command4("render").description(
|
|
1215
2042
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
1216
2043
|
);
|
|
1217
2044
|
registerRenderSingle(renderCmd);
|
|
@@ -1500,8 +2327,8 @@ function buildStructuredReport(report) {
|
|
|
1500
2327
|
}
|
|
1501
2328
|
|
|
1502
2329
|
// src/tokens/commands.ts
|
|
1503
|
-
import { existsSync as
|
|
1504
|
-
import { resolve as
|
|
2330
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2331
|
+
import { resolve as resolve5 } from "path";
|
|
1505
2332
|
import {
|
|
1506
2333
|
parseTokenFileSync,
|
|
1507
2334
|
TokenParseError,
|
|
@@ -1509,7 +2336,7 @@ import {
|
|
|
1509
2336
|
TokenValidationError,
|
|
1510
2337
|
validateTokenFile
|
|
1511
2338
|
} from "@agent-scope/tokens";
|
|
1512
|
-
import { Command as
|
|
2339
|
+
import { Command as Command5 } from "commander";
|
|
1513
2340
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
1514
2341
|
var CONFIG_FILE = "reactscope.config.json";
|
|
1515
2342
|
function isTTY2() {
|
|
@@ -1531,30 +2358,30 @@ function buildTable2(headers, rows) {
|
|
|
1531
2358
|
}
|
|
1532
2359
|
function resolveTokenFilePath(fileFlag) {
|
|
1533
2360
|
if (fileFlag !== void 0) {
|
|
1534
|
-
return
|
|
2361
|
+
return resolve5(process.cwd(), fileFlag);
|
|
1535
2362
|
}
|
|
1536
|
-
const configPath =
|
|
1537
|
-
if (
|
|
2363
|
+
const configPath = resolve5(process.cwd(), CONFIG_FILE);
|
|
2364
|
+
if (existsSync5(configPath)) {
|
|
1538
2365
|
try {
|
|
1539
|
-
const raw =
|
|
2366
|
+
const raw = readFileSync5(configPath, "utf-8");
|
|
1540
2367
|
const config = JSON.parse(raw);
|
|
1541
2368
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
1542
2369
|
const file = config.tokens.file;
|
|
1543
|
-
return
|
|
2370
|
+
return resolve5(process.cwd(), file);
|
|
1544
2371
|
}
|
|
1545
2372
|
} catch {
|
|
1546
2373
|
}
|
|
1547
2374
|
}
|
|
1548
|
-
return
|
|
2375
|
+
return resolve5(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
1549
2376
|
}
|
|
1550
2377
|
function loadTokens(absPath) {
|
|
1551
|
-
if (!
|
|
2378
|
+
if (!existsSync5(absPath)) {
|
|
1552
2379
|
throw new Error(
|
|
1553
2380
|
`Token file not found at ${absPath}.
|
|
1554
2381
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
1555
2382
|
);
|
|
1556
2383
|
}
|
|
1557
|
-
const raw =
|
|
2384
|
+
const raw = readFileSync5(absPath, "utf-8");
|
|
1558
2385
|
return parseTokenFileSync(raw);
|
|
1559
2386
|
}
|
|
1560
2387
|
function getRawValue(node, segments) {
|
|
@@ -1768,13 +2595,13 @@ function registerValidate(tokensCmd) {
|
|
|
1768
2595
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
1769
2596
|
try {
|
|
1770
2597
|
const filePath = resolveTokenFilePath(opts.file);
|
|
1771
|
-
if (!
|
|
2598
|
+
if (!existsSync5(filePath)) {
|
|
1772
2599
|
throw new Error(
|
|
1773
2600
|
`Token file not found at ${filePath}.
|
|
1774
2601
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
1775
2602
|
);
|
|
1776
2603
|
}
|
|
1777
|
-
const raw =
|
|
2604
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
1778
2605
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
1779
2606
|
const errors = [];
|
|
1780
2607
|
let parsed;
|
|
@@ -1842,7 +2669,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
1842
2669
|
}
|
|
1843
2670
|
}
|
|
1844
2671
|
function createTokensCommand() {
|
|
1845
|
-
const tokensCmd = new
|
|
2672
|
+
const tokensCmd = new Command5("tokens").description(
|
|
1846
2673
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
1847
2674
|
);
|
|
1848
2675
|
registerGet2(tokensCmd);
|
|
@@ -1855,7 +2682,7 @@ function createTokensCommand() {
|
|
|
1855
2682
|
|
|
1856
2683
|
// src/program.ts
|
|
1857
2684
|
function createProgram(options = {}) {
|
|
1858
|
-
const program2 = new
|
|
2685
|
+
const program2 = new Command6("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
1859
2686
|
program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
1860
2687
|
async (url, opts) => {
|
|
1861
2688
|
try {
|
|
@@ -1928,7 +2755,7 @@ function createProgram(options = {}) {
|
|
|
1928
2755
|
}
|
|
1929
2756
|
);
|
|
1930
2757
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
1931
|
-
const raw =
|
|
2758
|
+
const raw = readFileSync6(tracePath, "utf-8");
|
|
1932
2759
|
const trace = loadTrace(raw);
|
|
1933
2760
|
const source = generateTest(trace, {
|
|
1934
2761
|
description: opts.description,
|
|
@@ -1940,6 +2767,8 @@ function createProgram(options = {}) {
|
|
|
1940
2767
|
program2.addCommand(createManifestCommand());
|
|
1941
2768
|
program2.addCommand(createRenderCommand());
|
|
1942
2769
|
program2.addCommand(createTokensCommand());
|
|
2770
|
+
program2.addCommand(createInstrumentCommand());
|
|
2771
|
+
program2.addCommand(createInitCommand());
|
|
1943
2772
|
return program2;
|
|
1944
2773
|
}
|
|
1945
2774
|
|