@agent-scope/cli 1.10.0 → 1.11.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
@@ -4,11 +4,11 @@ var fs = require('fs');
4
4
  var path = require('path');
5
5
  var readline = require('readline');
6
6
  var commander = require('commander');
7
- var manifest = require('@agent-scope/manifest');
8
- var playwright = require('@agent-scope/playwright');
9
- var playwright$1 = require('playwright');
10
7
  var render = require('@agent-scope/render');
11
8
  var esbuild = require('esbuild');
9
+ var manifest = require('@agent-scope/manifest');
10
+ var playwright$1 = require('playwright');
11
+ var playwright = require('@agent-scope/playwright');
12
12
  var module$1 = require('module');
13
13
  var tokens = require('@agent-scope/tokens');
14
14
 
@@ -228,9 +228,9 @@ function createRL() {
228
228
  });
229
229
  }
230
230
  async function ask(rl, question) {
231
- return new Promise((resolve7) => {
231
+ return new Promise((resolve10) => {
232
232
  rl.question(question, (answer) => {
233
- resolve7(answer.trim());
233
+ resolve10(answer.trim());
234
234
  });
235
235
  });
236
236
  }
@@ -377,6 +377,114 @@ function createInitCommand() {
377
377
  }
378
378
  });
379
379
  }
380
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
381
+ const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
382
+ return wrapInHtml(bundledScript, viewportWidth);
383
+ }
384
+ async function bundleComponentToIIFE(filePath, componentName, props) {
385
+ const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
386
+ const wrapperCode = (
387
+ /* ts */
388
+ `
389
+ import * as __scopeMod from ${JSON.stringify(filePath)};
390
+ import { createRoot } from "react-dom/client";
391
+ import { createElement } from "react";
392
+
393
+ (function scopeRenderHarness() {
394
+ var Component =
395
+ __scopeMod["default"] ||
396
+ __scopeMod[${JSON.stringify(componentName)}] ||
397
+ (Object.values(__scopeMod).find(
398
+ function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
399
+ ));
400
+
401
+ if (!Component) {
402
+ window.__SCOPE_RENDER_ERROR__ =
403
+ "No renderable component found. Checked: default, " +
404
+ ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
405
+ "Available exports: " + Object.keys(__scopeMod).join(", ");
406
+ window.__SCOPE_RENDER_COMPLETE__ = true;
407
+ return;
408
+ }
409
+
410
+ try {
411
+ var props = ${propsJson};
412
+ var rootEl = document.getElementById("scope-root");
413
+ if (!rootEl) {
414
+ window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
415
+ window.__SCOPE_RENDER_COMPLETE__ = true;
416
+ return;
417
+ }
418
+ createRoot(rootEl).render(createElement(Component, props));
419
+ // Use requestAnimationFrame to let React flush the render
420
+ requestAnimationFrame(function() {
421
+ window.__SCOPE_RENDER_COMPLETE__ = true;
422
+ });
423
+ } catch (err) {
424
+ window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
425
+ window.__SCOPE_RENDER_COMPLETE__ = true;
426
+ }
427
+ })();
428
+ `
429
+ );
430
+ const result = await esbuild__namespace.build({
431
+ stdin: {
432
+ contents: wrapperCode,
433
+ // Resolve relative imports (within the component's dir)
434
+ resolveDir: path.dirname(filePath),
435
+ loader: "tsx",
436
+ sourcefile: "__scope_harness__.tsx"
437
+ },
438
+ bundle: true,
439
+ format: "iife",
440
+ write: false,
441
+ platform: "browser",
442
+ jsx: "automatic",
443
+ jsxImportSource: "react",
444
+ target: "es2020",
445
+ // Bundle everything — no externals
446
+ external: [],
447
+ define: {
448
+ "process.env.NODE_ENV": '"development"',
449
+ global: "globalThis"
450
+ },
451
+ logLevel: "silent",
452
+ // Suppress "React must be in scope" warnings from old JSX (we use automatic)
453
+ banner: {
454
+ js: "/* @agent-scope/cli component harness */"
455
+ }
456
+ });
457
+ if (result.errors.length > 0) {
458
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
459
+ throw new Error(`esbuild failed to bundle component:
460
+ ${msg}`);
461
+ }
462
+ const outputFile = result.outputFiles?.[0];
463
+ if (outputFile === void 0 || outputFile.text.length === 0) {
464
+ throw new Error("esbuild produced no output");
465
+ }
466
+ return outputFile.text;
467
+ }
468
+ function wrapInHtml(bundledScript, viewportWidth, projectCss) {
469
+ const projectStyleBlock = "";
470
+ return `<!DOCTYPE html>
471
+ <html lang="en">
472
+ <head>
473
+ <meta charset="UTF-8" />
474
+ <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
475
+ <style>
476
+ *, *::before, *::after { box-sizing: border-box; }
477
+ html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
478
+ #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
479
+ </style>
480
+ ${projectStyleBlock}
481
+ </head>
482
+ <body>
483
+ <div id="scope-root" data-reactscope-root></div>
484
+ <script>${bundledScript}</script>
485
+ </body>
486
+ </html>`;
487
+ }
380
488
 
381
489
  // src/manifest-formatter.ts
