@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.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync,
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname } from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { generateManifest } from '@agent-scope/manifest';
|
|
5
6
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
6
7
|
import { chromium } from 'playwright';
|
|
7
8
|
import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer, BrowserPool } from '@agent-scope/render';
|
|
@@ -9,7 +10,350 @@ import * as esbuild from 'esbuild';
|
|
|
9
10
|
import { createRequire } from 'module';
|
|
10
11
|
import { TokenResolver, validateTokenFile, TokenValidationError, parseTokenFileSync, TokenParseError } from '@agent-scope/tokens';
|
|
11
12
|
|
|
12
|
-
// src/
|
|
13
|
+
// src/init/index.ts
|
|
14
|
+
function hasConfigFile(dir, stem) {
|
|
15
|
+
if (!existsSync(dir)) return false;
|
|
16
|
+
try {
|
|
17
|
+
const entries = readdirSync(dir);
|
|
18
|
+
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readSafe(path) {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(path, "utf-8");
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function detectFramework(rootDir, packageDeps) {
|
|
31
|
+
if (hasConfigFile(rootDir, "next.config")) return "next";
|
|
32
|
+
if (hasConfigFile(rootDir, "vite.config")) return "vite";
|
|
33
|
+
if (hasConfigFile(rootDir, "remix.config")) return "remix";
|
|
34
|
+
if ("react-scripts" in packageDeps) return "cra";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
function detectPackageManager(rootDir) {
|
|
38
|
+
if (existsSync(join(rootDir, "bun.lock"))) return "bun";
|
|
39
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
40
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
41
|
+
if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
|
|
42
|
+
return "npm";
|
|
43
|
+
}
|
|
44
|
+
function detectTypeScript(rootDir) {
|
|
45
|
+
const candidate = join(rootDir, "tsconfig.json");
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
return { typescript: true, tsconfigPath: candidate };
|
|
48
|
+
}
|
|
49
|
+
return { typescript: false, tsconfigPath: null };
|
|
50
|
+
}
|
|
51
|
+
var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
|
|
52
|
+
var COMPONENT_EXTS = [".tsx", ".jsx"];
|
|
53
|
+
function detectComponentPatterns(rootDir, typescript) {
|
|
54
|
+
const patterns = [];
|
|
55
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
56
|
+
const altExt = typescript ? "jsx" : "jsx";
|
|
57
|
+
for (const dir of COMPONENT_DIRS) {
|
|
58
|
+
const absDir = join(rootDir, dir);
|
|
59
|
+
if (!existsSync(absDir)) continue;
|
|
60
|
+
let hasComponents = false;
|
|
61
|
+
try {
|
|
62
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
63
|
+
hasComponents = entries.some(
|
|
64
|
+
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
65
|
+
);
|
|
66
|
+
if (!hasComponents) {
|
|
67
|
+
hasComponents = entries.some(
|
|
68
|
+
(e) => e.isDirectory() && (() => {
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(join(absDir, e.name)).some(
|
|
71
|
+
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (hasComponents) {
|
|
83
|
+
patterns.push(`${dir}/**/*.${ext}`);
|
|
84
|
+
if (altExt !== ext) {
|
|
85
|
+
patterns.push(`${dir}/**/*.${altExt}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const unique = [...new Set(patterns)];
|
|
90
|
+
if (unique.length === 0) {
|
|
91
|
+
return [`**/*.${ext}`];
|
|
92
|
+
}
|
|
93
|
+
return unique;
|
|
94
|
+
}
|
|
95
|
+
var TAILWIND_STEMS = ["tailwind.config"];
|
|
96
|
+
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
97
|
+
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
98
|
+
var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
|
|
99
|
+
function detectTokenSources(rootDir) {
|
|
100
|
+
const sources = [];
|
|
101
|
+
for (const stem of TAILWIND_STEMS) {
|
|
102
|
+
if (hasConfigFile(rootDir, stem)) {
|
|
103
|
+
try {
|
|
104
|
+
const entries = readdirSync(rootDir);
|
|
105
|
+
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
106
|
+
if (match) {
|
|
107
|
+
sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const srcDir = join(rootDir, "src");
|
|
114
|
+
const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
|
|
115
|
+
for (const scanDir of dirsToScan) {
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(scanDir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
120
|
+
const filePath = join(scanDir, entry.name);
|
|
121
|
+
const content = readSafe(filePath);
|
|
122
|
+
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
123
|
+
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (existsSync(srcDir)) {
|
|
131
|
+
try {
|
|
132
|
+
const entries = readdirSync(srcDir);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
135
|
+
sources.push({ kind: "theme-file", path: join(srcDir, entry) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return sources;
|
|
142
|
+
}
|
|
143
|
+
function detectProject(rootDir) {
|
|
144
|
+
const pkgPath = join(rootDir, "package.json");
|
|
145
|
+
let packageDeps = {};
|
|
146
|
+
const pkgContent = readSafe(pkgPath);
|
|
147
|
+
if (pkgContent !== null) {
|
|
148
|
+
try {
|
|
149
|
+
const pkg = JSON.parse(pkgContent);
|
|
150
|
+
packageDeps = {
|
|
151
|
+
...pkg.dependencies,
|
|
152
|
+
...pkg.devDependencies
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const framework = detectFramework(rootDir, packageDeps);
|
|
158
|
+
const { typescript, tsconfigPath } = detectTypeScript(rootDir);
|
|
159
|
+
const packageManager = detectPackageManager(rootDir);
|
|
160
|
+
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
161
|
+
const tokenSources = detectTokenSources(rootDir);
|
|
162
|
+
return {
|
|
163
|
+
framework,
|
|
164
|
+
typescript,
|
|
165
|
+
tsconfigPath,
|
|
166
|
+
componentPatterns,
|
|
167
|
+
tokenSources,
|
|
168
|
+
packageManager
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
172
|
+
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
173
|
+
return {
|
|
174
|
+
components: {
|
|
175
|
+
include,
|
|
176
|
+
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
177
|
+
wrappers: { providers: [], globalCSS: [] }
|
|
178
|
+
},
|
|
179
|
+
render: {
|
|
180
|
+
viewport: { default: { width: 1280, height: 800 } },
|
|
181
|
+
theme: "light",
|
|
182
|
+
warmBrowser: true
|
|
183
|
+
},
|
|
184
|
+
tokens: {
|
|
185
|
+
file: tokenFile,
|
|
186
|
+
compliance: { threshold: 90 }
|
|
187
|
+
},
|
|
188
|
+
output: {
|
|
189
|
+
dir: outputDir,
|
|
190
|
+
sprites: { format: "png", cellPadding: 8, labelAxes: true },
|
|
191
|
+
json: { pretty: true }
|
|
192
|
+
},
|
|
193
|
+
ci: {
|
|
194
|
+
complianceThreshold: 90,
|
|
195
|
+
failOnA11yViolations: true,
|
|
196
|
+
failOnConsoleErrors: false,
|
|
197
|
+
baselinePath: `${outputDir}baseline/`
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function createRL() {
|
|
202
|
+
return readline.createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function ask(rl, question) {
|
|
208
|
+
return new Promise((resolve6) => {
|
|
209
|
+
rl.question(question, (answer) => {
|
|
210
|
+
resolve6(answer.trim());
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function askWithDefault(rl, label, defaultValue) {
|
|
215
|
+
const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
|
|
216
|
+
return answer.length > 0 ? answer : defaultValue;
|
|
217
|
+
}
|
|
218
|
+
function ensureGitignoreEntry(rootDir, entry) {
|
|
219
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
220
|
+
if (existsSync(gitignorePath)) {
|
|
221
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
222
|
+
const normalised = entry.replace(/\/$/, "");
|
|
223
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
224
|
+
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
228
|
+
appendFileSync(gitignorePath, `${suffix}${entry}
|
|
229
|
+
`);
|
|
230
|
+
} else {
|
|
231
|
+
writeFileSync(gitignorePath, `${entry}
|
|
232
|
+
`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function scaffoldConfig(rootDir, config) {
|
|
236
|
+
const path = join(rootDir, "reactscope.config.json");
|
|
237
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
238
|
+
`);
|
|
239
|
+
return path;
|
|
240
|
+
}
|
|
241
|
+
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
242
|
+
const path = join(rootDir, tokenFile);
|
|
243
|
+
if (!existsSync(path)) {
|
|
244
|
+
const stub = {
|
|
245
|
+
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
246
|
+
tokens: {}
|
|
247
|
+
};
|
|
248
|
+
writeFileSync(path, `${JSON.stringify(stub, null, 2)}
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
return path;
|
|
252
|
+
}
|
|
253
|
+
function scaffoldOutputDir(rootDir, outputDir) {
|
|
254
|
+
const dirPath = join(rootDir, outputDir);
|
|
255
|
+
mkdirSync(dirPath, { recursive: true });
|
|
256
|
+
const keepPath = join(dirPath, ".gitkeep");
|
|
257
|
+
if (!existsSync(keepPath)) {
|
|
258
|
+
writeFileSync(keepPath, "");
|
|
259
|
+
}
|
|
260
|
+
return dirPath;
|
|
261
|
+
}
|
|
262
|
+
async function runInit(options) {
|
|
263
|
+
const rootDir = options.cwd ?? process.cwd();
|
|
264
|
+
const configPath = join(rootDir, "reactscope.config.json");
|
|
265
|
+
const created = [];
|
|
266
|
+
if (existsSync(configPath) && !options.force) {
|
|
267
|
+
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
268
|
+
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
269
|
+
`);
|
|
270
|
+
return { success: false, message: msg, created: [], skipped: true };
|
|
271
|
+
}
|
|
272
|
+
const detected = detectProject(rootDir);
|
|
273
|
+
const defaultTokenFile = "reactscope.tokens.json";
|
|
274
|
+
const defaultOutputDir = ".reactscope/";
|
|
275
|
+
let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
|
|
276
|
+
if (options.yes) {
|
|
277
|
+
process.stdout.write("\n\u{1F50D} Detected project settings:\n");
|
|
278
|
+
process.stdout.write(` Framework : ${detected.framework}
|
|
279
|
+
`);
|
|
280
|
+
process.stdout.write(` TypeScript : ${detected.typescript}
|
|
281
|
+
`);
|
|
282
|
+
process.stdout.write(` Include globs : ${config.components.include.join(", ")}
|
|
283
|
+
`);
|
|
284
|
+
process.stdout.write(` Token file : ${config.tokens.file}
|
|
285
|
+
`);
|
|
286
|
+
process.stdout.write(` Output dir : ${config.output.dir}
|
|
287
|
+
|
|
288
|
+
`);
|
|
289
|
+
} else {
|
|
290
|
+
const rl = createRL();
|
|
291
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
292
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
293
|
+
try {
|
|
294
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
295
|
+
`);
|
|
296
|
+
const includeRaw = await askWithDefault(
|
|
297
|
+
rl,
|
|
298
|
+
"Component include patterns (comma-separated)",
|
|
299
|
+
config.components.include.join(", ")
|
|
300
|
+
);
|
|
301
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
302
|
+
const excludeRaw = await askWithDefault(
|
|
303
|
+
rl,
|
|
304
|
+
"Component exclude patterns (comma-separated)",
|
|
305
|
+
config.components.exclude.join(", ")
|
|
306
|
+
);
|
|
307
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
308
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
309
|
+
config.tokens.file = tokenFile;
|
|
310
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
311
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
312
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
313
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
314
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
315
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
316
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
317
|
+
} finally {
|
|
318
|
+
rl.close();
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write("\n");
|
|
321
|
+
}
|
|
322
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
323
|
+
created.push(cfgPath);
|
|
324
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
325
|
+
created.push(tokPath);
|
|
326
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
327
|
+
created.push(outDirPath);
|
|
328
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
329
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
330
|
+
process.stdout.write(" Created files:\n");
|
|
331
|
+
for (const p of created) {
|
|
332
|
+
process.stdout.write(` ${p}
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
message: "Project initialised successfully.",
|
|
339
|
+
created,
|
|
340
|
+
skipped: false
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function createInitCommand() {
|
|
344
|
+
return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
345
|
+
try {
|
|
346
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
347
|
+
if (!result.success && !result.skipped) {
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
352
|
+
`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
13
357
|
|
|
14
358
|
// src/manifest-formatter.ts
|
|
15
359
|
function isTTY() {
|
|
@@ -585,6 +929,471 @@ function csvEscape(value) {
|
|
|
585
929
|
}
|
|
586
930
|
return value;
|
|
587
931
|
}
|
|
932
|
+
|
|
933
|
+
// src/instrument/renders.ts
|
|
934
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
935
|
+
function determineTrigger(event) {
|
|
936
|
+
if (event.forceUpdate) return "force_update";
|
|
937
|
+
if (event.stateChanged) return "state_change";
|
|
938
|
+
if (event.propsChanged) return "props_change";
|
|
939
|
+
if (event.contextChanged) return "context_change";
|
|
940
|
+
if (event.hookDepsChanged) return "hook_dependency";
|
|
941
|
+
return "parent_rerender";
|
|
942
|
+
}
|
|
943
|
+
function isWastedRender(event) {
|
|
944
|
+
return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
|
|
945
|
+
}
|
|
946
|
+
function buildCausalityChains(rawEvents) {
|
|
947
|
+
const result = [];
|
|
948
|
+
const componentLastRender = /* @__PURE__ */ new Map();
|
|
949
|
+
for (const raw of rawEvents) {
|
|
950
|
+
const trigger = determineTrigger(raw);
|
|
951
|
+
const wasted = isWastedRender(raw);
|
|
952
|
+
const chain = [];
|
|
953
|
+
let current = raw;
|
|
954
|
+
const visited = /* @__PURE__ */ new Set();
|
|
955
|
+
while (true) {
|
|
956
|
+
if (visited.has(current.component)) break;
|
|
957
|
+
visited.add(current.component);
|
|
958
|
+
const currentTrigger = determineTrigger(current);
|
|
959
|
+
chain.unshift({
|
|
960
|
+
component: current.component,
|
|
961
|
+
trigger: currentTrigger,
|
|
962
|
+
propsChanged: current.propsChanged,
|
|
963
|
+
stateChanged: current.stateChanged,
|
|
964
|
+
contextChanged: current.contextChanged
|
|
965
|
+
});
|
|
966
|
+
if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
const parentEvent = componentLastRender.get(current.parentComponent);
|
|
970
|
+
if (parentEvent === void 0) break;
|
|
971
|
+
current = parentEvent;
|
|
972
|
+
}
|
|
973
|
+
const rootCause = chain[0];
|
|
974
|
+
const cascadeRenders = rawEvents.filter((e) => {
|
|
975
|
+
if (rootCause === void 0) return false;
|
|
976
|
+
return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
|
|
977
|
+
});
|
|
978
|
+
result.push({
|
|
979
|
+
component: raw.component,
|
|
980
|
+
renderIndex: raw.renderIndex,
|
|
981
|
+
trigger,
|
|
982
|
+
propsChanged: raw.propsChanged,
|
|
983
|
+
stateChanged: raw.stateChanged,
|
|
984
|
+
contextChanged: raw.contextChanged,
|
|
985
|
+
memoized: raw.memoized,
|
|
986
|
+
wasted,
|
|
987
|
+
chain,
|
|
988
|
+
cascade: {
|
|
989
|
+
totalRendersTriggered: cascadeRenders.length,
|
|
990
|
+
uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
|
|
991
|
+
unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
componentLastRender.set(raw.component, raw);
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
}
|
|
998
|
+
function applyHeuristicFlags(renders) {
|
|
999
|
+
const flags = [];
|
|
1000
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
1001
|
+
for (const r of renders) {
|
|
1002
|
+
if (!byComponent.has(r.component)) byComponent.set(r.component, []);
|
|
1003
|
+
byComponent.get(r.component).push(r);
|
|
1004
|
+
}
|
|
1005
|
+
for (const [component, events] of byComponent) {
|
|
1006
|
+
const wastedCount = events.filter((e) => e.wasted).length;
|
|
1007
|
+
const totalCount = events.length;
|
|
1008
|
+
if (wastedCount > 0) {
|
|
1009
|
+
flags.push({
|
|
1010
|
+
id: "WASTED_RENDER",
|
|
1011
|
+
severity: "warning",
|
|
1012
|
+
component,
|
|
1013
|
+
detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
|
|
1014
|
+
data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
for (const event of events) {
|
|
1018
|
+
if (event.cascade.totalRendersTriggered > 50) {
|
|
1019
|
+
flags.push({
|
|
1020
|
+
id: "RENDER_CASCADE",
|
|
1021
|
+
severity: "warning",
|
|
1022
|
+
component,
|
|
1023
|
+
detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
|
|
1024
|
+
data: {
|
|
1025
|
+
totalRendersTriggered: event.cascade.totalRendersTriggered,
|
|
1026
|
+
uniqueComponents: event.cascade.uniqueComponents,
|
|
1027
|
+
unchangedPropRenders: event.cascade.unchangedPropRenders
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return flags;
|
|
1035
|
+
}
|
|
1036
|
+
function buildInstrumentationScript() {
|
|
1037
|
+
return (
|
|
1038
|
+
/* js */
|
|
1039
|
+
`
|
|
1040
|
+
(function installScopeRenderInstrumentation() {
|
|
1041
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1042
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1043
|
+
|
|
1044
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1045
|
+
if (!hook) return;
|
|
1046
|
+
|
|
1047
|
+
var originalOnCommit = hook.onCommitFiberRoot;
|
|
1048
|
+
var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
|
|
1049
|
+
|
|
1050
|
+
function extractName(fiber) {
|
|
1051
|
+
if (!fiber) return 'Unknown';
|
|
1052
|
+
var type = fiber.type;
|
|
1053
|
+
if (!type) return 'Unknown';
|
|
1054
|
+
if (typeof type === 'string') return type; // host element
|
|
1055
|
+
if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
|
|
1056
|
+
if (type.displayName) return type.displayName;
|
|
1057
|
+
if (type.render && typeof type.render === 'function') {
|
|
1058
|
+
return type.render.displayName || type.render.name || 'Anonymous';
|
|
1059
|
+
}
|
|
1060
|
+
return 'Anonymous';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function isMemoized(fiber) {
|
|
1064
|
+
// MemoComponent = 14, SimpleMemoComponent = 15
|
|
1065
|
+
return fiber.tag === 14 || fiber.tag === 15;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function isComponent(fiber) {
|
|
1069
|
+
// FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
|
|
1070
|
+
return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function shallowEqual(a, b) {
|
|
1074
|
+
if (a === b) return true;
|
|
1075
|
+
if (!a || !b) return a === b;
|
|
1076
|
+
var keysA = Object.keys(a);
|
|
1077
|
+
var keysB = Object.keys(b);
|
|
1078
|
+
if (keysA.length !== keysB.length) return false;
|
|
1079
|
+
for (var i = 0; i < keysA.length; i++) {
|
|
1080
|
+
var k = keysA[i];
|
|
1081
|
+
if (k === 'children') continue; // ignore children prop
|
|
1082
|
+
if (a[k] !== b[k]) return false;
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function getParentComponentName(fiber) {
|
|
1088
|
+
var parent = fiber.return;
|
|
1089
|
+
while (parent) {
|
|
1090
|
+
if (isComponent(parent)) {
|
|
1091
|
+
var name = extractName(parent);
|
|
1092
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
|
|
1093
|
+
}
|
|
1094
|
+
parent = parent.return;
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function walkCommit(fiber) {
|
|
1100
|
+
if (!fiber) return;
|
|
1101
|
+
|
|
1102
|
+
if (isComponent(fiber)) {
|
|
1103
|
+
var name = extractName(fiber);
|
|
1104
|
+
if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
|
|
1105
|
+
var memoized = isMemoized(fiber);
|
|
1106
|
+
var currentProps = fiber.memoizedProps || {};
|
|
1107
|
+
var prev = renderedComponents.get(name);
|
|
1108
|
+
|
|
1109
|
+
var propsChanged = true;
|
|
1110
|
+
var stateChanged = false;
|
|
1111
|
+
var contextChanged = false;
|
|
1112
|
+
var hookDepsChanged = false;
|
|
1113
|
+
var forceUpdate = false;
|
|
1114
|
+
|
|
1115
|
+
if (prev) {
|
|
1116
|
+
propsChanged = !shallowEqual(prev.lastProps, currentProps);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// State: check memoizedState chain
|
|
1120
|
+
var memoizedState = fiber.memoizedState;
|
|
1121
|
+
if (prev && prev.lastStateSerialized !== undefined) {
|
|
1122
|
+
try {
|
|
1123
|
+
var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1124
|
+
stateChanged = stateSig !== prev.lastStateSerialized;
|
|
1125
|
+
} catch (_) {
|
|
1126
|
+
stateChanged = false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Context: use _debugHookTypes or check dependencies
|
|
1131
|
+
var deps = fiber.dependencies;
|
|
1132
|
+
if (deps && deps.firstContext) {
|
|
1133
|
+
contextChanged = true; // conservative: context dep present = may have changed
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
var stateSig;
|
|
1137
|
+
try {
|
|
1138
|
+
stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
|
|
1139
|
+
} catch (_) {
|
|
1140
|
+
stateSig = null;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
renderedComponents.set(name, {
|
|
1144
|
+
lastProps: currentProps,
|
|
1145
|
+
lastStateSerialized: stateSig,
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
var parentName = getParentComponentName(fiber);
|
|
1149
|
+
|
|
1150
|
+
window.__SCOPE_RENDER_EVENTS__.push({
|
|
1151
|
+
component: name,
|
|
1152
|
+
renderIndex: window.__SCOPE_RENDER_INDEX__++,
|
|
1153
|
+
propsChanged: prev ? propsChanged : false,
|
|
1154
|
+
stateChanged: stateChanged,
|
|
1155
|
+
contextChanged: contextChanged,
|
|
1156
|
+
memoized: memoized,
|
|
1157
|
+
parentComponent: parentName,
|
|
1158
|
+
hookDepsChanged: hookDepsChanged,
|
|
1159
|
+
forceUpdate: forceUpdate,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
walkCommit(fiber.child);
|
|
1165
|
+
walkCommit(fiber.sibling);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1169
|
+
if (typeof originalOnCommit === 'function') {
|
|
1170
|
+
originalOnCommit.call(hook, rendererID, root, priorityLevel);
|
|
1171
|
+
}
|
|
1172
|
+
var wipRoot = root && root.current && root.current.alternate;
|
|
1173
|
+
if (wipRoot) walkCommit(wipRoot);
|
|
1174
|
+
};
|
|
1175
|
+
})();
|
|
1176
|
+
`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
async function replayInteraction(page, steps) {
|
|
1180
|
+
for (const step of steps) {
|
|
1181
|
+
switch (step.action) {
|
|
1182
|
+
case "click":
|
|
1183
|
+
if (step.target !== void 0) {
|
|
1184
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
case "type":
|
|
1189
|
+
if (step.target !== void 0 && step.text !== void 0) {
|
|
1190
|
+
await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
|
|
1191
|
+
await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
break;
|
|
1196
|
+
case "hover":
|
|
1197
|
+
if (step.target !== void 0) {
|
|
1198
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
break;
|
|
1202
|
+
case "blur":
|
|
1203
|
+
if (step.target !== void 0) {
|
|
1204
|
+
await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
case "focus":
|
|
1209
|
+
if (step.target !== void 0) {
|
|
1210
|
+
await page.focus(step.target, { timeout: 5e3 }).catch(() => {
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
break;
|
|
1214
|
+
case "scroll":
|
|
1215
|
+
if (step.target !== void 0) {
|
|
1216
|
+
await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
break;
|
|
1220
|
+
case "wait": {
|
|
1221
|
+
const timeout = step.timeout ?? 1e3;
|
|
1222
|
+
if (step.condition === "idle") {
|
|
1223
|
+
await page.waitForLoadState("networkidle", { timeout }).catch(() => {
|
|
1224
|
+
});
|
|
1225
|
+
} else {
|
|
1226
|
+
await page.waitForTimeout(timeout);
|
|
1227
|
+
}
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
var _pool = null;
|
|
1234
|
+
async function getPool() {
|
|
1235
|
+
if (_pool === null) {
|
|
1236
|
+
_pool = new BrowserPool({
|
|
1237
|
+
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1238
|
+
viewportWidth: 1280,
|
|
1239
|
+
viewportHeight: 800
|
|
1240
|
+
});
|
|
1241
|
+
await _pool.init();
|
|
1242
|
+
}
|
|
1243
|
+
return _pool;
|
|
1244
|
+
}
|
|
1245
|
+
async function shutdownPool() {
|
|
1246
|
+
if (_pool !== null) {
|
|
1247
|
+
await _pool.close();
|
|
1248
|
+
_pool = null;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function analyzeRenders(options) {
|
|
1252
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
|
|
1253
|
+
const manifest = loadManifest(manifestPath);
|
|
1254
|
+
const descriptor = manifest.components[options.componentName];
|
|
1255
|
+
if (descriptor === void 0) {
|
|
1256
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Component "${options.componentName}" not found in manifest.
|
|
1259
|
+
Available: ${available}`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
const rootDir = process.cwd();
|
|
1263
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
1264
|
+
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1265
|
+
const pool = await getPool();
|
|
1266
|
+
const slot = await pool.acquire();
|
|
1267
|
+
const { page } = slot;
|
|
1268
|
+
const startMs = performance.now();
|
|
1269
|
+
try {
|
|
1270
|
+
await page.addInitScript(buildInstrumentationScript());
|
|
1271
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1272
|
+
await page.waitForFunction(
|
|
1273
|
+
() => window.__SCOPE_RENDER_COMPLETE__ === true,
|
|
1274
|
+
{ timeout: 15e3 }
|
|
1275
|
+
);
|
|
1276
|
+
await page.waitForTimeout(100);
|
|
1277
|
+
await page.evaluate(() => {
|
|
1278
|
+
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1279
|
+
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1280
|
+
});
|
|
1281
|
+
await replayInteraction(page, options.interaction);
|
|
1282
|
+
await page.waitForTimeout(200);
|
|
1283
|
+
const interactionDurationMs = performance.now() - startMs;
|
|
1284
|
+
const rawEvents = await page.evaluate(() => {
|
|
1285
|
+
return window.__SCOPE_RENDER_EVENTS__ ?? [];
|
|
1286
|
+
});
|
|
1287
|
+
const renders = buildCausalityChains(rawEvents);
|
|
1288
|
+
const flags = applyHeuristicFlags(renders);
|
|
1289
|
+
const uniqueComponents = new Set(renders.map((r) => r.component)).size;
|
|
1290
|
+
const wastedRenders = renders.filter((r) => r.wasted).length;
|
|
1291
|
+
return {
|
|
1292
|
+
component: options.componentName,
|
|
1293
|
+
interaction: options.interaction,
|
|
1294
|
+
summary: {
|
|
1295
|
+
totalRenders: renders.length,
|
|
1296
|
+
uniqueComponents,
|
|
1297
|
+
wastedRenders,
|
|
1298
|
+
interactionDurationMs: Math.round(interactionDurationMs)
|
|
1299
|
+
},
|
|
1300
|
+
renders,
|
|
1301
|
+
flags
|
|
1302
|
+
};
|
|
1303
|
+
} finally {
|
|
1304
|
+
pool.release(slot);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function formatRendersTable(result) {
|
|
1308
|
+
const lines = [];
|
|
1309
|
+
lines.push(`
|
|
1310
|
+
\u{1F50D} Re-render Analysis: ${result.component}`);
|
|
1311
|
+
lines.push(`${"\u2500".repeat(60)}`);
|
|
1312
|
+
lines.push(`Total renders: ${result.summary.totalRenders}`);
|
|
1313
|
+
lines.push(`Unique components: ${result.summary.uniqueComponents}`);
|
|
1314
|
+
lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
|
|
1315
|
+
lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
|
|
1316
|
+
lines.push("");
|
|
1317
|
+
if (result.renders.length === 0) {
|
|
1318
|
+
lines.push("No re-renders captured during interaction.");
|
|
1319
|
+
} else {
|
|
1320
|
+
lines.push("Re-renders:");
|
|
1321
|
+
lines.push(
|
|
1322
|
+
`${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
|
|
1323
|
+
);
|
|
1324
|
+
lines.push("\u2500".repeat(80));
|
|
1325
|
+
for (const r of result.renders) {
|
|
1326
|
+
const wasted = r.wasted ? "\u26A0 yes" : "no";
|
|
1327
|
+
const idx = String(r.renderIndex).padEnd(4);
|
|
1328
|
+
const comp = r.component.slice(0, 29).padEnd(30);
|
|
1329
|
+
const trig = r.trigger.padEnd(18);
|
|
1330
|
+
const w = wasted.padEnd(7);
|
|
1331
|
+
const depth = r.chain.length;
|
|
1332
|
+
lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (result.flags.length > 0) {
|
|
1336
|
+
lines.push("");
|
|
1337
|
+
lines.push("Flags:");
|
|
1338
|
+
for (const flag of result.flags) {
|
|
1339
|
+
const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
|
|
1340
|
+
lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return lines.join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
function createInstrumentRendersCommand() {
|
|
1346
|
+
return new 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(
|
|
1347
|
+
"--interaction <json>",
|
|
1348
|
+
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1349
|
+
"[]"
|
|
1350
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
|
|
1351
|
+
async (componentName, opts) => {
|
|
1352
|
+
let interaction = [];
|
|
1353
|
+
try {
|
|
1354
|
+
interaction = JSON.parse(opts.interaction);
|
|
1355
|
+
if (!Array.isArray(interaction)) {
|
|
1356
|
+
throw new Error("Interaction must be a JSON array");
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
|
|
1360
|
+
`);
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
process.stderr.write(
|
|
1365
|
+
`Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
|
|
1366
|
+
`
|
|
1367
|
+
);
|
|
1368
|
+
const result = await analyzeRenders({
|
|
1369
|
+
componentName,
|
|
1370
|
+
interaction,
|
|
1371
|
+
manifestPath: opts.manifest
|
|
1372
|
+
});
|
|
1373
|
+
await shutdownPool();
|
|
1374
|
+
if (opts.json || !isTTY()) {
|
|
1375
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1376
|
+
`);
|
|
1377
|
+
} else {
|
|
1378
|
+
process.stdout.write(`${formatRendersTable(result)}
|
|
1379
|
+
`);
|
|
1380
|
+
}
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
await shutdownPool();
|
|
1383
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1384
|
+
`);
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
function createInstrumentCommand() {
|
|
1391
|
+
const instrumentCmd = new Command("instrument").description(
|
|
1392
|
+
"Structured instrumentation commands for React component analysis"
|
|
1393
|
+
);
|
|
1394
|
+
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1395
|
+
return instrumentCmd;
|
|
1396
|
+
}
|
|
588
1397
|
var CONFIG_FILENAMES = [
|
|
589
1398
|
".reactscope/config.json",
|
|
590
1399
|
".reactscope/config.js",
|
|
@@ -702,24 +1511,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
702
1511
|
}
|
|
703
1512
|
|
|
704
1513
|
// src/render-commands.ts
|
|
705
|
-
var
|
|
1514
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
706
1515
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
707
|
-
var
|
|
708
|
-
async function
|
|
709
|
-
if (
|
|
710
|
-
|
|
1516
|
+
var _pool2 = null;
|
|
1517
|
+
async function getPool2(viewportWidth, viewportHeight) {
|
|
1518
|
+
if (_pool2 === null) {
|
|
1519
|
+
_pool2 = new BrowserPool({
|
|
711
1520
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
712
1521
|
viewportWidth,
|
|
713
1522
|
viewportHeight
|
|
714
1523
|
});
|
|
715
|
-
await
|
|
1524
|
+
await _pool2.init();
|
|
716
1525
|
}
|
|
717
|
-
return
|
|
1526
|
+
return _pool2;
|
|
718
1527
|
}
|
|
719
|
-
async function
|
|
720
|
-
if (
|
|
721
|
-
await
|
|
722
|
-
|
|
1528
|
+
async function shutdownPool2() {
|
|
1529
|
+
if (_pool2 !== null) {
|
|
1530
|
+
await _pool2.close();
|
|
1531
|
+
_pool2 = null;
|
|
723
1532
|
}
|
|
724
1533
|
}
|
|
725
1534
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -730,7 +1539,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
730
1539
|
_satori: satori,
|
|
731
1540
|
async renderCell(props, _complexityClass) {
|
|
732
1541
|
const startMs = performance.now();
|
|
733
|
-
const pool = await
|
|
1542
|
+
const pool = await getPool2(viewportWidth, viewportHeight);
|
|
734
1543
|
const htmlHarness = await buildComponentHarness(
|
|
735
1544
|
filePath,
|
|
736
1545
|
componentName,
|
|
@@ -827,7 +1636,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
827
1636
|
};
|
|
828
1637
|
}
|
|
829
1638
|
function registerRenderSingle(renderCmd) {
|
|
830
|
-
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",
|
|
1639
|
+
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(
|
|
831
1640
|
async (componentName, opts) => {
|
|
832
1641
|
try {
|
|
833
1642
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -866,7 +1675,7 @@ Available: ${available}`
|
|
|
866
1675
|
}
|
|
867
1676
|
}
|
|
868
1677
|
);
|
|
869
|
-
await
|
|
1678
|
+
await shutdownPool2();
|
|
870
1679
|
if (outcome.crashed) {
|
|
871
1680
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
872
1681
|
`);
|
|
@@ -914,7 +1723,7 @@ Available: ${available}`
|
|
|
914
1723
|
);
|
|
915
1724
|
}
|
|
916
1725
|
} catch (err) {
|
|
917
|
-
await
|
|
1726
|
+
await shutdownPool2();
|
|
918
1727
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
919
1728
|
`);
|
|
920
1729
|
process.exit(1);
|
|
@@ -926,7 +1735,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
926
1735
|
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(
|
|
927
1736
|
"--contexts <ids>",
|
|
928
1737
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
929
|
-
).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",
|
|
1738
|
+
).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(
|
|
930
1739
|
async (componentName, opts) => {
|
|
931
1740
|
try {
|
|
932
1741
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -999,7 +1808,7 @@ Available: ${available}`
|
|
|
999
1808
|
concurrency
|
|
1000
1809
|
});
|
|
1001
1810
|
const result = await matrix.render();
|
|
1002
|
-
await
|
|
1811
|
+
await shutdownPool2();
|
|
1003
1812
|
process.stderr.write(
|
|
1004
1813
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1005
1814
|
`
|
|
@@ -1044,7 +1853,7 @@ Available: ${available}`
|
|
|
1044
1853
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1045
1854
|
}
|
|
1046
1855
|
} catch (err) {
|
|
1047
|
-
await
|
|
1856
|
+
await shutdownPool2();
|
|
1048
1857
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1049
1858
|
`);
|
|
1050
1859
|
process.exit(1);
|
|
@@ -1053,7 +1862,7 @@ Available: ${available}`
|
|
|
1053
1862
|
);
|
|
1054
1863
|
}
|
|
1055
1864
|
function registerRenderAll(renderCmd) {
|
|
1056
|
-
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",
|
|
1865
|
+
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(
|
|
1057
1866
|
async (opts) => {
|
|
1058
1867
|
try {
|
|
1059
1868
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1141,13 +1950,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1141
1950
|
workers.push(worker());
|
|
1142
1951
|
}
|
|
1143
1952
|
await Promise.all(workers);
|
|
1144
|
-
await
|
|
1953
|
+
await shutdownPool2();
|
|
1145
1954
|
process.stderr.write("\n");
|
|
1146
1955
|
const summary = formatSummaryText(results, outputDir);
|
|
1147
1956
|
process.stderr.write(`${summary}
|
|
1148
1957
|
`);
|
|
1149
1958
|
} catch (err) {
|
|
1150
|
-
await
|
|
1959
|
+
await shutdownPool2();
|
|
1151
1960
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1152
1961
|
`);
|
|
1153
1962
|
process.exit(1);
|
|
@@ -1896,9 +2705,11 @@ function createProgram(options = {}) {
|
|
|
1896
2705
|
program.addCommand(createManifestCommand());
|
|
1897
2706
|
program.addCommand(createRenderCommand());
|
|
1898
2707
|
program.addCommand(createTokensCommand());
|
|
2708
|
+
program.addCommand(createInstrumentCommand());
|
|
2709
|
+
program.addCommand(createInitCommand());
|
|
1899
2710
|
return program;
|
|
1900
2711
|
}
|
|
1901
2712
|
|
|
1902
|
-
export { createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
|
|
2713
|
+
export { createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
|
|
1903
2714
|
//# sourceMappingURL=index.js.map
|
|
1904
2715
|
//# sourceMappingURL=index.js.map
|