@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.js CHANGED
@@ -1,7 +1,8 @@
1
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
- import { resolve, dirname } from 'path';
3
- import { generateManifest } from '@agent-scope/manifest';
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/manifest-commands.ts
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 MANIFEST_PATH2 = ".reactscope/manifest.json";
1514
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
706
1515
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
707
- var _pool = null;
708
- async function getPool(viewportWidth, viewportHeight) {
709
- if (_pool === null) {
710
- _pool = new BrowserPool({
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 _pool.init();
1524
+ await _pool2.init();
716
1525
  }
717
- return _pool;
1526
+ return _pool2;
718
1527
  }
719
- async function shutdownPool() {
720
- if (_pool !== null) {
721
- await _pool.close();
722
- _pool = null;
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 getPool(viewportWidth, viewportHeight);
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", MANIFEST_PATH2).action(
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 shutdownPool();
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 shutdownPool();
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", MANIFEST_PATH2).action(
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 shutdownPool();
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 shutdownPool();
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", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
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 shutdownPool();
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 shutdownPool();
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