382
490
  function isTTY() {
@@ -644,154 +752,6 @@ function createManifestCommand() {
644
752
  registerGenerate(manifestCmd);
645
753
  return manifestCmd;
646
754
  }
647
- async function browserCapture(options) {
648
- const { url, timeout = 1e4, wait = 0 } = options;
649
- const browser = await playwright$1.chromium.launch({ headless: true });
650
- try {
651
- const context = await browser.newContext();
652
- const page = await context.newPage();
653
- await page.addInitScript({ content: playwright.getBrowserEntryScript() });
654
- await page.goto(url, {
655
- waitUntil: "networkidle",
656
- timeout: timeout + 5e3
657
- });
658
- await page.waitForFunction(
659
- () => {
660
- return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
661
- },
662
- { timeout }
663
- );
664
- if (wait > 0) {
665
- await page.waitForTimeout(wait);
666
- }
667
- const raw = await page.evaluate(async () => {
668
- const win = window;
669
- if (typeof win.__SCOPE_CAPTURE__ !== "function") {
670
- throw new Error("Scope runtime not injected");
671
- }
672
- return win.__SCOPE_CAPTURE__();
673
- });
674
- if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
675
- throw new Error(`Scope capture failed: ${raw.error}`);
676
- }
677
- const report = { ...raw, route: null };
678
- return { report };
679
- } finally {
680
- await browser.close();
681
- }
682
- }
683
- function writeReportToFile(report, outputPath, pretty) {
684
- const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
685
- fs.writeFileSync(outputPath, json, "utf-8");
686
- }
687
- async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
688
- const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
689
- return wrapInHtml(bundledScript, viewportWidth);
690
- }
691
- async function bundleComponentToIIFE(filePath, componentName, props) {
692
- const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
693
- const wrapperCode = (
694
- /* ts */
695
- `
696
- import * as __scopeMod from ${JSON.stringify(filePath)};
697
- import { createRoot } from "react-dom/client";
698
- import { createElement } from "react";
699
-
700
- (function scopeRenderHarness() {
701
- var Component =
702
- __scopeMod["default"] ||
703
- __scopeMod[${JSON.stringify(componentName)}] ||
704
- (Object.values(__scopeMod).find(
705
- function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
706
- ));
707
-
708
- if (!Component) {
709
- window.__SCOPE_RENDER_ERROR__ =
710
- "No renderable component found. Checked: default, " +
711
- ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
712
- "Available exports: " + Object.keys(__scopeMod).join(", ");
713
- window.__SCOPE_RENDER_COMPLETE__ = true;
714
- return;
715
- }
716
-
717
- try {
718
- var props = ${propsJson};
719
- var rootEl = document.getElementById("scope-root");
720
- if (!rootEl) {
721
- window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
722
- window.__SCOPE_RENDER_COMPLETE__ = true;
723
- return;
724
- }
725
- createRoot(rootEl).render(createElement(Component, props));
726
- // Use requestAnimationFrame to let React flush the render
727
- requestAnimationFrame(function() {
728
- window.__SCOPE_RENDER_COMPLETE__ = true;
729
- });
730
- } catch (err) {
731
- window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
732
- window.__SCOPE_RENDER_COMPLETE__ = true;
733
- }
734
- })();
735
- `
736
- );
737
- const result = await esbuild__namespace.build({
738
- stdin: {
739
- contents: wrapperCode,
740
- // Resolve relative imports (within the component's dir)
741
- resolveDir: path.dirname(filePath),
742
- loader: "tsx",
743
- sourcefile: "__scope_harness__.tsx"
744
- },
745
- bundle: true,
746
- format: "iife",
747
- write: false,
748
- platform: "browser",
749
- jsx: "automatic",
750
- jsxImportSource: "react",
751
- target: "es2020",
752
- // Bundle everything — no externals
753
- external: [],
754
- define: {
755
- "process.env.NODE_ENV": '"development"',
756
- global: "globalThis"
757
- },
758
- logLevel: "silent",
759
- // Suppress "React must be in scope" warnings from old JSX (we use automatic)
760
- banner: {
761
- js: "/* @agent-scope/cli component harness */"
762
- }
763
- });
764
- if (result.errors.length > 0) {
765
- const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
766
- throw new Error(`esbuild failed to bundle component:
767
- ${msg}`);
768
- }
769
- const outputFile = result.outputFiles?.[0];
770
- if (outputFile === void 0 || outputFile.text.length === 0) {
771
- throw new Error("esbuild produced no output");
772
- }
773
- return outputFile.text;
774
- }
775
- function wrapInHtml(bundledScript, viewportWidth, projectCss) {
776
- const projectStyleBlock = "";
777
- return `<!DOCTYPE html>
778
- <html lang="en">
779
- <head>
780
- <meta charset="UTF-8" />
781
- <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
782
- <style>
783
- *, *::before, *::after { box-sizing: border-box; }
784
- html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
785
- #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
786
- </style>
787
- ${projectStyleBlock}
788
- </head>
789
- <body>
790
- <div id="scope-root" data-reactscope-root></div>
791
- <script>${bundledScript}</script>
792
- </body>
793
- </html>`;
794
- }
795
755
 
796
756
  // src/render-formatter.ts
797
757
  function parseViewport(spec) {
@@ -898,63 +858,698 @@ function formatMatrixCsv(componentName, result) {
898
858
  const val = axis.values[axisIdx];
899
859
  return val !== void 0 ? csvEscape(String(val)) : "";
900
860
  });
