@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/index.cjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
|
-
var
|
|
5
|
+
var readline = require('readline');
|
|
6
6
|
var commander = require('commander');
|
|
7
|
+
var manifest = require('@agent-scope/manifest');
|
|
7
8
|
var playwright = require('@agent-scope/playwright');
|
|
8
9
|
var playwright$1 = require('playwright');
|
|
9
10
|
var render = require('@agent-scope/render');
|
|
@@ -29,9 +30,353 @@ function _interopNamespace(e) {
|
|
|
29
30
|
return Object.freeze(n);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
|
|
32
34
|
var esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild);
|
|
33
35
|
|
|
34
|
-
// src/
|
|
36
|
+
// src/init/index.ts
|
|
37
|
+
function hasConfigFile(dir, stem) {
|
|
38
|
+
if (!fs.existsSync(dir)) return false;
|
|
39
|
+
try {
|
|
40
|
+
const entries = fs.readdirSync(dir);
|
|
41
|
+
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readSafe(path) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(path, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function detectFramework(rootDir, packageDeps) {
|
|
54
|
+
if (hasConfigFile(rootDir, "next.config")) return "next";
|
|
55
|
+
if (hasConfigFile(rootDir, "vite.config")) return "vite";
|
|
56
|
+
if (hasConfigFile(rootDir, "remix.config")) return "remix";
|
|
57
|
+
if ("react-scripts" in packageDeps) return "cra";
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
function detectPackageManager(rootDir) {
|
|
61
|
+
if (fs.existsSync(path.join(rootDir, "bun.lock"))) return "bun";
|
|
62
|
+
if (fs.existsSync(path.join(rootDir, "yarn.lock"))) return "yarn";
|
|
63
|
+
if (fs.existsSync(path.join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
64
|
+
if (fs.existsSync(path.join(rootDir, "package-lock.json"))) return "npm";
|
|
65
|
+
return "npm";
|
|
66
|
+
}
|
|
67
|
+
function detectTypeScript(rootDir) {
|
|
68
|
+
const candidate = path.join(rootDir, "tsconfig.json");
|
|
69
|
+
if (fs.existsSync(candidate)) {
|
|
70
|
+
return { typescript: true, tsconfigPath: candidate };
|
|
71
|
+
}
|
|
72
|
+
return { typescript: false, tsconfigPath: null };
|
|
73
|
+
}
|
|
74
|
+
var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
|
|
75
|
+
var COMPONENT_EXTS = [".tsx", ".jsx"];
|
|
76
|
+
function detectComponentPatterns(rootDir, typescript) {
|
|
77
|
+
const patterns = [];
|
|
78
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
79
|
+
const altExt = typescript ? "jsx" : "jsx";
|
|
80
|
+
for (const dir of COMPONENT_DIRS) {
|
|
81
|
+
const absDir = path.join(rootDir, dir);
|
|
82
|
+
if (!fs.existsSync(absDir)) continue;
|
|
83
|
+
let hasComponents = false;
|
|
84
|
+
try {
|
|
85
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
86
|
+
hasComponents = entries.some(
|
|
87
|
+
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
88
|
+
);
|
|
89
|
+
if (!hasComponents) {
|
|
90
|
+
hasComponents = entries.some(
|
|
91
|
+
(e) => e.isDirectory() && (() => {
|
|
92
|
+
try {
|
|
93
|
+
return fs.readdirSync(path.join(absDir, e.name)).some(
|
|
94
|
+
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
95
|
+
);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
})()
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (hasComponents) {
|
|
106
|
+
patterns.push(`${dir}/**/*.${ext}`);
|
|
107
|
+
if (altExt !== ext) {
|
|
108
|
+
patterns.push(`${dir}/**/*.${altExt}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const unique = [...new Set(patterns)];
|
|
113
|
+
if (unique.length === 0) {
|
|
114
|
+
return [`**/*.${ext}`];
|
|
115
|
+
}
|
|
116
|
+
return unique;
|
|
117
|
+
}
|
|
118
|
+
var TAILWIND_STEMS = ["tailwind.config"];
|
|
119
|
+
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
120
|
+
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
121
|
+
var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
|
|
122
|
+
function detectTokenSources(rootDir) {
|
|
123
|
+
const sources = [];
|
|
124
|
+
for (const stem of TAILWIND_STEMS) {
|
|
125
|
+
if (hasConfigFile(rootDir, stem)) {
|
|
126
|
+
try {
|
|
127
|
+
const entries = fs.readdirSync(rootDir);
|
|
128
|
+
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
129
|
+
if (match) {
|
|
130
|
+
sources.push({ kind: "tailwind-config", path: path.join(rootDir, match) });
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const srcDir = path.join(rootDir, "src");
|
|
137
|
+
const dirsToScan = fs.existsSync(srcDir) ? [srcDir] : [];
|
|
138
|
+
for (const scanDir of dirsToScan) {
|
|
139
|
+
try {
|
|
140
|
+
const entries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
143
|
+
const filePath = path.join(scanDir, entry.name);
|
|
144
|
+
const content = readSafe(filePath);
|
|
145
|
+
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
146
|
+
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (fs.existsSync(srcDir)) {
|
|
154
|
+
try {
|
|
155
|
+
const entries = fs.readdirSync(srcDir);
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
158
|
+
sources.push({ kind: "theme-file", path: path.join(srcDir, entry) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return sources;
|
|
165
|
+
}
|
|
166
|
+
function detectProject(rootDir) {
|
|
167
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
168
|
+
let packageDeps = {};
|
|
169
|
+
const pkgContent = readSafe(pkgPath);
|
|
170
|
+
if (pkgContent !== null) {
|
|
171
|
+
try {
|
|
172
|
+
const pkg = JSON.parse(pkgContent);
|
|
173
|
+
packageDeps = {
|
|
174
|
+
...pkg.dependencies,
|
|
175
|
+
...pkg.devDependencies
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const framework = detectFramework(rootDir, packageDeps);
|
|
181
|
+
const { typescript, tsconfigPath } = detectTypeScript(rootDir);
|
|
182
|
+
const packageManager = detectPackageManager(rootDir);
|
|
183
|
+
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
184
|
+
const tokenSources = detectTokenSources(rootDir);
|
|
185
|
+
return {
|
|
186
|
+
framework,
|
|
187
|
+
typescript,
|
|
188
|
+
tsconfigPath,
|
|
189
|
+
componentPatterns,
|
|
190
|
+
tokenSources,
|
|
191
|
+
packageManager
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
195
|
+
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
196
|
+
return {
|
|
197
|
+
components: {
|
|
198
|
+
include,
|
|
199
|
+
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
200
|
+
wrappers: { providers: [], globalCSS: [] }
|
|
201
|
+
},
|
|
202
|
+
render: {
|
|
203
|
+
viewport: { default: { width: 1280, height: 800 } },
|
|
204
|
+
theme: "light",
|
|
205
|
+
warmBrowser: true
|
|
206
|
+
},
|
|
207
|
+
tokens: {
|
|
208
|
+
file: tokenFile,
|
|
209
|
+
compliance: { threshold: 90 }
|
|
210
|
+
},
|
|
211
|
+
output: {
|
|
212
|
+
dir: outputDir,
|
|
213
|
+
sprites: { format: "png", cellPadding: 8, labelAxes: true },
|
|
214
|
+
json: { pretty: true }
|
|
215
|
+
},
|
|
216
|
+
ci: {
|
|
217
|
+
complianceThreshold: 90,
|
|
218
|
+
failOnA11yViolations: true,
|
|
219
|
+
failOnConsoleErrors: false,
|
|
220
|
+
baselinePath: `${outputDir}baseline/`
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function createRL() {
|
|
225
|
+
return readline__namespace.createInterface({
|
|
226
|
+
input: process.stdin,
|
|
227
|
+
output: process.stdout
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function ask(rl, question) {
|
|
231
|
+
return new Promise((resolve6) => {
|
|
232
|
+
rl.question(question, (answer) => {
|
|
233
|
+
resolve6(answer.trim());
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function askWithDefault(rl, label, defaultValue) {
|
|
238
|
+
const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
|
|
239
|
+
return answer.length > 0 ? answer : defaultValue;
|
|
240
|
+
}
|
|
241
|
+
function ensureGitignoreEntry(rootDir, entry) {
|
|
242
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
243
|
+
if (fs.existsSync(gitignorePath)) {
|
|
244
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
245
|
+
const normalised = entry.replace(/\/$/, "");
|
|
246
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
247
|
+
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
251
|
+
fs.appendFileSync(gitignorePath, `${suffix}${entry}
|
|
252
|
+
`);
|
|
253
|
+
} else {
|
|
254
|
+
fs.writeFileSync(gitignorePath, `${entry}
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function scaffoldConfig(rootDir, config) {
|
|
259
|
+
const path$1 = path.join(rootDir, "reactscope.config.json");
|
|
260
|
+
fs.writeFileSync(path$1, `${JSON.stringify(config, null, 2)}
|
|
261
|
+
`);
|
|
262
|
+
return path$1;
|
|
263
|
+
}
|
|
264
|
+
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
265
|
+
const path$1 = path.join(rootDir, tokenFile);
|
|
266
|
+
if (!fs.existsSync(path$1)) {
|
|
267
|
+
const stub = {
|
|
268
|
+
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
269
|
+
tokens: {}
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(path$1, `${JSON.stringify(stub, null, 2)}
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
return path$1;
|
|
275
|
+
}
|
|
276
|
+
function scaffoldOutputDir(rootDir, outputDir) {
|
|
277
|
+
const dirPath = path.join(rootDir, outputDir);
|
|
278
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
279
|
+
const keepPath = path.join(dirPath, ".gitkeep");
|
|
280
|
+
if (!fs.existsSync(keepPath)) {
|
|
281
|
+
fs.writeFileSync(keepPath, "");
|
|
282
|
+
}
|
|
283
|
+
return dirPath;
|
|
284
|
+
}
|
|
285
|
+
async function runInit(options) {
|
|
286
|
+
const rootDir = options.cwd ?? process.cwd();
|
|
287
|
+
const configPath = path.join(rootDir, "reactscope.config.json");
|
|
288
|
+
const created = [];
|
|
289
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
290
|
+
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
291
|
+
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
292
|
+
`);
|
|
293
|
+
return { success: false, message: msg, created: [], skipped: true };
|
|
294
|
+
}
|
|
295
|
+
const detected = detectProject(rootDir);
|
|
296
|
+
const defaultTokenFile = "reactscope.tokens.json";
|
|
297
|
+
const defaultOutputDir = ".reactscope/";
|
|
298
|
+
let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
|
|
299
|
+
if (options.yes) {
|
|
300
|
+
process.stdout.write("\n\u{1F50D} Detected project settings:\n");
|
|
301
|
+
process.stdout.write(` Framework : ${detected.framework}
|
|
302
|
+
`);
|
|
303
|
+
process.stdout.write(` TypeScript : ${detected.typescript}
|
|
304
|
+
`);
|
|
305
|
+
process.stdout.write(` Include globs : ${config.components.include.join(", ")}
|
|
306
|
+
`);
|
|
307
|
+
process.stdout.write(` Token file : ${config.tokens.file}
|
|
308
|
+
`);
|
|
309
|
+
process.stdout.write(` Output dir : ${config.output.dir}
|
|
310
|
+
|
|
311
|
+
`);
|
|
312
|
+
} else {
|
|
313
|
+
const rl = createRL();
|
|
314
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
315
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
316
|
+
try {
|
|
317
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
318
|
+
`);
|
|
319
|
+
const includeRaw = await askWithDefault(
|
|
320
|
+
rl,
|
|
321
|
+
"Component include patterns (comma-separated)",
|
|
322
|
+
config.components.include.join(", ")
|
|
323
|
+
);
|
|
324
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
325
|
+
const excludeRaw = await askWithDefault(
|
|
326
|
+
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
|
+
}
|
|
35
380
|
|
|
36
381
|
// src/manifest-formatter.ts
|
|
37
382
|
function isTTY() {
|
|
@@ -607,6 +952,471 @@ function csvEscape(value) {
|
|
|
607
952
|
}
|
|
608
953
|
return value;
|
|
609
954
|
}
|
|
955
|
+
|
|
956
|
+
// src/instrument/renders.ts
|
|
957
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
958
|
+
function determineTrigger(event) {
|
|
959
|
+
if (event.forceUpdate) return "force_update";
|
|
960
|
+
if (event.stateChanged) return "state_change";
|
|
961
|
+
if (event.propsChanged) return "props_change";
|
|
962
|
+
if (event.contextChanged) return "context_change";
|
|
963
|
+
if (event.hookDepsChanged) return "hook_dependency";
|
|
964
|
+
return "parent_rerender";
|
|
965
|
+
}
|
|
966
|
+
function isWastedRender(event) {
|
|
967
|
+
return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
|
|
968
|
+
}
|
|
969
|
+
function buildCausalityChains(rawEvents) {
|
|
970
|
+
const result = [];
|
|
971
|
+
const componentLastRender = /* @__PURE__ */ new Map();
|
|
972
|
+
for (const raw of rawEvents) {
|
|
973
|
+
const trigger = determineTrigger(raw);
|
|
974
|
+
const wasted = isWastedRender(raw);
|
|
975
|
+
const chain = [];
|
|
976
|
+
let current = raw;
|
|
977
|
+
const visited = /* @__PURE__ */ new Set();
|
|
978
|
+
while (true) {
|
|
979
|
+
if (visited.has(current.component)) break;
|
|
980
|
+
visited.add(current.component);
|
|
981
|
+
const currentTrigger = determineTrigger(current);
|
|
982
|
+
chain.unshift({
|
|
983
|
+
component: current.component,
|
|
984
|
+
trigger: currentTrigger,
|
|
985
|
+
propsChanged: current.propsChanged,
|
|
986
|
+
stateChanged: current.stateChanged,
|
|
987
|
+
contextChanged: current.contextChanged
|
|
988
|
+
});
|
|
989
|
+
if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
const parentEvent = componentLastRender.get(current.parentComponent);
|
|
993
|
+
if (parentEvent === void 0) break;
|
|
994
|
+
current = parentEvent;
|
|
995
|
+
}
|
|
996
|
+
const rootCause = chain[0];
|
|
997
|
+
const cascadeRenders = rawEvents.filter((e) => {
|
|
998
|
+
if (rootCause === void 0) return false;
|
|
999
|
+
return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
|
|
1000
|
+
});
|
|
1001
|
+
result.push({
|
|
1002
|
+
component: raw.component,
|
|
1003
|
+
renderIndex: raw.renderIndex,
|
|
1004
|
+
trigger,
|
|
1005
|
+
propsChanged: raw.propsChanged,
|
|
1006
|
+
stateChanged: raw.stateChanged,
|
|
1007
|
+
contextChanged: raw.contextChanged,
|
|
1008
|
+
memoized: raw.memoized,
|
|
1009
|
+
wasted,
|
|
1010
|
+
chain,
|
|
1011
|
+
cascade: {
|
|
1012
|
+
totalRendersTriggered: cascadeRenders.length,
|
|
1013
|
+
uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
|
|
1014
|
+
unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
componentLastRender.set(raw.component, raw);
|
|
1018
|
+
}
|
|
1019
|
+
return result;
|
|
1020
|
+
}
|
|
1021
|
+
function applyHeuristicFlags(renders) {
|
|
1022
|
+
const flags = [];
|
|
1023
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
1024
|
+
for (const r of renders) {
|
|
1025
|
+
if (!byComponent.has(r.component)) byComponent.set(r.component, []);
|
|
1026
|
+
byComponent.get(r.component).push(r);
|
|
1027
|
+
}
|
|
1028
|
+
for (const [component, events] of byComponent) {
|
|
1029
|
+
const wastedCount = events.filter((e) => e.wasted).length;
|
|
1030
|
+
const totalCount = events.length;
|
|
1031
|
+
if (wastedCount > 0) {
|
|
1032
|
+
flags.push({
|
|
1033
|
+
id: "WASTED_RENDER",
|
|
1034
|
+
severity: "warning",
|
|
1035
|
+
component,
|
|
1036
|
+
detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
|
|
1037
|
+
data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
for (const event of events) {
|
|
1041
|
+
if (event.cascade.totalRendersTriggered > 50) {
|
|
1042
|
+
flags.push({
|
|
1043
|
+
id: "RENDER_CASCADE",
|
|
1044
|
+
severity: "warning",
|
|
1045
|
+
component,
|
|
1046
|
+
detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
|
|
1047
|
+
data: {
|
|
1048
|
+
totalRendersTriggered: event.cascade.totalRendersTriggered,
|
|
1049
|
+
uniqueComponents: event.cascade.uniqueComponents,
|
|
1050
|
+
unchangedPropRenders: event.cascade.unchangedPropRenders
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return flags;
|
|
1058
|
+
}
|
|
1059
|
+
function buildInstrumentationScript() {
|
|
1060
|
+
return (
|
|
1061
|
+
/* js */
|
|
1062
|
+
`
|
|
1063
|
+
(function installScopeRenderInstrumentation() {
|
|
1064
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1065
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1066
|
+
|
|
1067
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1068
|
+
if (!hook) return;
|
|
1069
|
+
|
|
1070
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
1071
|
+
var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
|
|
1072
|
+
|
|
1073
|
+
function extractName(fiber) {
|
|
1074
|
+
if (!fiber) return 'Unknown';
|
|
1075
|
+
var type = fiber.type;
|
|
1076
|
+
if (!type) return 'Unknown';
|
|
1077
|
+
if (typeof type === 'string') return type; // host element
|
|
1078
|
+
if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
|
|
1079
|
+
if (type.displayName) return type.displayName;
|
|
1080
|
+
if (type.render && typeof type.render === 'function') {
|
|
1081
|
+
return type.render.displayName || type.render.name || 'Anonymous';
|
|
1082
|
+
}
|
|
1083
|
+
return 'Anonymous';
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function isMemoized(fiber) {
|
|
1087
|
+
// MemoComponent = 14, SimpleMemoComponent = 15
|
|
1088
|
+
return fiber.tag === 14 || fiber.tag === 15;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function isComponent(fiber) {
|
|
1092
|
+
// FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
|
|
1093
|
+
return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function shallowEqual(a, b) {
|
|
1097
|
+
if (a === b) return true;
|
|
1098
|
+
if (!a || !b) return a === b;
|
|
1099
|
+
var keysA = Object.keys(a);
|
|
1100
|
+
var keysB = Object.keys(b);
|
|
1101
|
+
if (keysA.length !== keysB.length) return false;
|
|
1102
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
1103
|
+
var k = keysA[i];
|
|
1104
|
+
if (k === 'children') continue; // ignore children prop
|
|
1105
|
+
if (a[k] !== b[k]) return false;
|
|
1106
|
+
}
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function getParentComponentName(fiber) {
|
|
1111
|
+
var parent = fiber.return;
|
|
1112
|
+
while (parent) {
|
|
1113
|
+
if (isComponent(parent)) {
|
|
1114
|
+
var name = extractName(parent);
|
|
1115
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
|
|
1116
|
+
}
|
|
1117
|
+
parent = parent.return;
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function walkCommit(fiber) {
|
|
1123
|
+
if (!fiber) return;
|
|
1124
|
+
|
|
1125
|
+
if (isComponent(fiber)) {
|
|
1126
|
+
var name = extractName(fiber);
|
|
1127
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
|
|
1128
|
+
var memoized = isMemoized(fiber);
|
|
1129
|
+
var currentProps = fiber.memoizedProps || {};
|
|
1130
|
+
var prev = renderedComponents.get(name);
|
|
1131
|
+
|
|
1132
|
+
var propsChanged = true;
|
|
1133
|
+
var stateChanged = false;
|
|
1134
|
+
var contextChanged = false;
|
|
1135
|
+
var hookDepsChanged = false;
|
|
1136
|
+
var forceUpdate = false;
|
|
1137
|
+
|
|
1138
|
+
if (prev) {
|
|
1139
|
+
propsChanged = !shallowEqual(prev.lastProps, currentProps);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// State: check memoizedState chain
|
|
1143
|
+
var memoizedState = fiber.memoizedState;
|
|
1144
|
+
if (prev && prev.lastStateSerialized !== undefined) {
|
|
1145
|
+
try {
|
|
1146
|
+
var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1147
|
+
stateChanged = stateSig !== prev.lastStateSerialized;
|
|
1148
|
+
} catch (_) {
|
|
1149
|
+
stateChanged = false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Context: use _debugHookTypes or check dependencies
|
|
1154
|
+
var deps = fiber.dependencies;
|
|
1155
|
+
if (deps && deps.firstContext) {
|
|
1156
|
+
contextChanged = true; // conservative: context dep present = may have changed
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
var stateSig;
|
|
1160
|
+
try {
|
|
1161
|
+
stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1162
|
+
} catch (_) {
|
|
1163
|
+
stateSig = null;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
renderedComponents.set(name, {
|
|
1167
|
+
lastProps: currentProps,
|
|
1168
|
+
lastStateSerialized: stateSig,
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
var parentName = getParentComponentName(fiber);
|
|
1172
|
+
|
|
1173
|
+
window.__SCOPE_RENDER_EVENTS__.push({
|
|
1174
|
+
component: name,
|
|
1175
|
+
renderIndex: window.__SCOPE_RENDER_INDEX__++,
|
|
1176
|
+
propsChanged: prev ? propsChanged : false,
|
|
1177
|
+
stateChanged: stateChanged,
|
|
1178
|
+
contextChanged: contextChanged,
|
|
1179
|
+
memoized: memoized,
|
|
1180
|
+
parentComponent: parentName,
|
|
1181
|
+
hookDepsChanged: hookDepsChanged,
|
|
1182
|
+
forceUpdate: forceUpdate,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
walkCommit(fiber.child);
|
|
1188
|
+
walkCommit(fiber.sibling);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1192
|
+
if (typeof originalOnCommit === 'function') {
|
|
1193
|
+
originalOnCommit.call(hook, rendererID, root, priorityLevel);
|
|
1194
|
+
}
|
|
1195
|
+
var wipRoot = root && root.current && root.current.alternate;
|
|
1196
|
+
if (wipRoot) walkCommit(wipRoot);
|
|
1197
|
+
};
|
|
1198
|
+
})();
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
async function replayInteraction(page, steps) {
|
|
1203
|
+
for (const step of steps) {
|
|
1204
|
+
switch (step.action) {
|
|
1205
|
+
case "click":
|
|
1206
|
+
if (step.target !== void 0) {
|
|
1207
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
break;
|
|
1211
|
+
case "type":
|
|
1212
|
+
if (step.target !== void 0 && step.text !== void 0) {
|
|
1213
|
+
await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
|
|
1214
|
+
await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
break;
|
|
1219
|
+
case "hover":
|
|
1220
|
+
if (step.target !== void 0) {
|
|
1221
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
case "blur":
|
|
1226
|
+
if (step.target !== void 0) {
|
|
1227
|
+
await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
break;
|
|
1231
|
+
case "focus":
|
|
1232
|
+
if (step.target !== void 0) {
|
|
1233
|
+
await page.focus(step.target, { timeout: 5e3 }).catch(() => {
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
case "scroll":
|
|
1238
|
+
if (step.target !== void 0) {
|
|
1239
|
+
await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
break;
|
|
1243
|
+
case "wait": {
|
|
1244
|
+
const timeout = step.timeout ?? 1e3;
|
|
1245
|
+
if (step.condition === "idle") {
|
|
1246
|
+
await page.waitForLoadState("networkidle", { timeout }).catch(() => {
|
|
1247
|
+
});
|
|
1248
|
+
} else {
|
|
1249
|
+
await page.waitForTimeout(timeout);
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
var _pool = null;
|
|
1257
|
+
async function getPool() {
|
|
1258
|
+
if (_pool === null) {
|
|
1259
|
+
_pool = new render.BrowserPool({
|
|
1260
|
+
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1261
|
+
viewportWidth: 1280,
|
|
1262
|
+
viewportHeight: 800
|
|
1263
|
+
});
|
|
1264
|
+
await _pool.init();
|
|
1265
|
+
}
|
|
1266
|
+
return _pool;
|
|
1267
|
+
}
|
|
1268
|
+
async function shutdownPool() {
|
|
1269
|
+
if (_pool !== null) {
|
|
1270
|
+
await _pool.close();
|
|
1271
|
+
_pool = null;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function analyzeRenders(options) {
|
|
1275
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
|
|
1276
|
+
const manifest = loadManifest(manifestPath);
|
|
1277
|
+
const descriptor = manifest.components[options.componentName];
|
|
1278
|
+
if (descriptor === void 0) {
|
|
1279
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`Component "${options.componentName}" not found in manifest.
|
|
1282
|
+
Available: ${available}`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const rootDir = process.cwd();
|
|
1286
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1287
|
+
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1288
|
+
const pool = await getPool();
|
|
1289
|
+
const slot = await pool.acquire();
|
|
1290
|
+
const { page } = slot;
|
|
1291
|
+
const startMs = performance.now();
|
|
1292
|
+
try {
|
|
1293
|
+
await page.addInitScript(buildInstrumentationScript());
|
|
1294
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1295
|
+
await page.waitForFunction(
|
|
1296
|
+
() => window.__SCOPE_RENDER_COMPLETE__ === true,
|
|
1297
|
+
{ timeout: 15e3 }
|
|
1298
|
+
);
|
|
1299
|
+
await page.waitForTimeout(100);
|
|
1300
|
+
await page.evaluate(() => {
|
|
1301
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1302
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1303
|
+
});
|
|
1304
|
+
await replayInteraction(page, options.interaction);
|
|
1305
|
+
await page.waitForTimeout(200);
|
|
1306
|
+
const interactionDurationMs = performance.now() - startMs;
|
|
1307
|
+
const rawEvents = await page.evaluate(() => {
|
|
1308
|
+
return window.__SCOPE_RENDER_EVENTS__ ?? [];
|
|
1309
|
+
});
|
|
1310
|
+
const renders = buildCausalityChains(rawEvents);
|
|
1311
|
+
const flags = applyHeuristicFlags(renders);
|
|
1312
|
+
const uniqueComponents = new Set(renders.map((r) => r.component)).size;
|
|
1313
|
+
const wastedRenders = renders.filter((r) => r.wasted).length;
|
|
1314
|
+
return {
|
|
1315
|
+
component: options.componentName,
|
|
1316
|
+
interaction: options.interaction,
|
|
1317
|
+
summary: {
|
|
1318
|
+
totalRenders: renders.length,
|
|
1319
|
+
uniqueComponents,
|
|
1320
|
+
wastedRenders,
|
|
1321
|
+
interactionDurationMs: Math.round(interactionDurationMs)
|
|
1322
|
+
},
|
|
1323
|
+
renders,
|
|
1324
|
+
flags
|
|
1325
|
+
};
|
|
1326
|
+
} finally {
|
|
1327
|
+
pool.release(slot);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function formatRendersTable(result) {
|
|
1331
|
+
const lines = [];
|
|
1332
|
+
lines.push(`
|
|
1333
|
+
\u{1F50D} Re-render Analysis: ${result.component}`);
|
|
1334
|
+
lines.push(`${"\u2500".repeat(60)}`);
|
|
1335
|
+
lines.push(`Total renders: ${result.summary.totalRenders}`);
|
|
1336
|
+
lines.push(`Unique components: ${result.summary.uniqueComponents}`);
|
|
1337
|
+
lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
|
|
1338
|
+
lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
|
|
1339
|
+
lines.push("");
|
|
1340
|
+
if (result.renders.length === 0) {
|
|
1341
|
+
lines.push("No re-renders captured during interaction.");
|
|
1342
|
+
} else {
|
|
1343
|
+
lines.push("Re-renders:");
|
|
1344
|
+
lines.push(
|
|
1345
|
+
`${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
|
|
1346
|
+
);
|
|
1347
|
+
lines.push("\u2500".repeat(80));
|
|
1348
|
+
for (const r of result.renders) {
|
|
1349
|
+
const wasted = r.wasted ? "\u26A0 yes" : "no";
|
|
1350
|
+
const idx = String(r.renderIndex).padEnd(4);
|
|
1351
|
+
const comp = r.component.slice(0, 29).padEnd(30);
|
|
1352
|
+
const trig = r.trigger.padEnd(18);
|
|
1353
|
+
const w = wasted.padEnd(7);
|
|
1354
|
+
const depth = r.chain.length;
|
|
1355
|
+
lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (result.flags.length > 0) {
|
|
1359
|
+
lines.push("");
|
|
1360
|
+
lines.push("Flags:");
|
|
1361
|
+
for (const flag of result.flags) {
|
|
1362
|
+
const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1363
|
+
lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return lines.join("\n");
|
|
1367
|
+
}
|
|
1368
|
+
function createInstrumentRendersCommand() {
|
|
1369
|
+
return new commander.Command("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(
|
|
1370
|
+
"--interaction <json>",
|
|
1371
|
+
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1372
|
+
"[]"
|
|
1373
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
|
|
1374
|
+
async (componentName, opts) => {
|
|
1375
|
+
let interaction = [];
|
|
1376
|
+
try {
|
|
1377
|
+
interaction = JSON.parse(opts.interaction);
|
|
1378
|
+
if (!Array.isArray(interaction)) {
|
|
1379
|
+
throw new Error("Interaction must be a JSON array");
|
|
1380
|
+
}
|
|
1381
|
+
} catch {
|
|
1382
|
+
process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
|
|
1383
|
+
`);
|
|
1384
|
+
process.exit(1);
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
process.stderr.write(
|
|
1388
|
+
`Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
|
|
1389
|
+
`
|
|
1390
|
+
);
|
|
1391
|
+
const result = await analyzeRenders({
|
|
1392
|
+
componentName,
|
|
1393
|
+
interaction,
|
|
1394
|
+
manifestPath: opts.manifest
|
|
1395
|
+
});
|
|
1396
|
+
await shutdownPool();
|
|
1397
|
+
if (opts.json || !isTTY()) {
|
|
1398
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1399
|
+
`);
|
|
1400
|
+
} else {
|
|
1401
|
+
process.stdout.write(`${formatRendersTable(result)}
|
|
1402
|
+
`);
|
|
1403
|
+
}
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
await shutdownPool();
|
|
1406
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1407
|
+
`);
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function createInstrumentCommand() {
|
|
1414
|
+
const instrumentCmd = new commander.Command("instrument").description(
|
|
1415
|
+
"Structured instrumentation commands for React component analysis"
|
|
1416
|
+
);
|
|
1417
|
+
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1418
|
+
return instrumentCmd;
|
|
1419
|
+
}
|
|
610
1420
|
var CONFIG_FILENAMES = [
|
|
611
1421
|
".reactscope/config.json",
|
|
612
1422
|
".reactscope/config.js",
|
|
@@ -724,24 +1534,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
724
1534
|
}
|
|
725
1535
|
|
|
726
1536
|
// src/render-commands.ts
|
|
727
|
-
var
|
|
1537
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
728
1538
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
729
|
-
var
|
|
730
|
-
async function
|
|
731
|
-
if (
|
|
732
|
-
|
|
1539
|
+
var _pool2 = null;
|
|
1540
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1541
|
+
if (_pool2 === null) {
|
|
1542
|
+
_pool2 = new render.BrowserPool({
|
|
733
1543
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
734
1544
|
viewportWidth,
|
|
735
1545
|
viewportHeight
|
|
736
1546
|
});
|
|
737
|
-
await
|
|
1547
|
+
await _pool2.init();
|
|
738
1548
|
}
|
|
739
|
-
return
|
|
1549
|
+
return _pool2;
|
|
740
1550
|
}
|
|
741
|
-
async function
|
|
742
|
-
if (
|
|
743
|
-
await
|
|
744
|
-
|
|
1551
|
+
async function shutdownPool2() {
|
|
1552
|
+
if (_pool2 !== null) {
|
|
1553
|
+
await _pool2.close();
|
|
1554
|
+
_pool2 = null;
|
|
745
1555
|
}
|
|
746
1556
|
}
|
|
747
1557
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -752,7 +1562,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
752
1562
|
_satori: satori,
|
|
753
1563
|
async renderCell(props, _complexityClass) {
|
|
754
1564
|
const startMs = performance.now();
|
|
755
|
-
const pool = await
|
|
1565
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
756
1566
|
const htmlHarness = await buildComponentHarness(
|
|
757
1567
|
filePath,
|
|
758
1568
|
componentName,
|
|
@@ -849,7 +1659,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
849
1659
|
};
|
|
850
1660
|
}
|
|
851
1661
|
function registerRenderSingle(renderCmd) {
|
|
852
|
-
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",
|
|
1662
|
+
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(
|
|
853
1663
|
async (componentName, opts) => {
|
|
854
1664
|
try {
|
|
855
1665
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -888,7 +1698,7 @@ Available: ${available}`
|
|
|
888
1698
|
}
|
|
889
1699
|
}
|
|
890
1700
|
);
|
|
891
|
-
await
|
|
1701
|
+
await shutdownPool2();
|
|
892
1702
|
if (outcome.crashed) {
|
|
893
1703
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
894
1704
|
`);
|
|
@@ -936,7 +1746,7 @@ Available: ${available}`
|
|
|
936
1746
|
);
|
|
937
1747
|
}
|
|
938
1748
|
} catch (err) {
|
|
939
|
-
await
|
|
1749
|
+
await shutdownPool2();
|
|
940
1750
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
941
1751
|
`);
|
|
942
1752
|
process.exit(1);
|
|
@@ -948,7 +1758,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
948
1758
|
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(
|
|
949
1759
|
"--contexts <ids>",
|
|
950
1760
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
951
|
-
).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",
|
|
1761
|
+
).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(
|
|
952
1762
|
async (componentName, opts) => {
|
|
953
1763
|
try {
|
|
954
1764
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1021,7 +1831,7 @@ Available: ${available}`
|
|
|
1021
1831
|
concurrency
|
|
1022
1832
|
});
|
|
1023
1833
|
const result = await matrix.render();
|
|
1024
|
-
await
|
|
1834
|
+
await shutdownPool2();
|
|
1025
1835
|
process.stderr.write(
|
|
1026
1836
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1027
1837
|
`
|
|
@@ -1066,7 +1876,7 @@ Available: ${available}`
|
|
|
1066
1876
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1067
1877
|
}
|
|
1068
1878
|
} catch (err) {
|
|
1069
|
-
await
|
|
1879
|
+
await shutdownPool2();
|
|
1070
1880
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1071
1881
|
`);
|
|
1072
1882
|
process.exit(1);
|
|
@@ -1075,7 +1885,7 @@ Available: ${available}`
|
|
|
1075
1885
|
);
|
|
1076
1886
|
}
|
|
1077
1887
|
function registerRenderAll(renderCmd) {
|
|
1078
|
-
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",
|
|
1888
|
+
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(
|
|
1079
1889
|
async (opts) => {
|
|
1080
1890
|
try {
|
|
1081
1891
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1163,13 +1973,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1163
1973
|
workers.push(worker());
|
|
1164
1974
|
}
|
|
1165
1975
|
await Promise.all(workers);
|
|
1166
|
-
await
|
|
1976
|
+
await shutdownPool2();
|
|
1167
1977
|
process.stderr.write("\n");
|
|
1168
1978
|
const summary = formatSummaryText(results, outputDir);
|
|
1169
1979
|
process.stderr.write(`${summary}
|
|
1170
1980
|
`);
|
|
1171
1981
|
} catch (err) {
|
|
1172
|
-
await
|
|
1982
|
+
await shutdownPool2();
|
|
1173
1983
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1174
1984
|
`);
|
|
1175
1985
|
process.exit(1);
|
|
@@ -1918,13 +2728,17 @@ function createProgram(options = {}) {
|
|
|
1918
2728
|
program.addCommand(createManifestCommand());
|
|
1919
2729
|
program.addCommand(createRenderCommand());
|
|
1920
2730
|
program.addCommand(createTokensCommand());
|
|
2731
|
+
program.addCommand(createInstrumentCommand());
|
|
2732
|
+
program.addCommand(createInitCommand());
|
|
1921
2733
|
return program;
|
|
1922
2734
|
}
|
|
1923
2735
|
|
|
2736
|
+
exports.createInitCommand = createInitCommand;
|
|
1924
2737
|
exports.createManifestCommand = createManifestCommand;
|
|
1925
2738
|
exports.createProgram = createProgram;
|
|
1926
2739
|
exports.createTokensCommand = createTokensCommand;
|
|
1927
2740
|
exports.isTTY = isTTY;
|
|
1928
2741
|
exports.matchGlob = matchGlob;
|
|
2742
|
+
exports.runInit = runInit;
|
|
1929
2743
|
//# sourceMappingURL=index.cjs.map
|
|
1930
2744
|
//# sourceMappingURL=index.cjs.map
|