@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/index.cjs CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
- var manifest = require('@agent-scope/manifest');
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/manifest-commands.ts
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 MANIFEST_PATH2 = ".reactscope/manifest.json";
1537
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
728
1538
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
729
- var _pool = null;
730
- async function getPool(viewportWidth, viewportHeight) {
731
- if (_pool === null) {
732
- _pool = new render.BrowserPool({
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 _pool.init();
1547
+ await _pool2.init();
738
1548
  }
739
- return _pool;
1549
+ return _pool2;
740
1550
  }
741
- async function shutdownPool() {
742
- if (_pool !== null) {
743
- await _pool.close();
744
- _pool = null;
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 getPool(viewportWidth, viewportHeight);
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", MANIFEST_PATH2).action(
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 shutdownPool();
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 shutdownPool();
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", MANIFEST_PATH2).action(
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 shutdownPool();
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 shutdownPool();
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", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
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 shutdownPool();
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 shutdownPool();
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