901
- return [
902
- csvEscape(componentName),
903
- ...axisVals,
904
- cell.result.renderTimeMs.toFixed(3),
905
- String(cell.result.width),
906
- String(cell.result.height)
907
- ].join(",");
908
- });
909
- return `${[headers.join(","), ...rows].join("\n")}
861
+ return [
862
+ csvEscape(componentName),
863
+ ...axisVals,
864
+ cell.result.renderTimeMs.toFixed(3),
865
+ String(cell.result.width),
866
+ String(cell.result.height)
867
+ ].join(",");
868
+ });
869
+ return `${[headers.join(","), ...rows].join("\n")}
870
+ `;
871
+ }
872
+ function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
873
+ const filled = Math.round(pct / 100 * barWidth);
874
+ const empty = barWidth - filled;
875
+ const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
876
+ const nameSlice = currentName.slice(0, 25).padEnd(25);
877
+ return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
878
+ }
879
+ function formatSummaryText(results, outputDir) {
880
+ const total = results.length;
881
+ const passed = results.filter((r) => r.success).length;
882
+ const failed = total - passed;
883
+ const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
884
+ const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
885
+ const lines = [
886
+ "\u2500".repeat(60),
887
+ `Render Summary`,
888
+ "\u2500".repeat(60),
889
+ ` Total components : ${total}`,
890
+ ` Passed : ${passed}`,
891
+ ` Failed : ${failed}`,
892
+ ` Avg render time : ${avgMs.toFixed(1)}ms`,
893
+ ` Output dir : ${outputDir}`
894
+ ];
895
+ if (failed > 0) {
896
+ lines.push("", " Failed components:");
897
+ for (const r of results) {
898
+ if (!r.success) {
899
+ lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
900
+ }
901
+ }
902
+ }
903
+ lines.push("\u2500".repeat(60));
904
+ return lines.join("\n");
905
+ }
906
+ function escapeHtml(str) {
907
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
908
+ }
909
+ function csvEscape(value) {
910
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
911
+ return `"${value.replace(/"/g, '""')}"`;
912
+ }
913
+ return value;
914
+ }
915
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
916
+ function buildHookInstrumentationScript() {
917
+ return `
918
+ (function __scopeHooksInstrument() {
919
+ // Locate the React DevTools hook installed by the browser-entry bundle.
920
+ // We use a lighter approach: walk __REACT_DEVTOOLS_GLOBAL_HOOK__ renderers.
921
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
922
+ if (!hook) return { error: "No React DevTools hook found" };
923
+
924
+ var renderers = hook._renderers || hook.renderers;
925
+ if (!renderers || renderers.size === 0) return { error: "No React renderers registered" };
926
+
927
+ // Get the first renderer's fiber roots
928
+ var renderer = null;
929
+ if (renderers.forEach) {
930
+ renderers.forEach(function(r) { if (!renderer) renderer = r; });
931
+ } else {
932
+ renderer = Object.values(renderers)[0];
933
+ }
934
+
935
+ // Handle both our wrapped format {id, renderer, fiberRoots} and raw renderer
936
+ var fiberRoots;
937
+ if (renderer && renderer.fiberRoots) {
938
+ fiberRoots = renderer.fiberRoots;
939
+ } else {
940
+ return { error: "No fiber roots found" };
941
+ }
942
+
943
+ var rootFiber = null;
944
+ fiberRoots.forEach(function(r) { if (!rootFiber) rootFiber = r; });
945
+ if (!rootFiber) return { error: "No fiber root" };
946
+
947
+ var current = rootFiber.current;
948
+ if (!current) return { error: "No current fiber" };
949
+
950
+ // Walk fiber tree and collect hook data per component
951
+ var components = [];
952
+
953
+ function serializeValue(v) {
954
+ if (v === null) return null;
955
+ if (v === undefined) return undefined;
956
+ var t = typeof v;
957
+ if (t === "string" || t === "number" || t === "boolean") return v;
958
+ if (t === "function") return "[function " + (v.name || "anonymous") + "]";
959
+ if (Array.isArray(v)) {
960
+ try { return v.map(serializeValue); } catch(e) { return "[Array]"; }
961
+ }
962
+ if (t === "object") {
963
+ try {
964
+ var keys = Object.keys(v).slice(0, 5);
965
+ var out = {};
966
+ for (var k of keys) { out[k] = serializeValue(v[k]); }
967
+ if (Object.keys(v).length > 5) out["..."] = "(truncated)";
968
+ return out;
969
+ } catch(e) { return "[Object]"; }
970
+ }
971
+ return String(v);
972
+ }
973
+
974
+ // Hook node classifiers (mirrors hooks-extractor.ts logic)
975
+ var HookLayout = 0b00100;
976
+
977
+ function isEffectNode(node) {
978
+ var ms = node.memoizedState;
979
+ if (!ms || typeof ms !== "object") return false;
980
+ return typeof ms.create === "function" && "deps" in ms && typeof ms.tag === "number";
981
+ }
982
+
983
+ function isRefNode(node) {
984
+ if (node.queue != null) return false;
985
+ var ms = node.memoizedState;
986
+ if (!ms || typeof ms !== "object" || Array.isArray(ms)) return false;
987
+ var keys = Object.keys(ms);
988
+ return keys.length === 1 && keys[0] === "current";
989
+ }
990
+
991
+ function isMemoTuple(node) {
992
+ if (node.queue != null) return false;
993
+ var ms = node.memoizedState;
994
+ if (!Array.isArray(ms) || ms.length !== 2) return false;
995
+ return ms[1] === null || Array.isArray(ms[1]);
996
+ }
997
+
998
+ function isStateOrReducer(node) {
999
+ return node.queue != null &&
1000
+ typeof node.queue === "object" &&
1001
+ typeof node.queue.dispatch === "function";
1002
+ }
1003
+
1004
+ function isReducer(node) {
1005
+ if (!isStateOrReducer(node)) return false;
1006
+ var q = node.queue;
1007
+ if (typeof q.reducer === "function") return true;
1008
+ var lrr = q.lastRenderedReducer;
1009
+ if (typeof lrr !== "function") return false;
1010
+ var name = lrr.name || "";
1011
+ return name !== "basicStateReducer" && name !== "";
1012
+ }
1013
+
1014
+ function classifyHookNode(node, index) {
1015
+ var profile = { index: index, type: "custom" };
1016
+
1017
+ if (isEffectNode(node)) {
1018
+ var effect = node.memoizedState;
1019
+ profile.type = (effect.tag & HookLayout) ? "useLayoutEffect" : "useEffect";
1020
+ profile.dependencyValues = effect.deps ? effect.deps.map(serializeValue) : null;
1021
+ profile.cleanupPresence = typeof effect.destroy === "function";
1022
+ profile.fireCount = 1; // We can only observe the mount; runtime tracking would need injection
1023
+ profile.lastDuration = null;
1024
+ return profile;
1025
+ }
1026
+
1027
+ if (isRefNode(node)) {
1028
+ var ref = node.memoizedState;
1029
+ profile.type = "useRef";
1030
+ profile.currentRefValue = serializeValue(ref.current);
1031
+ profile.readCountDuringRender = 0; // static snapshot; read count requires instrumented wrapper
1032
+ return profile;
1033
+ }
1034
+
1035
+ if (isMemoTuple(node)) {
1036
+ var tuple = node.memoizedState;
1037
+ var val = tuple[0];
1038
+ var deps = tuple[1];
1039
+ profile.type = typeof val === "function" ? "useCallback" : "useMemo";
1040
+ profile.currentValue = serializeValue(val);
1041
+ profile.dependencyValues = deps ? deps.map(serializeValue) : null;
1042
+ // recomputeCount cannot be known from a single snapshot; set to 0 for mount
1043
+ profile.recomputeCount = 0;
1044
+ // On mount, first render always computes \u2192 cacheHitRate is 0 (no prior hits)
1045
+ profile.cacheHitRate = 0;
1046
+ return profile;
1047
+ }
1048
+
1049
+ if (isStateOrReducer(node)) {
1050
+ if (isReducer(node)) {
1051
+ profile.type = "useReducer";
1052
+ profile.currentValue = serializeValue(node.memoizedState);
1053
+ profile.updateCount = 0;
1054
+ profile.actionTypesDispatched = [];
1055
+ // stateTransitions: we record current state as first entry
1056
+ profile.stateTransitions = [serializeValue(node.memoizedState)];
1057
+ } else {
1058
+ profile.type = "useState";
1059
+ profile.currentValue = serializeValue(node.memoizedState);
1060
+ profile.updateCount = 0;
1061
+ profile.updateOrigins = [];
1062
+ }
1063
+ return profile;
1064
+ }
1065
+
1066
+ // useContext: detected via _currentValue / _currentValue2 property (React context internals)
1067
+ // Context consumers in React store the context VALUE in memoizedState directly, not
1068
+ // via a queue. Context objects themselves have _currentValue.
1069
+ // We check if memoizedState could be a context value by seeing if node has no queue
1070
+ // and memoizedState is not an array and not an effect.
1071
+ if (!node.queue && !isRefNode(node) && !isMemoTuple(node) && !isEffectNode(node)) {
1072
+ profile.type = "custom";
1073
+ profile.currentValue = serializeValue(node.memoizedState);
1074
+ return profile;
1075
+ }
1076
+
1077
+ return profile;
1078
+ }
1079
+
1080
+ // Walk the fiber tree
1081
+ var FunctionComponent = 0;
1082
+ var ClassComponent = 1;
1083
+ var ForwardRef = 11;
1084
+ var MemoComponent = 14;
1085
+ var SimpleMemoComponent = 15;
1086
+ var HostRoot = 3;
1087
+
1088
+ function getFiberName(fiber) {
1089
+ if (!fiber.type) return null;
1090
+ if (typeof fiber.type === "string") return fiber.type;
1091
+ if (typeof fiber.type === "function") return fiber.type.displayName || fiber.type.name || "Anonymous";
1092
+ if (fiber.type.displayName) return fiber.type.displayName;
1093
+ if (fiber.type.render) {
1094
+ return fiber.type.render.displayName || fiber.type.render.name || "ForwardRef";
1095
+ }
1096
+ if (fiber.type.type) {
1097
+ return (fiber.type.type.displayName || fiber.type.type.name || "Memo");
1098
+ }
1099
+ return "Unknown";
1100
+ }
1101
+
1102
+ function isComponentFiber(fiber) {
1103
+ var tag = fiber.tag;
1104
+ return tag === FunctionComponent || tag === ClassComponent ||
1105
+ tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
1106
+ }
1107
+
1108
+ function walkFiber(fiber) {
1109
+ if (!fiber) return;
1110
+
1111
+ if (isComponentFiber(fiber)) {
1112
+ var name = getFiberName(fiber);
1113
+ if (name) {
1114
+ var hooks = [];
1115
+ var hookNode = fiber.memoizedState;
1116
+ var idx = 0;
1117
+ while (hookNode !== null && hookNode !== undefined) {
1118
+ hooks.push(classifyHookNode(hookNode, idx));
1119
+ hookNode = hookNode.next;
1120
+ idx++;
1121
+ }
1122
+
1123
+ var source = null;
1124
+ if (fiber._debugSource) {
1125
+ source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
1126
+ }
1127
+
1128
+ if (hooks.length > 0) {
1129
+ components.push({ name: name, source: source, hooks: hooks });
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ walkFiber(fiber.child);
1135
+ walkFiber(fiber.sibling);
1136
+ }
1137
+
1138
+ walkFiber(current.child);
1139
+
1140
+ return { components: components };
1141
+ })();
1142
+ `;
1143
+ }
1144
+ function analyzeHookFlags(hooks) {
1145
+ const flags = /* @__PURE__ */ new Set();
1146
+ for (const hook of hooks) {
1147
+ if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.dependencyValues !== void 0 && hook.dependencyValues === null) {
1148
+ flags.add("EFFECT_EVERY_RENDER");
1149
+ }
1150
+ if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.cleanupPresence === false && hook.dependencyValues === null) {
1151
+ flags.add("MISSING_CLEANUP");
1152
+ }
1153
+ if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.dependencyValues === null) {
1154
+ flags.add("MEMO_INEFFECTIVE");
1155
+ }
1156
+ if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.cacheHitRate !== void 0 && hook.cacheHitRate === 0 && hook.recomputeCount !== void 0 && hook.recomputeCount > 1) {
1157
+ flags.add("MEMO_INEFFECTIVE");
1158
+ }
1159
+ if ((hook.type === "useState" || hook.type === "useReducer") && hook.updateCount !== void 0 && hook.updateCount > 10) {
1160
+ flags.add("STATE_UPDATE_LOOP");
1161
+ }
1162
+ }
1163
+ return [...flags];
1164
+ }
1165
+ async function runHooksProfiling(componentName, filePath, props) {
1166
+ const browser = await playwright$1.chromium.launch({ headless: true });
1167
+ try {
1168
+ const context = await browser.newContext();
1169
+ const page = await context.newPage();
1170
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1171
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1172
+ await page.waitForFunction(
1173
+ () => {
1174
+ const w = window;
1175
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1176
+ },
1177
+ { timeout: 15e3 }
1178
+ );
1179
+ const renderError = await page.evaluate(() => {
1180
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
1181
+ });
1182
+ if (renderError !== null) {
1183
+ throw new Error(`Component render error: ${renderError}`);
1184
+ }
1185
+ const instrumentScript = buildHookInstrumentationScript();
1186
+ const raw = await page.evaluate(instrumentScript);
1187
+ const result = raw;
1188
+ if (result.error) {
1189
+ throw new Error(`Hook instrumentation failed: ${result.error}`);
1190
+ }
1191
+ const rawComponents = result.components ?? [];
1192
+ const components = rawComponents.map((c) => ({
1193
+ name: c.name,
1194
+ source: c.source,
1195
+ hooks: c.hooks,
1196
+ flags: analyzeHookFlags(c.hooks)
1197
+ }));
1198
+ const allFlags = /* @__PURE__ */ new Set();
1199
+ for (const comp of components) {
1200
+ for (const flag of comp.flags) {
1201
+ allFlags.add(flag);
1202
+ }
1203
+ }
1204
+ return {
1205
+ component: componentName,
1206
+ components,
1207
+ flags: [...allFlags]
1208
+ };
1209
+ } finally {
1210
+ await browser.close();
1211
+ }
1212
+ }
1213
+ function createInstrumentHooksCommand() {
1214
+ const cmd = new commander.Command("hooks").description(
1215
+ "Profile per-hook-instance data for a component: update counts, cache hit rates, effect counts, and more"
1216
+ ).argument("<component>", "Component name (must exist in the manifest)").option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|text (default: auto)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
1217
+ async (componentName, opts) => {
1218
+ try {
1219
+ const manifest = loadManifest(opts.manifest);
1220
+ const descriptor = manifest.components[componentName];
1221
+ if (descriptor === void 0) {
1222
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1223
+ throw new Error(
1224
+ `Component "${componentName}" not found in manifest.
1225
+ Available: ${available}`
1226
+ );
1227
+ }
1228
+ let props = {};
1229
+ try {
1230
+ props = JSON.parse(opts.props);
1231
+ } catch {
1232
+ throw new Error(`Invalid props JSON: ${opts.props}`);
1233
+ }
1234
+ const rootDir = process.cwd();
1235
+ const filePath = path.resolve(rootDir, descriptor.filePath);
1236
+ process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
1237
+ `);
1238
+ const result = await runHooksProfiling(componentName, filePath, props);
1239
+ if (opts.showFlags) {
1240
+ if (result.flags.length === 0) {
1241
+ process.stdout.write("No heuristic flags detected.\n");
1242
+ } else {
1243
+ for (const flag of result.flags) {
1244
+ process.stdout.write(`${flag}
1245
+ `);
1246
+ }
1247
+ }
1248
+ return;
1249
+ }
1250
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1251
+ `);
1252
+ } catch (err) {
1253
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1254
+ `);
1255
+ process.exit(1);
1256
+ }
1257
+ }
1258
+ );
1259
+ return cmd;
1260
+ }
1261
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
1262
+ function buildProfilingSetupScript() {
1263
+ return `
1264
+ (function __scopeProfileSetup() {
1265
+ window.__scopeProfileData = {
1266
+ commitCount: 0,
1267
+ componentNames: new Set(),
1268
+ fiberSnapshots: []
1269
+ };
1270
+
1271
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1272
+ if (!hook) return { error: "No DevTools hook" };
1273
+
1274
+ // Wrap onCommitFiberRoot to count commits and collect component names
1275
+ var origCommit = hook.onCommitFiberRoot.bind(hook);
1276
+ hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
1277
+ origCommit(rendererID, root, priorityLevel);
1278
+
1279
+ window.__scopeProfileData.commitCount++;
1280
+
1281
+ // Walk the committed tree to collect re-rendered component names
1282
+ var current = root && root.current;
1283
+ if (!current) return;
1284
+
1285
+ function walkFiber(fiber) {
1286
+ if (!fiber) return;
1287
+ var tag = fiber.tag;
1288
+ // FunctionComponent=0, ClassComponent=1, ForwardRef=11, Memo=14, SimpleMemo=15
1289
+ if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
1290
+ var name = null;
1291
+ if (fiber.type) {
1292
+ if (typeof fiber.type === "function") name = fiber.type.displayName || fiber.type.name;
1293
+ else if (fiber.type.displayName) name = fiber.type.displayName;
1294
+ else if (fiber.type.render) name = fiber.type.render.displayName || fiber.type.render.name;
1295
+ else if (fiber.type.type) name = fiber.type.type.displayName || fiber.type.type.name;
1296
+ }
1297
+ // Only count fibers with a positive actualDuration (actually re-rendered this commit)
1298
+ if (name && typeof fiber.actualDuration === "number" && fiber.actualDuration >= 0) {
1299
+ window.__scopeProfileData.componentNames.add(name);
1300
+ }
1301
+ }
1302
+ walkFiber(fiber.child);
1303
+ walkFiber(fiber.sibling);
1304
+ }
1305
+
1306
+ var wip = root.current.alternate || root.current;
1307
+ walkFiber(wip.child);
1308
+ };
1309
+
1310
+ // Install PerformanceObserver for layout and paint timing
1311
+ window.__scopeLayoutTime = 0;
1312
+ window.__scopePaintTime = 0;
1313
+ window.__scopeLayoutShifts = { count: 0, score: 0 };
1314
+
1315
+ try {
1316
+ var observer = new PerformanceObserver(function(list) {
1317
+ for (var entry of list.getEntries()) {
1318
+ if (entry.entryType === "layout-shift") {
1319
+ window.__scopeLayoutShifts.count++;
1320
+ window.__scopeLayoutShifts.score += entry.value || 0;
1321
+ }
1322
+ if (entry.entryType === "longtask") {
1323
+ window.__scopeLayoutTime += entry.duration || 0;
1324
+ }
1325
+ if (entry.entryType === "paint") {
1326
+ window.__scopePaintTime += entry.startTime || 0;
1327
+ }
1328
+ }
1329
+ });
1330
+ observer.observe({ entryTypes: ["layout-shift", "longtask", "paint"] });
1331
+ } catch(e) {
1332
+ // PerformanceObserver may not be available in all Playwright contexts
1333
+ }
1334
+
1335
+ return { ok: true };
1336
+ })();
910
1337
  `;
911
1338
  }
912
- function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
913
- const filled = Math.round(pct / 100 * barWidth);
914
- const empty = barWidth - filled;
915
- const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
916
- const nameSlice = currentName.slice(0, 25).padEnd(25);
917
- return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
1339
+ function buildProfilingCollectScript() {
1340
+ return `
1341
+ (function __scopeProfileCollect() {
1342
+ var data = window.__scopeProfileData;
1343
+ if (!data) return { error: "Profiling not set up" };
1344
+
1345
+ // Estimate wasted renders: use paint entries count as a heuristic for
1346
+ // "components that re-rendered but their subtree output was likely unchanged".
1347
+ // A more accurate method would require React's own wasted-render detection,
1348
+ // which requires React Profiler API. We use a conservative estimate here.
1349
+ var totalCommits = data.commitCount;
1350
+ var uniqueNames = Array.from(data.componentNames);
1351
+
1352
+ // Wasted renders heuristic: if a component is in a subsequent commit (not the initial
1353
+ // mount commit), it *may* have been wasted if it didn't actually need to re-render.
1354
+ // For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
1355
+ // This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
1356
+ var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
1357
+
1358
+ return {
1359
+ commitCount: totalCommits,
1360
+ uniqueComponents: uniqueNames.length,
1361
+ componentNames: uniqueNames,
1362
+ wastedRenders: wastedRenders,
1363
+ layoutTime: window.__scopeLayoutTime || 0,
1364
+ paintTime: window.__scopePaintTime || 0,
1365
+ layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
1366
+ };
1367
+ })();
1368
+ `;
918
1369
  }
919
- function formatSummaryText(results, outputDir) {
920
- const total = results.length;
921
- const passed = results.filter((r) => r.success).length;
922
- const failed = total - passed;
923
- const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
924
- const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
925
- const lines = [
926
- "\u2500".repeat(60),
927
- `Render Summary`,
928
- "\u2500".repeat(60),
929
- ` Total components : ${total}`,
930
- ` Passed : ${passed}`,
931
- ` Failed : ${failed}`,
932
- ` Avg render time : ${avgMs.toFixed(1)}ms`,
933
- ` Output dir : ${outputDir}`
934
- ];
935
- if (failed > 0) {
936
- lines.push("", " Failed components:");
937
- for (const r of results) {
938
- if (!r.success) {
939
- lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
940
- }
1370
+ async function replayInteraction(page, steps) {
1371
+ for (const step of steps) {
1372
+ switch (step.action) {
1373
+ case "click":
1374
+ if (step.target) {
1375
+ await page.click(step.target, { timeout: 5e3 }).catch(() => {
1376
+ process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
1377
+ `);
1378
+ });
1379
+ }
1380
+ break;
1381
+ case "fill":
1382
+ if (step.target && step.value !== void 0) {
1383
+ await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
1384
+ process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
1385
+ `);
1386
+ });
1387
+ }
1388
+ break;
1389
+ case "hover":
1390
+ if (step.target) {
1391
+ await page.hover(step.target, { timeout: 5e3 }).catch(() => {
1392
+ process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
1393
+ `);
1394
+ });
1395
+ }
1396
+ break;
1397
+ case "press":
1398
+ if (step.value) {
1399
+ await page.keyboard.press(step.value);
1400
+ }
1401
+ break;
1402
+ case "wait":
1403
+ await page.waitForTimeout(step.delay ?? 500);
1404
+ break;
941
1405
  }
942
1406
  }
943
- lines.push("\u2500".repeat(60));
944
- return lines.join("\n");
945
1407
  }
946
- function escapeHtml(str) {
947
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1408
+ function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
1409
+ const flags = /* @__PURE__ */ new Set();
1410
+ if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
1411
+ flags.add("WASTED_RENDER");
1412
+ }
1413
+ if (totalRenders > 10) {
1414
+ flags.add("HIGH_RENDER_COUNT");
1415
+ }
1416
+ if (layoutShifts.cumulativeScore > 0.1) {
1417
+ flags.add("LAYOUT_SHIFT_DETECTED");
1418
+ }
1419
+ if (timing.js > 100) {
1420
+ flags.add("SLOW_INTERACTION");
1421
+ }
1422
+ return [...flags];
948
1423
  }
949
- function csvEscape(value) {
950
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
951
- return `"${value.replace(/"/g, '""')}"`;
1424
+ async function runInteractionProfile(componentName, filePath, props, interaction) {
1425
+ const browser = await playwright$1.chromium.launch({ headless: true });
1426
+ try {
1427
+ const context = await browser.newContext();
1428
+ const page = await context.newPage();
1429
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1430
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1431
+ await page.waitForFunction(
1432
+ () => {
1433
+ const w = window;
1434
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1435
+ },
1436
+ { timeout: 15e3 }
1437
+ );
1438
+ const renderError = await page.evaluate(() => {
1439
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
1440
+ });
1441
+ if (renderError !== null) {
1442
+ throw new Error(`Component render error: ${renderError}`);
1443
+ }
1444
+ const setupResult = await page.evaluate(buildProfilingSetupScript());
1445
+ const setupData = setupResult;
1446
+ if (setupData.error) {
1447
+ throw new Error(`Profiling setup failed: ${setupData.error}`);
1448
+ }
1449
+ const jsStart = Date.now();
1450
+ if (interaction.length > 0) {
1451
+ process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
1452
+ `);
1453
+ await replayInteraction(page, interaction);
1454
+ await page.waitForTimeout(300);
1455
+ }
1456
+ const jsDuration = Date.now() - jsStart;
1457
+ const collected = await page.evaluate(buildProfilingCollectScript());
1458
+ const profileData = collected;
1459
+ if (profileData.error) {
1460
+ throw new Error(`Profile collection failed: ${profileData.error}`);
1461
+ }
1462
+ const timing = {
1463
+ js: jsDuration,
1464
+ layout: profileData.layoutTime ?? 0,
1465
+ paint: profileData.paintTime ?? 0
1466
+ };
1467
+ const layoutShifts = {
1468
+ count: profileData.layoutShifts?.count ?? 0,
1469
+ cumulativeScore: profileData.layoutShifts?.score ?? 0
1470
+ };
1471
+ const totalRenders = profileData.commitCount ?? 0;
1472
+ const uniqueComponents = profileData.uniqueComponents ?? 0;
1473
+ const wastedRenders = profileData.wastedRenders ?? 0;
1474
+ const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
1475
+ return {
1476
+ component: componentName,
1477
+ totalRenders,
1478
+ uniqueComponents,
1479
+ wastedRenders,
1480
+ timing,
1481
+ layoutShifts,
1482
+ flags,
1483
+ interaction
1484
+ };
1485
+ } finally {
1486
+ await browser.close();
952
1487
  }
953
- return value;
1488
+ }
1489
+ function createInstrumentProfileCommand() {
1490
+ const cmd = new commander.Command("profile").description(
1491
+ "Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
1492
+ ).argument("<component>", "Component name (must exist in the manifest)").option(
1493
+ "--interaction <json>",
1494
+ `Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
1495
+ "[]"
1496
+ ).option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|text (default: json)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
1497
+ async (componentName, opts) => {
1498
+ try {
1499
+ const manifest = loadManifest(opts.manifest);
1500
+ const descriptor = manifest.components[componentName];
1501
+ if (descriptor === void 0) {
1502
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1503
+ throw new Error(
1504
+ `Component "${componentName}" not found in manifest.
1505
+ Available: ${available}`
1506
+ );
1507
+ }
1508
+ let props = {};
1509
+ try {
1510
+ props = JSON.parse(opts.props);
1511
+ } catch {
1512
+ throw new Error(`Invalid props JSON: ${opts.props}`);
1513
+ }
1514
+ let interaction = [];
1515
+ try {
1516
+ interaction = JSON.parse(opts.interaction);
1517
+ if (!Array.isArray(interaction)) {
1518
+ throw new Error("Interaction must be a JSON array");
1519
+ }
1520
+ } catch {
1521
+ throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
1522
+ }
1523
+ const rootDir = process.cwd();
1524
+ const filePath = path.resolve(rootDir, descriptor.filePath);
1525
+ process.stderr.write(`Profiling interaction for ${componentName}\u2026
1526
+ `);
1527
+ const result = await runInteractionProfile(componentName, filePath, props, interaction);
1528
+ if (opts.showFlags) {
1529
+ if (result.flags.length === 0) {
1530
+ process.stdout.write("No heuristic flags detected.\n");
1531
+ } else {
1532
+ for (const flag of result.flags) {
1533
+ process.stdout.write(`${flag}
1534
+ `);
1535
+ }
1536
+ }
1537
+ return;
1538
+ }
1539
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1540
+ `);
1541
+ } catch (err) {
1542
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1543
+ `);
1544
+ process.exit(1);
1545
+ }
1546
+ }
1547
+ );
1548
+ return cmd;
954
1549
  }
955
1550
 
956
1551
  // src/instrument/renders.ts
957
- var MANIFEST_PATH2 = ".reactscope/manifest.json";
1552
+ var MANIFEST_PATH4 = ".reactscope/manifest.json";
958
1553
  function determineTrigger(event) {
959
1554
  if (event.forceUpdate) return "force_update";
960
1555
  if (event.stateChanged) return "state_change";
@@ -1199,7 +1794,7 @@ function buildInstrumentationScript() {
1199
1794
  `
1200
1795
  );
1201
1796
  }
1202
- async function replayInteraction(page, steps) {
1797
+ async function replayInteraction2(page, steps) {
1203
1798
  for (const step of steps) {
1204
1799
  switch (step.action) {
1205
1800
  case "click":
@@ -1272,7 +1867,7 @@ async function shutdownPool() {
1272
1867
  }
1273
1868
  }
1274
1869
  async function analyzeRenders(options) {
1275
- const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
1870
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
1276
1871
  const manifest = loadManifest(manifestPath);
1277
1872
  const descriptor = manifest.components[options.componentName];
1278
1873
  if (descriptor === void 0) {
@@ -1301,7 +1896,7 @@ Available: ${available}`
1301
1896
  window.__SCOPE_RENDER_EVENTS__ = [];
1302
1897
  window.__SCOPE_RENDER_INDEX__ = 0;
1303
1898
  });
1304
- await replayInteraction(page, options.interaction);
1899
+ await replayInteraction2(page, options.interaction);
1305
1900
  await page.waitForTimeout(200);
1306
1901
  const interactionDurationMs = performance.now() - startMs;
1307
1902
  const rawEvents = await page.evaluate(() => {
@@ -1370,7 +1965,7 @@ function createInstrumentRendersCommand() {
1370
1965
  "--interaction <json>",
1371
1966
  `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1372
1967
  "[]"
1373
- ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
1968
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
1374
1969
  async (componentName, opts) => {
1375
1970
  let interaction = [];
1376
1971
  try {
@@ -1415,8 +2010,50 @@ function createInstrumentCommand() {
1415
2010
  "Structured instrumentation commands for React component analysis"
1416
2011
  );
1417
2012
  instrumentCmd.addCommand(createInstrumentRendersCommand());
2013
+ instrumentCmd.addCommand(createInstrumentHooksCommand());
2014
+ instrumentCmd.addCommand(createInstrumentProfileCommand());
1418
2015
  return instrumentCmd;
1419
2016
  }
2017
+ async function browserCapture(options) {
2018
+ const { url, timeout = 1e4, wait = 0 } = options;
2019
+ const browser = await playwright$1.chromium.launch({ headless: true });
2020
+ try {
2021
+ const context = await browser.newContext();
2022
+ const page = await context.newPage();
2023
+ await page.addInitScript({ content: playwright.getBrowserEntryScript() });
2024
+ await page.goto(url, {
2025
+ waitUntil: "networkidle",
2026
+ timeout: timeout + 5e3
2027
+ });
2028
+ await page.waitForFunction(
2029
+ () => {
2030
+ return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
2031
+ },
2032
+ { timeout }
2033
+ );
2034
+ if (wait > 0) {
2035
+ await page.waitForTimeout(wait);
2036
+ }
2037
+ const raw = await page.evaluate(async () => {
2038
+ const win = window;
2039
+ if (typeof win.__SCOPE_CAPTURE__ !== "function") {
2040
+ throw new Error("Scope runtime not injected");
2041
+ }
2042
+ return win.__SCOPE_CAPTURE__();
2043
+ });
2044
+ if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
2045
+ throw new Error(`Scope capture failed: ${raw.error}`);
2046
+ }
2047
+ const report = { ...raw, route: null };
2048
+ return { report };
2049
+ } finally {
2050
+ await browser.close();
2051
+ }
2052
+ }
2053
+ function writeReportToFile(report, outputPath, pretty) {
2054
+ const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
2055
+ fs.writeFileSync(outputPath, json, "utf-8");
2056
+ }
1420
2057
  var CONFIG_FILENAMES = [
1421
2058
  ".reactscope/config.json",
1422
2059
  ".reactscope/config.js",
@@ -1534,7 +2171,7 @@ async function getCompiledCssForClasses(cwd, classes) {
1534
2171
  }
1535
2172
 
1536
2173
  // src/render-commands.ts
1537
- var MANIFEST_PATH3 = ".reactscope/manifest.json";
2174
+ var MANIFEST_PATH5 = ".reactscope/manifest.json";
1538
2175
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
1539
2176
  var _pool2 = null;
1540
2177
  async function getPool2(viewportWidth, viewportHeight) {
@@ -1659,7 +2296,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
1659
2296
  };
1660
2297
  }
1661
2298
  function registerRenderSingle(renderCmd) {
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(
2299
+ 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_PATH5).action(
1663
2300
  async (componentName, opts) => {
1664
2301
  try {
1665
2302
  const manifest = loadManifest(opts.manifest);
@@ -1758,7 +2395,7 @@ function registerRenderMatrix(renderCmd) {
1758
2395
  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(
1759
2396
  "--contexts <ids>",
1760
2397
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
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(
2398
+ ).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_PATH5).action(
1762
2399
  async (componentName, opts) => {
1763
2400
  try {
1764
2401
  const manifest = loadManifest(opts.manifest);
@@ -1885,7 +2522,7 @@ Available: ${available}`
1885
2522
  );
1886
2523
  }
1887
2524
  function registerRenderAll(renderCmd) {
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(
2525
+ 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_PATH5).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
1889
2526
  async (opts) => {
1890
2527
  try {
1891
2528
  const manifest = loadManifest(opts.manifest);
@@ -2173,12 +2810,12 @@ async function runBaseline(options = {}) {
2173
2810
  fs.mkdirSync(rendersDir, { recursive: true });
2174
2811
  let manifest$1;
2175
2812
  if (manifestPath !== void 0) {
2176
- const { readFileSync: readFileSync7 } = await import('fs');
2813
+ const { readFileSync: readFileSync8 } = await import('fs');
2177
2814
  const absPath = path.resolve(rootDir, manifestPath);
2178
2815
  if (!fs.existsSync(absPath)) {
2179
2816
  throw new Error(`Manifest not found at ${absPath}.`);
2180
2817
  }
2181
- manifest$1 = JSON.parse(readFileSync7(absPath, "utf-8"));
2818
+ manifest$1 = JSON.parse(readFileSync8(absPath, "utf-8"));
2182
2819
  process.stderr.write(`Loaded manifest from ${manifestPath}
2183
2820
  `);
2184
2821
  } else {
@@ -2619,6 +3256,109 @@ function buildStructuredReport(report) {
2619
3256
  }
2620
3257
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
2621
3258
  var CONFIG_FILE = "reactscope.config.json";
3259
+ var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3260
+ function resolveTokenFilePath(fileFlag) {
3261
+ if (fileFlag !== void 0) {
3262
+ return path.resolve(process.cwd(), fileFlag);
3263
+ }
3264
+ const configPath = path.resolve(process.cwd(), CONFIG_FILE);
3265
+ if (fs.existsSync(configPath)) {
3266
+ try {
3267
+ const raw = fs.readFileSync(configPath, "utf-8");
3268
+ const config = JSON.parse(raw);
3269
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3270
+ const file = config.tokens.file;
3271
+ return path.resolve(process.cwd(), file);
3272
+ }
3273
+ } catch {
3274
+ }
3275
+ }
3276
+ return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE);
3277
+ }
3278
+ function createTokensExportCommand() {
3279
+ return new commander.Command("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
3280
+ "--theme <name>",
3281
+ "Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
3282
+ ).action(
3283
+ (opts) => {
3284
+ if (!SUPPORTED_FORMATS.includes(opts.format)) {
3285
+ process.stderr.write(
3286
+ `Error: unsupported format "${opts.format}".
3287
+ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3288
+ `
3289
+ );
3290
+ process.exit(1);
3291
+ }
3292
+ const format = opts.format;
3293
+ try {
3294
+ const filePath = resolveTokenFilePath(opts.file);
3295
+ if (!fs.existsSync(filePath)) {
3296
+ throw new Error(
3297
+ `Token file not found at ${filePath}.
3298
+ Create a reactscope.tokens.json file or use --file to specify a path.`
3299
+ );
3300
+ }
3301
+ const raw = fs.readFileSync(filePath, "utf-8");
3302
+ const { tokens: tokens$1, rawFile } = tokens.parseTokenFileSync(raw);
3303
+ let themesMap;
3304
+ if (opts.theme !== void 0) {
3305
+ if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
3306
+ const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
3307
+ throw new Error(
3308
+ `Theme "${opts.theme}" not found in token file.
3309
+ Available themes: ${available}`
3310
+ );
3311
+ }
3312
+ const baseResolver = new tokens.TokenResolver(tokens$1);
3313
+ const themeResolver = tokens.ThemeResolver.fromTokenFile(
3314
+ baseResolver,
3315
+ rawFile
3316
+ );
3317
+ const themeNames = themeResolver.listThemes();
3318
+ if (!themeNames.includes(opts.theme)) {
3319
+ throw new Error(
3320
+ `Theme "${opts.theme}" could not be resolved.
3321
+ Available themes: ${themeNames.join(", ")}`
3322
+ );
3323
+ }
3324
+ const themedTokens = themeResolver.buildThemedTokens(opts.theme);
3325
+ const overrideMap = /* @__PURE__ */ new Map();
3326
+ for (const themedToken of themedTokens) {
3327
+ const baseToken = tokens$1.find((t) => t.path === themedToken.path);
3328
+ if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
3329
+ overrideMap.set(themedToken.path, themedToken.resolvedValue);
3330
+ }
3331
+ }
3332
+ themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
3333
+ }
3334
+ const output = tokens.exportTokens(tokens$1, format, {
3335
+ prefix: opts.prefix,
3336
+ rootSelector: opts.selector,
3337
+ themes: themesMap
3338
+ });
3339
+ if (opts.out !== void 0) {
3340
+ const outPath = path.resolve(process.cwd(), opts.out);
3341
+ fs.writeFileSync(outPath, output, "utf-8");
3342
+ process.stderr.write(`Exported ${tokens$1.length} tokens to ${outPath}
3343
+ `);
3344
+ } else {
3345
+ process.stdout.write(output);
3346
+ if (!output.endsWith("\n")) {
3347
+ process.stdout.write("\n");
3348
+ }
3349
+ }
3350
+ } catch (err) {
3351
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3352
+ `);
3353
+ process.exit(1);
3354
+ }
3355
+ }
3356
+ );
3357
+ }
3358
+
3359
+ // src/tokens/commands.ts
3360
+ var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
3361
+ var CONFIG_FILE2 = "reactscope.config.json";
2622
3362
  function isTTY2() {
2623
3363
  return process.stdout.isTTY === true;
2624
3364
  }
@@ -2636,11 +3376,11 @@ function buildTable2(headers, rows) {
2636
3376
  );
2637
3377
  return [headerRow, divider, ...dataRows].join("\n");
2638
3378
  }
2639
- function resolveTokenFilePath(fileFlag) {
3379
+ function resolveTokenFilePath2(fileFlag) {
2640
3380
  if (fileFlag !== void 0) {
2641
3381
  return path.resolve(process.cwd(), fileFlag);
2642
3382
  }
2643
- const configPath = path.resolve(process.cwd(), CONFIG_FILE);
3383
+ const configPath = path.resolve(process.cwd(), CONFIG_FILE2);
2644
3384
  if (fs.existsSync(configPath)) {
2645
3385
  try {
2646
3386
  const raw = fs.readFileSync(configPath, "utf-8");
@@ -2652,7 +3392,7 @@ function resolveTokenFilePath(fileFlag) {
2652
3392
  } catch {
2653
3393
  }
2654
3394
  }
2655
- return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE);
3395
+ return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE2);
2656
3396
  }
2657
3397
  function loadTokens(absPath) {
2658
3398
  if (!fs.existsSync(absPath)) {
@@ -2699,7 +3439,7 @@ function buildResolutionChain(startPath, rawTokens) {
2699
3439
  function registerGet2(tokensCmd) {
2700
3440
  tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
2701
3441
  try {
2702
- const filePath = resolveTokenFilePath(opts.file);
3442
+ const filePath = resolveTokenFilePath2(opts.file);
2703
3443
  const { tokens: tokens$1 } = loadTokens(filePath);
2704
3444
  const resolver = new tokens.TokenResolver(tokens$1);
2705
3445
  const resolvedValue = resolver.resolve(tokenPath);
@@ -2725,7 +3465,7 @@ function registerList2(tokensCmd) {
2725
3465
  tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
2726
3466
  (category, opts) => {
2727
3467
  try {
2728
- const filePath = resolveTokenFilePath(opts.file);
3468
+ const filePath = resolveTokenFilePath2(opts.file);
2729
3469
  const { tokens: tokens$1 } = loadTokens(filePath);
2730
3470
  const resolver = new tokens.TokenResolver(tokens$1);
2731
3471
  const filtered = resolver.list(opts.type, category);
@@ -2755,7 +3495,7 @@ function registerSearch(tokensCmd) {
2755
3495
  tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
2756
3496
  (value, opts) => {
2757
3497
  try {
2758
- const filePath = resolveTokenFilePath(opts.file);
3498
+ const filePath = resolveTokenFilePath2(opts.file);
2759
3499
  const { tokens: tokens$1 } = loadTokens(filePath);
2760
3500
  const resolver = new tokens.TokenResolver(tokens$1);
2761
3501
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
@@ -2837,7 +3577,7 @@ Tip: use --fuzzy for nearest-match search.
2837
3577
  function registerResolve(tokensCmd) {
2838
3578
  tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
2839
3579
  try {
2840
- const filePath = resolveTokenFilePath(opts.file);
3580
+ const filePath = resolveTokenFilePath2(opts.file);
2841
3581
  const absFilePath = filePath;
2842
3582
  const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
2843
3583
  const resolver = new tokens.TokenResolver(tokens$1);
@@ -2874,7 +3614,7 @@ function registerValidate(tokensCmd) {
2874
3614
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
2875
3615
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2876
3616
  try {
2877
- const filePath = resolveTokenFilePath(opts.file);
3617
+ const filePath = resolveTokenFilePath2(opts.file);
2878
3618
  if (!fs.existsSync(filePath)) {
2879
3619
  throw new Error(
2880
3620
  `Token file not found at ${filePath}.
@@ -2957,6 +3697,7 @@ function createTokensCommand() {
2957
3697
  registerSearch(tokensCmd);
2958
3698
  registerResolve(tokensCmd);
2959
3699
  registerValidate(tokensCmd);
3700
+ tokensCmd.addCommand(createTokensExportCommand());
2960
3701
  return tokensCmd;
2961
3702
  }
2962
3703
 
@@ -3057,11 +3798,14 @@ function createProgram(options = {}) {
3057
3798
  }
3058
3799
 
3059
3800
  exports.createInitCommand = createInitCommand;
3801
+ exports.createInstrumentCommand = createInstrumentCommand;
3060
3802
  exports.createManifestCommand = createManifestCommand;
3061
3803
  exports.createProgram = createProgram;
3062
3804
  exports.createTokensCommand = createTokensCommand;
3805
+ exports.createTokensExportCommand = createTokensExportCommand;
3063
3806
  exports.isTTY = isTTY;
3064
3807
  exports.matchGlob = matchGlob;
3808
+ exports.resolveTokenFilePath = resolveTokenFilePath;
3065
3809
  exports.runInit = runInit;
3066
3810
  //# sourceMappingURL=index.cjs.map
3067
3811
  //# sourceMappingURL=index.cjs.map