@agent-scope/cli 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1305 -179
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1341 -241
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -1
- package/dist/index.d.ts +264 -1
- package/dist/index.js +1340 -243
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command8 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -255,9 +255,9 @@ function createRL() {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
async function ask(rl, question) {
|
|
258
|
-
return new Promise((
|
|
258
|
+
return new Promise((resolve11) => {
|
|
259
259
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
260
|
+
resolve11(answer.trim());
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -406,9 +406,9 @@ function createInitCommand() {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
// src/instrument/renders.ts
|
|
409
|
-
import { resolve as
|
|
410
|
-
import { BrowserPool } from "@agent-scope/render";
|
|
411
|
-
import { Command as
|
|
409
|
+
import { resolve as resolve5 } from "path";
|
|
410
|
+
import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
|
|
411
|
+
import { Command as Command4 } from "commander";
|
|
412
412
|
|
|
413
413
|
// src/component-bundler.ts
|
|
414
414
|
import { dirname } from "path";
|
|
@@ -913,52 +913,1060 @@ function formatMatrixCsv(componentName, result) {
|
|
|
913
913
|
return `${[headers.join(","), ...rows].join("\n")}
|
|
914
914
|
`;
|
|
915
915
|
}
|
|
916
|
-
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
917
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
918
|
-
const empty = barWidth - filled;
|
|
919
|
-
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
920
|
-
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
921
|
-
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
916
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
917
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
918
|
+
const empty = barWidth - filled;
|
|
919
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
920
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
921
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
922
|
+
}
|
|
923
|
+
function formatSummaryText(results, outputDir) {
|
|
924
|
+
const total = results.length;
|
|
925
|
+
const passed = results.filter((r) => r.success).length;
|
|
926
|
+
const failed = total - passed;
|
|
927
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
928
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
929
|
+
const lines = [
|
|
930
|
+
"\u2500".repeat(60),
|
|
931
|
+
`Render Summary`,
|
|
932
|
+
"\u2500".repeat(60),
|
|
933
|
+
` Total components : ${total}`,
|
|
934
|
+
` Passed : ${passed}`,
|
|
935
|
+
` Failed : ${failed}`,
|
|
936
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
937
|
+
` Output dir : ${outputDir}`
|
|
938
|
+
];
|
|
939
|
+
if (failed > 0) {
|
|
940
|
+
lines.push("", " Failed components:");
|
|
941
|
+
for (const r of results) {
|
|
942
|
+
if (!r.success) {
|
|
943
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
lines.push("\u2500".repeat(60));
|
|
948
|
+
return lines.join("\n");
|
|
949
|
+
}
|
|
950
|
+
function escapeHtml(str) {
|
|
951
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
952
|
+
}
|
|
953
|
+
function csvEscape(value) {
|
|
954
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
955
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
956
|
+
}
|
|
957
|
+
return value;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/instrument/hooks.ts
|
|
961
|
+
import { resolve as resolve2 } from "path";
|
|
962
|
+
import { Command as Cmd } from "commander";
|
|
963
|
+
import { chromium as chromium2 } from "playwright";
|
|
964
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
965
|
+
function buildHookInstrumentationScript() {
|
|
966
|
+
return `
|
|
967
|
+
(function __scopeHooksInstrument() {
|
|
968
|
+
// Locate the React DevTools hook installed by the browser-entry bundle.
|
|
969
|
+
// We use a lighter approach: walk __REACT_DEVTOOLS_GLOBAL_HOOK__ renderers.
|
|
970
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
971
|
+
if (!hook) return { error: "No React DevTools hook found" };
|
|
972
|
+
|
|
973
|
+
var renderers = hook._renderers || hook.renderers;
|
|
974
|
+
if (!renderers || renderers.size === 0) return { error: "No React renderers registered" };
|
|
975
|
+
|
|
976
|
+
// Get the first renderer's fiber roots
|
|
977
|
+
var renderer = null;
|
|
978
|
+
if (renderers.forEach) {
|
|
979
|
+
renderers.forEach(function(r) { if (!renderer) renderer = r; });
|
|
980
|
+
} else {
|
|
981
|
+
renderer = Object.values(renderers)[0];
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Handle both our wrapped format {id, renderer, fiberRoots} and raw renderer
|
|
985
|
+
var fiberRoots;
|
|
986
|
+
if (renderer && renderer.fiberRoots) {
|
|
987
|
+
fiberRoots = renderer.fiberRoots;
|
|
988
|
+
} else {
|
|
989
|
+
return { error: "No fiber roots found" };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
var rootFiber = null;
|
|
993
|
+
fiberRoots.forEach(function(r) { if (!rootFiber) rootFiber = r; });
|
|
994
|
+
if (!rootFiber) return { error: "No fiber root" };
|
|
995
|
+
|
|
996
|
+
var current = rootFiber.current;
|
|
997
|
+
if (!current) return { error: "No current fiber" };
|
|
998
|
+
|
|
999
|
+
// Walk fiber tree and collect hook data per component
|
|
1000
|
+
var components = [];
|
|
1001
|
+
|
|
1002
|
+
function serializeValue(v) {
|
|
1003
|
+
if (v === null) return null;
|
|
1004
|
+
if (v === undefined) return undefined;
|
|
1005
|
+
var t = typeof v;
|
|
1006
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
1007
|
+
if (t === "function") return "[function " + (v.name || "anonymous") + "]";
|
|
1008
|
+
if (Array.isArray(v)) {
|
|
1009
|
+
try { return v.map(serializeValue); } catch(e) { return "[Array]"; }
|
|
1010
|
+
}
|
|
1011
|
+
if (t === "object") {
|
|
1012
|
+
try {
|
|
1013
|
+
var keys = Object.keys(v).slice(0, 5);
|
|
1014
|
+
var out = {};
|
|
1015
|
+
for (var k of keys) { out[k] = serializeValue(v[k]); }
|
|
1016
|
+
if (Object.keys(v).length > 5) out["..."] = "(truncated)";
|
|
1017
|
+
return out;
|
|
1018
|
+
} catch(e) { return "[Object]"; }
|
|
1019
|
+
}
|
|
1020
|
+
return String(v);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Hook node classifiers (mirrors hooks-extractor.ts logic)
|
|
1024
|
+
var HookLayout = 0b00100;
|
|
1025
|
+
|
|
1026
|
+
function isEffectNode(node) {
|
|
1027
|
+
var ms = node.memoizedState;
|
|
1028
|
+
if (!ms || typeof ms !== "object") return false;
|
|
1029
|
+
return typeof ms.create === "function" && "deps" in ms && typeof ms.tag === "number";
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function isRefNode(node) {
|
|
1033
|
+
if (node.queue != null) return false;
|
|
1034
|
+
var ms = node.memoizedState;
|
|
1035
|
+
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return false;
|
|
1036
|
+
var keys = Object.keys(ms);
|
|
1037
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function isMemoTuple(node) {
|
|
1041
|
+
if (node.queue != null) return false;
|
|
1042
|
+
var ms = node.memoizedState;
|
|
1043
|
+
if (!Array.isArray(ms) || ms.length !== 2) return false;
|
|
1044
|
+
return ms[1] === null || Array.isArray(ms[1]);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function isStateOrReducer(node) {
|
|
1048
|
+
return node.queue != null &&
|
|
1049
|
+
typeof node.queue === "object" &&
|
|
1050
|
+
typeof node.queue.dispatch === "function";
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function isReducer(node) {
|
|
1054
|
+
if (!isStateOrReducer(node)) return false;
|
|
1055
|
+
var q = node.queue;
|
|
1056
|
+
if (typeof q.reducer === "function") return true;
|
|
1057
|
+
var lrr = q.lastRenderedReducer;
|
|
1058
|
+
if (typeof lrr !== "function") return false;
|
|
1059
|
+
var name = lrr.name || "";
|
|
1060
|
+
return name !== "basicStateReducer" && name !== "";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function classifyHookNode(node, index) {
|
|
1064
|
+
var profile = { index: index, type: "custom" };
|
|
1065
|
+
|
|
1066
|
+
if (isEffectNode(node)) {
|
|
1067
|
+
var effect = node.memoizedState;
|
|
1068
|
+
profile.type = (effect.tag & HookLayout) ? "useLayoutEffect" : "useEffect";
|
|
1069
|
+
profile.dependencyValues = effect.deps ? effect.deps.map(serializeValue) : null;
|
|
1070
|
+
profile.cleanupPresence = typeof effect.destroy === "function";
|
|
1071
|
+
profile.fireCount = 1; // We can only observe the mount; runtime tracking would need injection
|
|
1072
|
+
profile.lastDuration = null;
|
|
1073
|
+
return profile;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (isRefNode(node)) {
|
|
1077
|
+
var ref = node.memoizedState;
|
|
1078
|
+
profile.type = "useRef";
|
|
1079
|
+
profile.currentRefValue = serializeValue(ref.current);
|
|
1080
|
+
profile.readCountDuringRender = 0; // static snapshot; read count requires instrumented wrapper
|
|
1081
|
+
return profile;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (isMemoTuple(node)) {
|
|
1085
|
+
var tuple = node.memoizedState;
|
|
1086
|
+
var val = tuple[0];
|
|
1087
|
+
var deps = tuple[1];
|
|
1088
|
+
profile.type = typeof val === "function" ? "useCallback" : "useMemo";
|
|
1089
|
+
profile.currentValue = serializeValue(val);
|
|
1090
|
+
profile.dependencyValues = deps ? deps.map(serializeValue) : null;
|
|
1091
|
+
// recomputeCount cannot be known from a single snapshot; set to 0 for mount
|
|
1092
|
+
profile.recomputeCount = 0;
|
|
1093
|
+
// On mount, first render always computes \u2192 cacheHitRate is 0 (no prior hits)
|
|
1094
|
+
profile.cacheHitRate = 0;
|
|
1095
|
+
return profile;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (isStateOrReducer(node)) {
|
|
1099
|
+
if (isReducer(node)) {
|
|
1100
|
+
profile.type = "useReducer";
|
|
1101
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1102
|
+
profile.updateCount = 0;
|
|
1103
|
+
profile.actionTypesDispatched = [];
|
|
1104
|
+
// stateTransitions: we record current state as first entry
|
|
1105
|
+
profile.stateTransitions = [serializeValue(node.memoizedState)];
|
|
1106
|
+
} else {
|
|
1107
|
+
profile.type = "useState";
|
|
1108
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1109
|
+
profile.updateCount = 0;
|
|
1110
|
+
profile.updateOrigins = [];
|
|
1111
|
+
}
|
|
1112
|
+
return profile;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// useContext: detected via _currentValue / _currentValue2 property (React context internals)
|
|
1116
|
+
// Context consumers in React store the context VALUE in memoizedState directly, not
|
|
1117
|
+
// via a queue. Context objects themselves have _currentValue.
|
|
1118
|
+
// We check if memoizedState could be a context value by seeing if node has no queue
|
|
1119
|
+
// and memoizedState is not an array and not an effect.
|
|
1120
|
+
if (!node.queue && !isRefNode(node) && !isMemoTuple(node) && !isEffectNode(node)) {
|
|
1121
|
+
profile.type = "custom";
|
|
1122
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1123
|
+
return profile;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return profile;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Walk the fiber tree
|
|
1130
|
+
var FunctionComponent = 0;
|
|
1131
|
+
var ClassComponent = 1;
|
|
1132
|
+
var ForwardRef = 11;
|
|
1133
|
+
var MemoComponent = 14;
|
|
1134
|
+
var SimpleMemoComponent = 15;
|
|
1135
|
+
var HostRoot = 3;
|
|
1136
|
+
|
|
1137
|
+
function getFiberName(fiber) {
|
|
1138
|
+
if (!fiber.type) return null;
|
|
1139
|
+
if (typeof fiber.type === "string") return fiber.type;
|
|
1140
|
+
if (typeof fiber.type === "function") return fiber.type.displayName || fiber.type.name || "Anonymous";
|
|
1141
|
+
if (fiber.type.displayName) return fiber.type.displayName;
|
|
1142
|
+
if (fiber.type.render) {
|
|
1143
|
+
return fiber.type.render.displayName || fiber.type.render.name || "ForwardRef";
|
|
1144
|
+
}
|
|
1145
|
+
if (fiber.type.type) {
|
|
1146
|
+
return (fiber.type.type.displayName || fiber.type.type.name || "Memo");
|
|
1147
|
+
}
|
|
1148
|
+
return "Unknown";
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function isComponentFiber(fiber) {
|
|
1152
|
+
var tag = fiber.tag;
|
|
1153
|
+
return tag === FunctionComponent || tag === ClassComponent ||
|
|
1154
|
+
tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function walkFiber(fiber) {
|
|
1158
|
+
if (!fiber) return;
|
|
1159
|
+
|
|
1160
|
+
if (isComponentFiber(fiber)) {
|
|
1161
|
+
var name = getFiberName(fiber);
|
|
1162
|
+
if (name) {
|
|
1163
|
+
var hooks = [];
|
|
1164
|
+
var hookNode = fiber.memoizedState;
|
|
1165
|
+
var idx = 0;
|
|
1166
|
+
while (hookNode !== null && hookNode !== undefined) {
|
|
1167
|
+
hooks.push(classifyHookNode(hookNode, idx));
|
|
1168
|
+
hookNode = hookNode.next;
|
|
1169
|
+
idx++;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
var source = null;
|
|
1173
|
+
if (fiber._debugSource) {
|
|
1174
|
+
source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (hooks.length > 0) {
|
|
1178
|
+
components.push({ name: name, source: source, hooks: hooks });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
walkFiber(fiber.child);
|
|
1184
|
+
walkFiber(fiber.sibling);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
walkFiber(current.child);
|
|
1188
|
+
|
|
1189
|
+
return { components: components };
|
|
1190
|
+
})();
|
|
1191
|
+
`;
|
|
1192
|
+
}
|
|
1193
|
+
function analyzeHookFlags(hooks) {
|
|
1194
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1195
|
+
for (const hook of hooks) {
|
|
1196
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.dependencyValues !== void 0 && hook.dependencyValues === null) {
|
|
1197
|
+
flags.add("EFFECT_EVERY_RENDER");
|
|
1198
|
+
}
|
|
1199
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.cleanupPresence === false && hook.dependencyValues === null) {
|
|
1200
|
+
flags.add("MISSING_CLEANUP");
|
|
1201
|
+
}
|
|
1202
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.dependencyValues === null) {
|
|
1203
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1204
|
+
}
|
|
1205
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.cacheHitRate !== void 0 && hook.cacheHitRate === 0 && hook.recomputeCount !== void 0 && hook.recomputeCount > 1) {
|
|
1206
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1207
|
+
}
|
|
1208
|
+
if ((hook.type === "useState" || hook.type === "useReducer") && hook.updateCount !== void 0 && hook.updateCount > 10) {
|
|
1209
|
+
flags.add("STATE_UPDATE_LOOP");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return [...flags];
|
|
1213
|
+
}
|
|
1214
|
+
async function runHooksProfiling(componentName, filePath, props) {
|
|
1215
|
+
const browser = await chromium2.launch({ headless: true });
|
|
1216
|
+
try {
|
|
1217
|
+
const context = await browser.newContext();
|
|
1218
|
+
const page = await context.newPage();
|
|
1219
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1220
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1221
|
+
await page.waitForFunction(
|
|
1222
|
+
() => {
|
|
1223
|
+
const w = window;
|
|
1224
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1225
|
+
},
|
|
1226
|
+
{ timeout: 15e3 }
|
|
1227
|
+
);
|
|
1228
|
+
const renderError = await page.evaluate(() => {
|
|
1229
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1230
|
+
});
|
|
1231
|
+
if (renderError !== null) {
|
|
1232
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1233
|
+
}
|
|
1234
|
+
const instrumentScript = buildHookInstrumentationScript();
|
|
1235
|
+
const raw = await page.evaluate(instrumentScript);
|
|
1236
|
+
const result = raw;
|
|
1237
|
+
if (result.error) {
|
|
1238
|
+
throw new Error(`Hook instrumentation failed: ${result.error}`);
|
|
1239
|
+
}
|
|
1240
|
+
const rawComponents = result.components ?? [];
|
|
1241
|
+
const components = rawComponents.map((c) => ({
|
|
1242
|
+
name: c.name,
|
|
1243
|
+
source: c.source,
|
|
1244
|
+
hooks: c.hooks,
|
|
1245
|
+
flags: analyzeHookFlags(c.hooks)
|
|
1246
|
+
}));
|
|
1247
|
+
const allFlags = /* @__PURE__ */ new Set();
|
|
1248
|
+
for (const comp of components) {
|
|
1249
|
+
for (const flag of comp.flags) {
|
|
1250
|
+
allFlags.add(flag);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
component: componentName,
|
|
1255
|
+
components,
|
|
1256
|
+
flags: [...allFlags]
|
|
1257
|
+
};
|
|
1258
|
+
} finally {
|
|
1259
|
+
await browser.close();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function createInstrumentHooksCommand() {
|
|
1263
|
+
const cmd = new Cmd("hooks").description(
|
|
1264
|
+
"Profile per-hook-instance data for a component: update counts, cache hit rates, effect counts, and more"
|
|
1265
|
+
).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(
|
|
1266
|
+
async (componentName, opts) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const manifest = loadManifest(opts.manifest);
|
|
1269
|
+
const descriptor = manifest.components[componentName];
|
|
1270
|
+
if (descriptor === void 0) {
|
|
1271
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
`Component "${componentName}" not found in manifest.
|
|
1274
|
+
Available: ${available}`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
let props = {};
|
|
1278
|
+
try {
|
|
1279
|
+
props = JSON.parse(opts.props);
|
|
1280
|
+
} catch {
|
|
1281
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1282
|
+
}
|
|
1283
|
+
const rootDir = process.cwd();
|
|
1284
|
+
const filePath = resolve2(rootDir, descriptor.filePath);
|
|
1285
|
+
process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
|
|
1286
|
+
`);
|
|
1287
|
+
const result = await runHooksProfiling(componentName, filePath, props);
|
|
1288
|
+
if (opts.showFlags) {
|
|
1289
|
+
if (result.flags.length === 0) {
|
|
1290
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1291
|
+
} else {
|
|
1292
|
+
for (const flag of result.flags) {
|
|
1293
|
+
process.stdout.write(`${flag}
|
|
1294
|
+
`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1300
|
+
`);
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1303
|
+
`);
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
);
|
|
1308
|
+
return cmd;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/instrument/profile.ts
|
|
1312
|
+
import { resolve as resolve3 } from "path";
|
|
1313
|
+
import { Command as Cmd2 } from "commander";
|
|
1314
|
+
import { chromium as chromium3 } from "playwright";
|
|
1315
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
1316
|
+
function buildProfilingSetupScript() {
|
|
1317
|
+
return `
|
|
1318
|
+
(function __scopeProfileSetup() {
|
|
1319
|
+
window.__scopeProfileData = {
|
|
1320
|
+
commitCount: 0,
|
|
1321
|
+
componentNames: new Set(),
|
|
1322
|
+
fiberSnapshots: []
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1326
|
+
if (!hook) return { error: "No DevTools hook" };
|
|
1327
|
+
|
|
1328
|
+
// Wrap onCommitFiberRoot to count commits and collect component names
|
|
1329
|
+
var origCommit = hook.onCommitFiberRoot.bind(hook);
|
|
1330
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1331
|
+
origCommit(rendererID, root, priorityLevel);
|
|
1332
|
+
|
|
1333
|
+
window.__scopeProfileData.commitCount++;
|
|
1334
|
+
|
|
1335
|
+
// Walk the committed tree to collect re-rendered component names
|
|
1336
|
+
var current = root && root.current;
|
|
1337
|
+
if (!current) return;
|
|
1338
|
+
|
|
1339
|
+
function walkFiber(fiber) {
|
|
1340
|
+
if (!fiber) return;
|
|
1341
|
+
var tag = fiber.tag;
|
|
1342
|
+
// FunctionComponent=0, ClassComponent=1, ForwardRef=11, Memo=14, SimpleMemo=15
|
|
1343
|
+
if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
|
|
1344
|
+
var name = null;
|
|
1345
|
+
if (fiber.type) {
|
|
1346
|
+
if (typeof fiber.type === "function") name = fiber.type.displayName || fiber.type.name;
|
|
1347
|
+
else if (fiber.type.displayName) name = fiber.type.displayName;
|
|
1348
|
+
else if (fiber.type.render) name = fiber.type.render.displayName || fiber.type.render.name;
|
|
1349
|
+
else if (fiber.type.type) name = fiber.type.type.displayName || fiber.type.type.name;
|
|
1350
|
+
}
|
|
1351
|
+
// Only count fibers with a positive actualDuration (actually re-rendered this commit)
|
|
1352
|
+
if (name && typeof fiber.actualDuration === "number" && fiber.actualDuration >= 0) {
|
|
1353
|
+
window.__scopeProfileData.componentNames.add(name);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
walkFiber(fiber.child);
|
|
1357
|
+
walkFiber(fiber.sibling);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
var wip = root.current.alternate || root.current;
|
|
1361
|
+
walkFiber(wip.child);
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// Install PerformanceObserver for layout and paint timing
|
|
1365
|
+
window.__scopeLayoutTime = 0;
|
|
1366
|
+
window.__scopePaintTime = 0;
|
|
1367
|
+
window.__scopeLayoutShifts = { count: 0, score: 0 };
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
var observer = new PerformanceObserver(function(list) {
|
|
1371
|
+
for (var entry of list.getEntries()) {
|
|
1372
|
+
if (entry.entryType === "layout-shift") {
|
|
1373
|
+
window.__scopeLayoutShifts.count++;
|
|
1374
|
+
window.__scopeLayoutShifts.score += entry.value || 0;
|
|
1375
|
+
}
|
|
1376
|
+
if (entry.entryType === "longtask") {
|
|
1377
|
+
window.__scopeLayoutTime += entry.duration || 0;
|
|
1378
|
+
}
|
|
1379
|
+
if (entry.entryType === "paint") {
|
|
1380
|
+
window.__scopePaintTime += entry.startTime || 0;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
observer.observe({ entryTypes: ["layout-shift", "longtask", "paint"] });
|
|
1385
|
+
} catch(e) {
|
|
1386
|
+
// PerformanceObserver may not be available in all Playwright contexts
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return { ok: true };
|
|
1390
|
+
})();
|
|
1391
|
+
`;
|
|
1392
|
+
}
|
|
1393
|
+
function buildProfilingCollectScript() {
|
|
1394
|
+
return `
|
|
1395
|
+
(function __scopeProfileCollect() {
|
|
1396
|
+
var data = window.__scopeProfileData;
|
|
1397
|
+
if (!data) return { error: "Profiling not set up" };
|
|
1398
|
+
|
|
1399
|
+
// Estimate wasted renders: use paint entries count as a heuristic for
|
|
1400
|
+
// "components that re-rendered but their subtree output was likely unchanged".
|
|
1401
|
+
// A more accurate method would require React's own wasted-render detection,
|
|
1402
|
+
// which requires React Profiler API. We use a conservative estimate here.
|
|
1403
|
+
var totalCommits = data.commitCount;
|
|
1404
|
+
var uniqueNames = Array.from(data.componentNames);
|
|
1405
|
+
|
|
1406
|
+
// Wasted renders heuristic: if a component is in a subsequent commit (not the initial
|
|
1407
|
+
// mount commit), it *may* have been wasted if it didn't actually need to re-render.
|
|
1408
|
+
// For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
|
|
1409
|
+
// This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
|
|
1410
|
+
var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
|
|
1411
|
+
|
|
1412
|
+
return {
|
|
1413
|
+
commitCount: totalCommits,
|
|
1414
|
+
uniqueComponents: uniqueNames.length,
|
|
1415
|
+
componentNames: uniqueNames,
|
|
1416
|
+
wastedRenders: wastedRenders,
|
|
1417
|
+
layoutTime: window.__scopeLayoutTime || 0,
|
|
1418
|
+
paintTime: window.__scopePaintTime || 0,
|
|
1419
|
+
layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
|
|
1420
|
+
};
|
|
1421
|
+
})();
|
|
1422
|
+
`;
|
|
1423
|
+
}
|
|
1424
|
+
async function replayInteraction(page, steps) {
|
|
1425
|
+
for (const step of steps) {
|
|
1426
|
+
switch (step.action) {
|
|
1427
|
+
case "click":
|
|
1428
|
+
if (step.target) {
|
|
1429
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1430
|
+
process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
|
|
1431
|
+
`);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
case "fill":
|
|
1436
|
+
if (step.target && step.value !== void 0) {
|
|
1437
|
+
await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
|
|
1438
|
+
process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
|
|
1439
|
+
`);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
break;
|
|
1443
|
+
case "hover":
|
|
1444
|
+
if (step.target) {
|
|
1445
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1446
|
+
process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
|
|
1447
|
+
`);
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
case "press":
|
|
1452
|
+
if (step.value) {
|
|
1453
|
+
await page.keyboard.press(step.value);
|
|
1454
|
+
}
|
|
1455
|
+
break;
|
|
1456
|
+
case "wait":
|
|
1457
|
+
await page.waitForTimeout(step.delay ?? 500);
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
|
|
1463
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1464
|
+
if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
|
|
1465
|
+
flags.add("WASTED_RENDER");
|
|
1466
|
+
}
|
|
1467
|
+
if (totalRenders > 10) {
|
|
1468
|
+
flags.add("HIGH_RENDER_COUNT");
|
|
1469
|
+
}
|
|
1470
|
+
if (layoutShifts.cumulativeScore > 0.1) {
|
|
1471
|
+
flags.add("LAYOUT_SHIFT_DETECTED");
|
|
1472
|
+
}
|
|
1473
|
+
if (timing.js > 100) {
|
|
1474
|
+
flags.add("SLOW_INTERACTION");
|
|
1475
|
+
}
|
|
1476
|
+
return [...flags];
|
|
1477
|
+
}
|
|
1478
|
+
async function runInteractionProfile(componentName, filePath, props, interaction) {
|
|
1479
|
+
const browser = await chromium3.launch({ headless: true });
|
|
1480
|
+
try {
|
|
1481
|
+
const context = await browser.newContext();
|
|
1482
|
+
const page = await context.newPage();
|
|
1483
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1484
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1485
|
+
await page.waitForFunction(
|
|
1486
|
+
() => {
|
|
1487
|
+
const w = window;
|
|
1488
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1489
|
+
},
|
|
1490
|
+
{ timeout: 15e3 }
|
|
1491
|
+
);
|
|
1492
|
+
const renderError = await page.evaluate(() => {
|
|
1493
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1494
|
+
});
|
|
1495
|
+
if (renderError !== null) {
|
|
1496
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1497
|
+
}
|
|
1498
|
+
const setupResult = await page.evaluate(buildProfilingSetupScript());
|
|
1499
|
+
const setupData = setupResult;
|
|
1500
|
+
if (setupData.error) {
|
|
1501
|
+
throw new Error(`Profiling setup failed: ${setupData.error}`);
|
|
1502
|
+
}
|
|
1503
|
+
const jsStart = Date.now();
|
|
1504
|
+
if (interaction.length > 0) {
|
|
1505
|
+
process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
|
|
1506
|
+
`);
|
|
1507
|
+
await replayInteraction(page, interaction);
|
|
1508
|
+
await page.waitForTimeout(300);
|
|
1509
|
+
}
|
|
1510
|
+
const jsDuration = Date.now() - jsStart;
|
|
1511
|
+
const collected = await page.evaluate(buildProfilingCollectScript());
|
|
1512
|
+
const profileData = collected;
|
|
1513
|
+
if (profileData.error) {
|
|
1514
|
+
throw new Error(`Profile collection failed: ${profileData.error}`);
|
|
1515
|
+
}
|
|
1516
|
+
const timing = {
|
|
1517
|
+
js: jsDuration,
|
|
1518
|
+
layout: profileData.layoutTime ?? 0,
|
|
1519
|
+
paint: profileData.paintTime ?? 0
|
|
1520
|
+
};
|
|
1521
|
+
const layoutShifts = {
|
|
1522
|
+
count: profileData.layoutShifts?.count ?? 0,
|
|
1523
|
+
cumulativeScore: profileData.layoutShifts?.score ?? 0
|
|
1524
|
+
};
|
|
1525
|
+
const totalRenders = profileData.commitCount ?? 0;
|
|
1526
|
+
const uniqueComponents = profileData.uniqueComponents ?? 0;
|
|
1527
|
+
const wastedRenders = profileData.wastedRenders ?? 0;
|
|
1528
|
+
const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
|
|
1529
|
+
return {
|
|
1530
|
+
component: componentName,
|
|
1531
|
+
totalRenders,
|
|
1532
|
+
uniqueComponents,
|
|
1533
|
+
wastedRenders,
|
|
1534
|
+
timing,
|
|
1535
|
+
layoutShifts,
|
|
1536
|
+
flags,
|
|
1537
|
+
interaction
|
|
1538
|
+
};
|
|
1539
|
+
} finally {
|
|
1540
|
+
await browser.close();
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function createInstrumentProfileCommand() {
|
|
1544
|
+
const cmd = new Cmd2("profile").description(
|
|
1545
|
+
"Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
|
|
1546
|
+
).argument("<component>", "Component name (must exist in the manifest)").option(
|
|
1547
|
+
"--interaction <json>",
|
|
1548
|
+
`Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
|
|
1549
|
+
"[]"
|
|
1550
|
+
).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(
|
|
1551
|
+
async (componentName, opts) => {
|
|
1552
|
+
try {
|
|
1553
|
+
const manifest = loadManifest(opts.manifest);
|
|
1554
|
+
const descriptor = manifest.components[componentName];
|
|
1555
|
+
if (descriptor === void 0) {
|
|
1556
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1557
|
+
throw new Error(
|
|
1558
|
+
`Component "${componentName}" not found in manifest.
|
|
1559
|
+
Available: ${available}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
let props = {};
|
|
1563
|
+
try {
|
|
1564
|
+
props = JSON.parse(opts.props);
|
|
1565
|
+
} catch {
|
|
1566
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1567
|
+
}
|
|
1568
|
+
let interaction = [];
|
|
1569
|
+
try {
|
|
1570
|
+
interaction = JSON.parse(opts.interaction);
|
|
1571
|
+
if (!Array.isArray(interaction)) {
|
|
1572
|
+
throw new Error("Interaction must be a JSON array");
|
|
1573
|
+
}
|
|
1574
|
+
} catch {
|
|
1575
|
+
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
1576
|
+
}
|
|
1577
|
+
const rootDir = process.cwd();
|
|
1578
|
+
const filePath = resolve3(rootDir, descriptor.filePath);
|
|
1579
|
+
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
1580
|
+
`);
|
|
1581
|
+
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
1582
|
+
if (opts.showFlags) {
|
|
1583
|
+
if (result.flags.length === 0) {
|
|
1584
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1585
|
+
} else {
|
|
1586
|
+
for (const flag of result.flags) {
|
|
1587
|
+
process.stdout.write(`${flag}
|
|
1588
|
+
`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1594
|
+
`);
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1597
|
+
`);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
);
|
|
1602
|
+
return cmd;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/instrument/tree.ts
|
|
1606
|
+
import { resolve as resolve4 } from "path";
|
|
1607
|
+
import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
|
|
1608
|
+
import { BrowserPool } from "@agent-scope/render";
|
|
1609
|
+
import { Command as Command3 } from "commander";
|
|
1610
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1611
|
+
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1612
|
+
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1613
|
+
var _pool = null;
|
|
1614
|
+
async function getPool() {
|
|
1615
|
+
if (_pool === null) {
|
|
1616
|
+
_pool = new BrowserPool({
|
|
1617
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1618
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1619
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1620
|
+
});
|
|
1621
|
+
await _pool.init();
|
|
1622
|
+
}
|
|
1623
|
+
return _pool;
|
|
1624
|
+
}
|
|
1625
|
+
async function shutdownPool() {
|
|
1626
|
+
if (_pool !== null) {
|
|
1627
|
+
await _pool.close();
|
|
1628
|
+
_pool = null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
function mapNodeType(node) {
|
|
1632
|
+
if (node.type === "forward_ref") return "forwardRef";
|
|
1633
|
+
if (node.type === "host") return "host";
|
|
1634
|
+
const name = node.name;
|
|
1635
|
+
if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
|
|
1636
|
+
if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
|
|
1637
|
+
return node.type;
|
|
1638
|
+
}
|
|
1639
|
+
function flattenSerializedValue(sv) {
|
|
1640
|
+
if (sv === null || sv === void 0) return null;
|
|
1641
|
+
const v = sv;
|
|
1642
|
+
switch (v.type) {
|
|
1643
|
+
case "null":
|
|
1644
|
+
case "undefined":
|
|
1645
|
+
return null;
|
|
1646
|
+
case "string":
|
|
1647
|
+
case "number":
|
|
1648
|
+
case "boolean":
|
|
1649
|
+
return v.value;
|
|
1650
|
+
case "object": {
|
|
1651
|
+
if (!Array.isArray(v.entries)) return {};
|
|
1652
|
+
const result = {};
|
|
1653
|
+
for (const entry of v.entries) {
|
|
1654
|
+
result[entry.key] = flattenSerializedValue(entry.value);
|
|
1655
|
+
}
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
case "array": {
|
|
1659
|
+
if (!Array.isArray(v.items)) return [];
|
|
1660
|
+
return v.items.map(flattenSerializedValue);
|
|
1661
|
+
}
|
|
1662
|
+
case "function":
|
|
1663
|
+
return "[Function]";
|
|
1664
|
+
case "symbol":
|
|
1665
|
+
return `[Symbol: ${v.description ?? ""}]`;
|
|
1666
|
+
case "circular":
|
|
1667
|
+
return "[Circular]";
|
|
1668
|
+
case "truncated":
|
|
1669
|
+
return `[Truncated: ${v.preview ?? ""}]`;
|
|
1670
|
+
default:
|
|
1671
|
+
return v.preview ?? null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function flattenHookState(hooks) {
|
|
1675
|
+
const result = {};
|
|
1676
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1677
|
+
const hook = hooks[i];
|
|
1678
|
+
if (hook === void 0) continue;
|
|
1679
|
+
const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
|
|
1680
|
+
result[key] = flattenSerializedValue(hook.value);
|
|
1681
|
+
}
|
|
1682
|
+
return result;
|
|
1683
|
+
}
|
|
1684
|
+
function extractContextNames(contexts) {
|
|
1685
|
+
const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1686
|
+
return names;
|
|
1687
|
+
}
|
|
1688
|
+
function anyContextChanged(contexts) {
|
|
1689
|
+
return contexts.some((c) => c.didTriggerRender);
|
|
1690
|
+
}
|
|
1691
|
+
function convertToInstrumentNode(node, depth = 0) {
|
|
1692
|
+
const contexts = extractContextNames(node.context);
|
|
1693
|
+
const contextChanged = anyContextChanged(node.context);
|
|
1694
|
+
const state = flattenHookState(node.state);
|
|
1695
|
+
const propsFlat = flattenSerializedValue(node.props);
|
|
1696
|
+
const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
|
|
1697
|
+
return {
|
|
1698
|
+
component: node.name,
|
|
1699
|
+
type: mapNodeType(node),
|
|
1700
|
+
renderCount: node.renderCount,
|
|
1701
|
+
lastRenderDuration: node.renderDuration,
|
|
1702
|
+
memoized: node.type === "memo",
|
|
1703
|
+
// memoSkipped requires tracking bail-outs across commits — not available from
|
|
1704
|
+
// a single-shot capture. Defaulted to 0.
|
|
1705
|
+
memoSkipped: 0,
|
|
1706
|
+
props,
|
|
1707
|
+
// propsChanged is not tracked in a single-shot capture — would need a diff
|
|
1708
|
+
// between two renders. Defaulted to false.
|
|
1709
|
+
propsChanged: false,
|
|
1710
|
+
state,
|
|
1711
|
+
stateChanged: false,
|
|
1712
|
+
contextChanged,
|
|
1713
|
+
contexts,
|
|
1714
|
+
depth,
|
|
1715
|
+
children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function filterByContext(node, contextName) {
|
|
1719
|
+
const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
|
|
1720
|
+
const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
|
|
1721
|
+
if (!selfMatches && filteredChildren.length === 0) return null;
|
|
1722
|
+
return { ...node, children: filteredChildren };
|
|
1723
|
+
}
|
|
1724
|
+
function filterWastedRenders(node) {
|
|
1725
|
+
const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
|
|
1726
|
+
const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
|
|
1727
|
+
if (!isWasted && filteredChildren.length === 0) return null;
|
|
1728
|
+
return { ...node, children: filteredChildren };
|
|
1729
|
+
}
|
|
1730
|
+
function sortTree(node, sortBy) {
|
|
1731
|
+
const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
|
|
1732
|
+
if (sortBy === "renderCount") return b.renderCount - a.renderCount;
|
|
1733
|
+
return a.depth - b.depth;
|
|
1734
|
+
});
|
|
1735
|
+
return { ...node, children: sortedChildren };
|
|
1736
|
+
}
|
|
1737
|
+
function annotateProviderDepth(node, providerDepth = 0) {
|
|
1738
|
+
const isProvider = node.type === "context.provider";
|
|
1739
|
+
const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
|
|
1740
|
+
return {
|
|
1741
|
+
...node,
|
|
1742
|
+
_providerDepth: providerDepth,
|
|
1743
|
+
children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function limitNodes(root, limit) {
|
|
1747
|
+
let remaining = limit;
|
|
1748
|
+
const clip = (node) => {
|
|
1749
|
+
if (remaining <= 0) return null;
|
|
1750
|
+
remaining--;
|
|
1751
|
+
const clippedChildren = [];
|
|
1752
|
+
for (const child of node.children) {
|
|
1753
|
+
const clipped = clip(child);
|
|
1754
|
+
if (clipped !== null) clippedChildren.push(clipped);
|
|
1755
|
+
}
|
|
1756
|
+
return { ...node, children: clippedChildren };
|
|
1757
|
+
};
|
|
1758
|
+
return clip(root) ?? root;
|
|
1759
|
+
}
|
|
1760
|
+
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1761
|
+
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
1762
|
+
var VERTICAL = "\u2502 ";
|
|
1763
|
+
var EMPTY = " ";
|
|
1764
|
+
function buildTTYLabel(node, showProviderDepth) {
|
|
1765
|
+
const parts = [node.component];
|
|
1766
|
+
switch (node.type) {
|
|
1767
|
+
case "memo":
|
|
1768
|
+
parts.push("[memo]");
|
|
1769
|
+
break;
|
|
1770
|
+
case "forwardRef":
|
|
1771
|
+
parts.push("[forwardRef]");
|
|
1772
|
+
break;
|
|
1773
|
+
case "class":
|
|
1774
|
+
parts.push("[class]");
|
|
1775
|
+
break;
|
|
1776
|
+
case "context.provider":
|
|
1777
|
+
parts.push("[provider]");
|
|
1778
|
+
break;
|
|
1779
|
+
case "context.consumer":
|
|
1780
|
+
parts.push("[consumer]");
|
|
1781
|
+
break;
|
|
1782
|
+
default:
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (node.renderCount > 0) {
|
|
1786
|
+
const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
|
|
1787
|
+
parts.push(`(renders:${node.renderCount}${durationStr})`);
|
|
1788
|
+
}
|
|
1789
|
+
if (node.contexts.length > 0) {
|
|
1790
|
+
parts.push(`[ctx:${node.contexts.join(",")}]`);
|
|
1791
|
+
}
|
|
1792
|
+
if (showProviderDepth) {
|
|
1793
|
+
const pd = node._providerDepth;
|
|
1794
|
+
if (pd !== void 0 && pd > 0) {
|
|
1795
|
+
parts.push(`[pd:${pd}]`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return parts.join(" ");
|
|
922
1799
|
}
|
|
923
|
-
function
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1800
|
+
function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
|
|
1801
|
+
if (node.type === "host") {
|
|
1802
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1803
|
+
const child = node.children[i];
|
|
1804
|
+
if (child !== void 0) {
|
|
1805
|
+
renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const connector = isLast ? LAST_BRANCH : BRANCH;
|
|
1811
|
+
lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
|
|
1812
|
+
const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
|
|
1813
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1814
|
+
const child = node.children[i];
|
|
1815
|
+
if (child !== void 0) {
|
|
1816
|
+
renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function formatInstrumentTree(root, showProviderDepth = false) {
|
|
1821
|
+
const lines = [];
|
|
1822
|
+
if (root.type !== "host") {
|
|
1823
|
+
lines.push(buildTTYLabel(root, showProviderDepth));
|
|
1824
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1825
|
+
const child = root.children[i];
|
|
1826
|
+
if (child !== void 0) {
|
|
1827
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
} else {
|
|
1831
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1832
|
+
const child = root.children[i];
|
|
1833
|
+
if (child !== void 0) {
|
|
1834
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
944
1835
|
}
|
|
945
1836
|
}
|
|
946
1837
|
}
|
|
947
|
-
lines.push("\u2500".repeat(60));
|
|
948
1838
|
return lines.join("\n");
|
|
949
1839
|
}
|
|
950
|
-
function
|
|
951
|
-
|
|
1840
|
+
async function runInstrumentTree(options) {
|
|
1841
|
+
const { componentName, filePath } = options;
|
|
1842
|
+
const pool = await getPool();
|
|
1843
|
+
const slot = await pool.acquire();
|
|
1844
|
+
const { page } = slot;
|
|
1845
|
+
try {
|
|
1846
|
+
await page.addInitScript({ content: getBrowserEntryScript2() });
|
|
1847
|
+
const htmlHarness = await buildComponentHarness(
|
|
1848
|
+
filePath,
|
|
1849
|
+
componentName,
|
|
1850
|
+
{},
|
|
1851
|
+
DEFAULT_VIEWPORT_WIDTH
|
|
1852
|
+
);
|
|
1853
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1854
|
+
await page.waitForFunction(
|
|
1855
|
+
() => {
|
|
1856
|
+
const w = window;
|
|
1857
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1858
|
+
},
|
|
1859
|
+
{ timeout: 15e3 }
|
|
1860
|
+
);
|
|
1861
|
+
const renderError = await page.evaluate(
|
|
1862
|
+
() => window.__SCOPE_RENDER_ERROR__ ?? null
|
|
1863
|
+
);
|
|
1864
|
+
if (renderError !== null) {
|
|
1865
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1866
|
+
}
|
|
1867
|
+
const captureJson = await page.evaluate(async () => {
|
|
1868
|
+
const w = window;
|
|
1869
|
+
if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
|
|
1870
|
+
throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
|
|
1871
|
+
}
|
|
1872
|
+
return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
|
|
1873
|
+
});
|
|
1874
|
+
const captureResult = JSON.parse(captureJson);
|
|
1875
|
+
const componentTree = captureResult.tree;
|
|
1876
|
+
if (componentTree === void 0 || componentTree === null) {
|
|
1877
|
+
throw new Error(`No component tree found for "${componentName}"`);
|
|
1878
|
+
}
|
|
1879
|
+
let instrumentRoot = convertToInstrumentNode(componentTree, 0);
|
|
1880
|
+
if (options.usesContext !== void 0) {
|
|
1881
|
+
const filtered = filterByContext(instrumentRoot, options.usesContext);
|
|
1882
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1883
|
+
}
|
|
1884
|
+
if (options.wastedRenders === true) {
|
|
1885
|
+
const filtered = filterWastedRenders(instrumentRoot);
|
|
1886
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1887
|
+
}
|
|
1888
|
+
if (options.sortBy !== void 0) {
|
|
1889
|
+
instrumentRoot = sortTree(instrumentRoot, options.sortBy);
|
|
1890
|
+
}
|
|
1891
|
+
if (options.providerDepth === true) {
|
|
1892
|
+
instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
|
|
1893
|
+
}
|
|
1894
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
1895
|
+
instrumentRoot = limitNodes(instrumentRoot, options.limit);
|
|
1896
|
+
}
|
|
1897
|
+
return instrumentRoot;
|
|
1898
|
+
} finally {
|
|
1899
|
+
pool.release(slot);
|
|
1900
|
+
}
|
|
952
1901
|
}
|
|
953
|
-
function
|
|
954
|
-
|
|
955
|
-
|
|
1902
|
+
function createInstrumentTreeCommand() {
|
|
1903
|
+
return new Command3("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
1904
|
+
"--wasted-renders",
|
|
1905
|
+
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1906
|
+
false
|
|
1907
|
+
).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
|
|
1908
|
+
try {
|
|
1909
|
+
const manifest = loadManifest(opts.manifest);
|
|
1910
|
+
const descriptor = manifest.components[componentName];
|
|
1911
|
+
if (descriptor === void 0) {
|
|
1912
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1913
|
+
throw new Error(
|
|
1914
|
+
`Component "${componentName}" not found in manifest.
|
|
1915
|
+
Available: ${available}`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if (opts.sortBy !== void 0) {
|
|
1919
|
+
const allowed = ["renderCount", "depth"];
|
|
1920
|
+
if (!allowed.includes(opts.sortBy)) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
const rootDir = process.cwd();
|
|
1927
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
1928
|
+
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1929
|
+
`);
|
|
1930
|
+
const instrumentRoot = await runInstrumentTree({
|
|
1931
|
+
componentName,
|
|
1932
|
+
filePath,
|
|
1933
|
+
sortBy: opts.sortBy,
|
|
1934
|
+
limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
|
|
1935
|
+
usesContext: opts.usesContext,
|
|
1936
|
+
providerDepth: opts.providerDepth,
|
|
1937
|
+
wastedRenders: opts.wastedRenders
|
|
1938
|
+
});
|
|
1939
|
+
await shutdownPool();
|
|
1940
|
+
const fmt = resolveFormat2(opts.format);
|
|
1941
|
+
if (fmt === "json") {
|
|
1942
|
+
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1943
|
+
`);
|
|
1944
|
+
} else {
|
|
1945
|
+
const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
|
|
1946
|
+
process.stdout.write(`${tree}
|
|
1947
|
+
`);
|
|
1948
|
+
}
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
await shutdownPool();
|
|
1951
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1952
|
+
`);
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
function resolveFormat2(formatFlag) {
|
|
1958
|
+
if (formatFlag !== void 0) {
|
|
1959
|
+
const lower = formatFlag.toLowerCase();
|
|
1960
|
+
if (lower !== "json" && lower !== "tree") {
|
|
1961
|
+
throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
|
|
1962
|
+
}
|
|
1963
|
+
return lower;
|
|
956
1964
|
}
|
|
957
|
-
return
|
|
1965
|
+
return isTTY() ? "tree" : "json";
|
|
958
1966
|
}
|
|
959
1967
|
|
|
960
1968
|
// src/instrument/renders.ts
|
|
961
|
-
var
|
|
1969
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
962
1970
|
function determineTrigger(event) {
|
|
963
1971
|
if (event.forceUpdate) return "force_update";
|
|
964
1972
|
if (event.stateChanged) return "state_change";
|
|
@@ -1203,7 +2211,7 @@ function buildInstrumentationScript() {
|
|
|
1203
2211
|
`
|
|
1204
2212
|
);
|
|
1205
2213
|
}
|
|
1206
|
-
async function
|
|
2214
|
+
async function replayInteraction2(page, steps) {
|
|
1207
2215
|
for (const step of steps) {
|
|
1208
2216
|
switch (step.action) {
|
|
1209
2217
|
case "click":
|
|
@@ -1259,26 +2267,26 @@ async function replayInteraction(page, steps) {
|
|
|
1259
2267
|
}
|
|
1260
2268
|
}
|
|
1261
2269
|
}
|
|
1262
|
-
var
|
|
1263
|
-
async function
|
|
1264
|
-
if (
|
|
1265
|
-
|
|
2270
|
+
var _pool2 = null;
|
|
2271
|
+
async function getPool2() {
|
|
2272
|
+
if (_pool2 === null) {
|
|
2273
|
+
_pool2 = new BrowserPool2({
|
|
1266
2274
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1267
2275
|
viewportWidth: 1280,
|
|
1268
2276
|
viewportHeight: 800
|
|
1269
2277
|
});
|
|
1270
|
-
await
|
|
2278
|
+
await _pool2.init();
|
|
1271
2279
|
}
|
|
1272
|
-
return
|
|
2280
|
+
return _pool2;
|
|
1273
2281
|
}
|
|
1274
|
-
async function
|
|
1275
|
-
if (
|
|
1276
|
-
await
|
|
1277
|
-
|
|
2282
|
+
async function shutdownPool2() {
|
|
2283
|
+
if (_pool2 !== null) {
|
|
2284
|
+
await _pool2.close();
|
|
2285
|
+
_pool2 = null;
|
|
1278
2286
|
}
|
|
1279
2287
|
}
|
|
1280
2288
|
async function analyzeRenders(options) {
|
|
1281
|
-
const manifestPath = options.manifestPath ??
|
|
2289
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
|
|
1282
2290
|
const manifest = loadManifest(manifestPath);
|
|
1283
2291
|
const descriptor = manifest.components[options.componentName];
|
|
1284
2292
|
if (descriptor === void 0) {
|
|
@@ -1289,9 +2297,9 @@ Available: ${available}`
|
|
|
1289
2297
|
);
|
|
1290
2298
|
}
|
|
1291
2299
|
const rootDir = process.cwd();
|
|
1292
|
-
const filePath =
|
|
2300
|
+
const filePath = resolve5(rootDir, descriptor.filePath);
|
|
1293
2301
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1294
|
-
const pool = await
|
|
2302
|
+
const pool = await getPool2();
|
|
1295
2303
|
const slot = await pool.acquire();
|
|
1296
2304
|
const { page } = slot;
|
|
1297
2305
|
const startMs = performance.now();
|
|
@@ -1307,7 +2315,7 @@ Available: ${available}`
|
|
|
1307
2315
|
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1308
2316
|
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1309
2317
|
});
|
|
1310
|
-
await
|
|
2318
|
+
await replayInteraction2(page, options.interaction);
|
|
1311
2319
|
await page.waitForTimeout(200);
|
|
1312
2320
|
const interactionDurationMs = performance.now() - startMs;
|
|
1313
2321
|
const rawEvents = await page.evaluate(() => {
|
|
@@ -1372,11 +2380,11 @@ function formatRendersTable(result) {
|
|
|
1372
2380
|
return lines.join("\n");
|
|
1373
2381
|
}
|
|
1374
2382
|
function createInstrumentRendersCommand() {
|
|
1375
|
-
return new
|
|
2383
|
+
return new Command4("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(
|
|
1376
2384
|
"--interaction <json>",
|
|
1377
2385
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1378
2386
|
"[]"
|
|
1379
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2387
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1380
2388
|
async (componentName, opts) => {
|
|
1381
2389
|
let interaction = [];
|
|
1382
2390
|
try {
|
|
@@ -1399,7 +2407,7 @@ function createInstrumentRendersCommand() {
|
|
|
1399
2407
|
interaction,
|
|
1400
2408
|
manifestPath: opts.manifest
|
|
1401
2409
|
});
|
|
1402
|
-
await
|
|
2410
|
+
await shutdownPool2();
|
|
1403
2411
|
if (opts.json || !isTTY()) {
|
|
1404
2412
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1405
2413
|
`);
|
|
@@ -1408,7 +2416,7 @@ function createInstrumentRendersCommand() {
|
|
|
1408
2416
|
`);
|
|
1409
2417
|
}
|
|
1410
2418
|
} catch (err) {
|
|
1411
|
-
await
|
|
2419
|
+
await shutdownPool2();
|
|
1412
2420
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1413
2421
|
`);
|
|
1414
2422
|
process.exit(1);
|
|
@@ -1417,32 +2425,35 @@ function createInstrumentRendersCommand() {
|
|
|
1417
2425
|
);
|
|
1418
2426
|
}
|
|
1419
2427
|
function createInstrumentCommand() {
|
|
1420
|
-
const instrumentCmd = new
|
|
2428
|
+
const instrumentCmd = new Command4("instrument").description(
|
|
1421
2429
|
"Structured instrumentation commands for React component analysis"
|
|
1422
2430
|
);
|
|
1423
2431
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2432
|
+
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2433
|
+
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
2434
|
+
instrumentCmd.addCommand(createInstrumentTreeCommand());
|
|
1424
2435
|
return instrumentCmd;
|
|
1425
2436
|
}
|
|
1426
2437
|
|
|
1427
2438
|
// src/render-commands.ts
|
|
1428
2439
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1429
|
-
import { resolve as
|
|
2440
|
+
import { resolve as resolve7 } from "path";
|
|
1430
2441
|
import {
|
|
1431
2442
|
ALL_CONTEXT_IDS,
|
|
1432
2443
|
ALL_STRESS_IDS,
|
|
1433
|
-
BrowserPool as
|
|
2444
|
+
BrowserPool as BrowserPool3,
|
|
1434
2445
|
contextAxis,
|
|
1435
2446
|
RenderMatrix,
|
|
1436
2447
|
SatoriRenderer,
|
|
1437
2448
|
safeRender,
|
|
1438
2449
|
stressAxis
|
|
1439
2450
|
} from "@agent-scope/render";
|
|
1440
|
-
import { Command as
|
|
2451
|
+
import { Command as Command5 } from "commander";
|
|
1441
2452
|
|
|
1442
2453
|
// src/tailwind-css.ts
|
|
1443
2454
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1444
2455
|
import { createRequire } from "module";
|
|
1445
|
-
import { resolve as
|
|
2456
|
+
import { resolve as resolve6 } from "path";
|
|
1446
2457
|
var CONFIG_FILENAMES = [
|
|
1447
2458
|
".reactscope/config.json",
|
|
1448
2459
|
".reactscope/config.js",
|
|
@@ -1459,14 +2470,14 @@ var STYLE_ENTRY_CANDIDATES = [
|
|
|
1459
2470
|
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
1460
2471
|
var compilerCache = null;
|
|
1461
2472
|
function getCachedBuild(cwd) {
|
|
1462
|
-
if (compilerCache !== null &&
|
|
2473
|
+
if (compilerCache !== null && resolve6(compilerCache.cwd) === resolve6(cwd)) {
|
|
1463
2474
|
return compilerCache.build;
|
|
1464
2475
|
}
|
|
1465
2476
|
return null;
|
|
1466
2477
|
}
|
|
1467
2478
|
function findStylesEntry(cwd) {
|
|
1468
2479
|
for (const name of CONFIG_FILENAMES) {
|
|
1469
|
-
const p =
|
|
2480
|
+
const p = resolve6(cwd, name);
|
|
1470
2481
|
if (!existsSync4(p)) continue;
|
|
1471
2482
|
try {
|
|
1472
2483
|
if (name.endsWith(".json")) {
|
|
@@ -1475,28 +2486,28 @@ function findStylesEntry(cwd) {
|
|
|
1475
2486
|
const scope = data.scope;
|
|
1476
2487
|
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
1477
2488
|
if (typeof entry === "string") {
|
|
1478
|
-
const full =
|
|
2489
|
+
const full = resolve6(cwd, entry);
|
|
1479
2490
|
if (existsSync4(full)) return full;
|
|
1480
2491
|
}
|
|
1481
2492
|
}
|
|
1482
2493
|
} catch {
|
|
1483
2494
|
}
|
|
1484
2495
|
}
|
|
1485
|
-
const pkgPath =
|
|
2496
|
+
const pkgPath = resolve6(cwd, "package.json");
|
|
1486
2497
|
if (existsSync4(pkgPath)) {
|
|
1487
2498
|
try {
|
|
1488
2499
|
const raw = readFileSync4(pkgPath, "utf-8");
|
|
1489
2500
|
const pkg = JSON.parse(raw);
|
|
1490
2501
|
const entry = pkg.scope?.stylesEntry;
|
|
1491
2502
|
if (typeof entry === "string") {
|
|
1492
|
-
const full =
|
|
2503
|
+
const full = resolve6(cwd, entry);
|
|
1493
2504
|
if (existsSync4(full)) return full;
|
|
1494
2505
|
}
|
|
1495
2506
|
} catch {
|
|
1496
2507
|
}
|
|
1497
2508
|
}
|
|
1498
2509
|
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
1499
|
-
const full =
|
|
2510
|
+
const full = resolve6(cwd, candidate);
|
|
1500
2511
|
if (existsSync4(full)) {
|
|
1501
2512
|
try {
|
|
1502
2513
|
const content = readFileSync4(full, "utf-8");
|
|
@@ -1514,7 +2525,7 @@ async function getTailwindCompiler(cwd) {
|
|
|
1514
2525
|
if (entryPath === null) return null;
|
|
1515
2526
|
let compile;
|
|
1516
2527
|
try {
|
|
1517
|
-
const require2 = createRequire(
|
|
2528
|
+
const require2 = createRequire(resolve6(cwd, "package.json"));
|
|
1518
2529
|
const tailwind = require2("tailwindcss");
|
|
1519
2530
|
const fn = tailwind.compile;
|
|
1520
2531
|
if (typeof fn !== "function") return null;
|
|
@@ -1525,8 +2536,8 @@ async function getTailwindCompiler(cwd) {
|
|
|
1525
2536
|
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
1526
2537
|
const loadStylesheet = async (id, base) => {
|
|
1527
2538
|
if (id === "tailwindcss") {
|
|
1528
|
-
const nodeModules =
|
|
1529
|
-
const tailwindCssPath =
|
|
2539
|
+
const nodeModules = resolve6(cwd, "node_modules");
|
|
2540
|
+
const tailwindCssPath = resolve6(nodeModules, "tailwindcss", "index.css");
|
|
1530
2541
|
if (!existsSync4(tailwindCssPath)) {
|
|
1531
2542
|
throw new Error(
|
|
1532
2543
|
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
@@ -1535,10 +2546,10 @@ async function getTailwindCompiler(cwd) {
|
|
|
1535
2546
|
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
1536
2547
|
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
1537
2548
|
}
|
|
1538
|
-
const full =
|
|
2549
|
+
const full = resolve6(base, id);
|
|
1539
2550
|
if (existsSync4(full)) {
|
|
1540
2551
|
const content = readFileSync4(full, "utf-8");
|
|
1541
|
-
return { path: full, base:
|
|
2552
|
+
return { path: full, base: resolve6(full, ".."), content };
|
|
1542
2553
|
}
|
|
1543
2554
|
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
1544
2555
|
};
|
|
@@ -1560,24 +2571,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
1560
2571
|
}
|
|
1561
2572
|
|
|
1562
2573
|
// src/render-commands.ts
|
|
1563
|
-
var
|
|
2574
|
+
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
1564
2575
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
1565
|
-
var
|
|
1566
|
-
async function
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
2576
|
+
var _pool3 = null;
|
|
2577
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2578
|
+
if (_pool3 === null) {
|
|
2579
|
+
_pool3 = new BrowserPool3({
|
|
1569
2580
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
1570
2581
|
viewportWidth,
|
|
1571
2582
|
viewportHeight
|
|
1572
2583
|
});
|
|
1573
|
-
await
|
|
2584
|
+
await _pool3.init();
|
|
1574
2585
|
}
|
|
1575
|
-
return
|
|
2586
|
+
return _pool3;
|
|
1576
2587
|
}
|
|
1577
|
-
async function
|
|
1578
|
-
if (
|
|
1579
|
-
await
|
|
1580
|
-
|
|
2588
|
+
async function shutdownPool3() {
|
|
2589
|
+
if (_pool3 !== null) {
|
|
2590
|
+
await _pool3.close();
|
|
2591
|
+
_pool3 = null;
|
|
1581
2592
|
}
|
|
1582
2593
|
}
|
|
1583
2594
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -1588,7 +2599,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1588
2599
|
_satori: satori,
|
|
1589
2600
|
async renderCell(props, _complexityClass) {
|
|
1590
2601
|
const startMs = performance.now();
|
|
1591
|
-
const pool = await
|
|
2602
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
1592
2603
|
const htmlHarness = await buildComponentHarness(
|
|
1593
2604
|
filePath,
|
|
1594
2605
|
componentName,
|
|
@@ -1685,7 +2696,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1685
2696
|
};
|
|
1686
2697
|
}
|
|
1687
2698
|
function registerRenderSingle(renderCmd) {
|
|
1688
|
-
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",
|
|
2699
|
+
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_PATH6).action(
|
|
1689
2700
|
async (componentName, opts) => {
|
|
1690
2701
|
try {
|
|
1691
2702
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1707,7 +2718,7 @@ Available: ${available}`
|
|
|
1707
2718
|
}
|
|
1708
2719
|
const { width, height } = parseViewport(opts.viewport);
|
|
1709
2720
|
const rootDir = process.cwd();
|
|
1710
|
-
const filePath =
|
|
2721
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
1711
2722
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
1712
2723
|
process.stderr.write(
|
|
1713
2724
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
@@ -1724,7 +2735,7 @@ Available: ${available}`
|
|
|
1724
2735
|
}
|
|
1725
2736
|
}
|
|
1726
2737
|
);
|
|
1727
|
-
await
|
|
2738
|
+
await shutdownPool3();
|
|
1728
2739
|
if (outcome.crashed) {
|
|
1729
2740
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
1730
2741
|
`);
|
|
@@ -1737,7 +2748,7 @@ Available: ${available}`
|
|
|
1737
2748
|
}
|
|
1738
2749
|
const result = outcome.result;
|
|
1739
2750
|
if (opts.output !== void 0) {
|
|
1740
|
-
const outPath =
|
|
2751
|
+
const outPath = resolve7(process.cwd(), opts.output);
|
|
1741
2752
|
writeFileSync4(outPath, result.screenshot);
|
|
1742
2753
|
process.stdout.write(
|
|
1743
2754
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1751,9 +2762,9 @@ Available: ${available}`
|
|
|
1751
2762
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
1752
2763
|
`);
|
|
1753
2764
|
} else if (fmt === "file") {
|
|
1754
|
-
const dir =
|
|
2765
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1755
2766
|
mkdirSync3(dir, { recursive: true });
|
|
1756
|
-
const outPath =
|
|
2767
|
+
const outPath = resolve7(dir, `${componentName}.png`);
|
|
1757
2768
|
writeFileSync4(outPath, result.screenshot);
|
|
1758
2769
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1759
2770
|
process.stdout.write(
|
|
@@ -1761,9 +2772,9 @@ Available: ${available}`
|
|
|
1761
2772
|
`
|
|
1762
2773
|
);
|
|
1763
2774
|
} else {
|
|
1764
|
-
const dir =
|
|
2775
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1765
2776
|
mkdirSync3(dir, { recursive: true });
|
|
1766
|
-
const outPath =
|
|
2777
|
+
const outPath = resolve7(dir, `${componentName}.png`);
|
|
1767
2778
|
writeFileSync4(outPath, result.screenshot);
|
|
1768
2779
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1769
2780
|
process.stdout.write(
|
|
@@ -1772,7 +2783,7 @@ Available: ${available}`
|
|
|
1772
2783
|
);
|
|
1773
2784
|
}
|
|
1774
2785
|
} catch (err) {
|
|
1775
|
-
await
|
|
2786
|
+
await shutdownPool3();
|
|
1776
2787
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1777
2788
|
`);
|
|
1778
2789
|
process.exit(1);
|
|
@@ -1784,7 +2795,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
1784
2795
|
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(
|
|
1785
2796
|
"--contexts <ids>",
|
|
1786
2797
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
1787
|
-
).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",
|
|
2798
|
+
).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_PATH6).action(
|
|
1788
2799
|
async (componentName, opts) => {
|
|
1789
2800
|
try {
|
|
1790
2801
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1799,7 +2810,7 @@ Available: ${available}`
|
|
|
1799
2810
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
1800
2811
|
const { width, height } = { width: 375, height: 812 };
|
|
1801
2812
|
const rootDir = process.cwd();
|
|
1802
|
-
const filePath =
|
|
2813
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
1803
2814
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
1804
2815
|
const axes = [];
|
|
1805
2816
|
if (opts.axes !== void 0) {
|
|
@@ -1857,7 +2868,7 @@ Available: ${available}`
|
|
|
1857
2868
|
concurrency
|
|
1858
2869
|
});
|
|
1859
2870
|
const result = await matrix.render();
|
|
1860
|
-
await
|
|
2871
|
+
await shutdownPool3();
|
|
1861
2872
|
process.stderr.write(
|
|
1862
2873
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1863
2874
|
`
|
|
@@ -1866,7 +2877,7 @@ Available: ${available}`
|
|
|
1866
2877
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1867
2878
|
const gen = new SpriteSheetGenerator();
|
|
1868
2879
|
const sheet = await gen.generate(result);
|
|
1869
|
-
const spritePath =
|
|
2880
|
+
const spritePath = resolve7(process.cwd(), opts.sprite);
|
|
1870
2881
|
writeFileSync4(spritePath, sheet.png);
|
|
1871
2882
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
1872
2883
|
`);
|
|
@@ -1876,9 +2887,9 @@ Available: ${available}`
|
|
|
1876
2887
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1877
2888
|
const gen = new SpriteSheetGenerator();
|
|
1878
2889
|
const sheet = await gen.generate(result);
|
|
1879
|
-
const dir =
|
|
2890
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1880
2891
|
mkdirSync3(dir, { recursive: true });
|
|
1881
|
-
const outPath =
|
|
2892
|
+
const outPath = resolve7(dir, `${componentName}-matrix.png`);
|
|
1882
2893
|
writeFileSync4(outPath, sheet.png);
|
|
1883
2894
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
1884
2895
|
process.stdout.write(
|
|
@@ -1902,7 +2913,7 @@ Available: ${available}`
|
|
|
1902
2913
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1903
2914
|
}
|
|
1904
2915
|
} catch (err) {
|
|
1905
|
-
await
|
|
2916
|
+
await shutdownPool3();
|
|
1906
2917
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1907
2918
|
`);
|
|
1908
2919
|
process.exit(1);
|
|
@@ -1911,7 +2922,7 @@ Available: ${available}`
|
|
|
1911
2922
|
);
|
|
1912
2923
|
}
|
|
1913
2924
|
function registerRenderAll(renderCmd) {
|
|
1914
|
-
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",
|
|
2925
|
+
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_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1915
2926
|
async (opts) => {
|
|
1916
2927
|
try {
|
|
1917
2928
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1922,7 +2933,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1922
2933
|
return;
|
|
1923
2934
|
}
|
|
1924
2935
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
1925
|
-
const outputDir =
|
|
2936
|
+
const outputDir = resolve7(process.cwd(), opts.outputDir);
|
|
1926
2937
|
mkdirSync3(outputDir, { recursive: true });
|
|
1927
2938
|
const rootDir = process.cwd();
|
|
1928
2939
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
@@ -1932,7 +2943,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1932
2943
|
const renderOne = async (name) => {
|
|
1933
2944
|
const descriptor = manifest.components[name];
|
|
1934
2945
|
if (descriptor === void 0) return;
|
|
1935
|
-
const filePath =
|
|
2946
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
1936
2947
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
1937
2948
|
const outcome = await safeRender(
|
|
1938
2949
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
@@ -1955,7 +2966,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1955
2966
|
success: false,
|
|
1956
2967
|
errorMessage: outcome.error.message
|
|
1957
2968
|
});
|
|
1958
|
-
const errPath =
|
|
2969
|
+
const errPath = resolve7(outputDir, `${name}.error.json`);
|
|
1959
2970
|
writeFileSync4(
|
|
1960
2971
|
errPath,
|
|
1961
2972
|
JSON.stringify(
|
|
@@ -1973,9 +2984,9 @@ function registerRenderAll(renderCmd) {
|
|
|
1973
2984
|
}
|
|
1974
2985
|
const result = outcome.result;
|
|
1975
2986
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
1976
|
-
const pngPath =
|
|
2987
|
+
const pngPath = resolve7(outputDir, `${name}.png`);
|
|
1977
2988
|
writeFileSync4(pngPath, result.screenshot);
|
|
1978
|
-
const jsonPath =
|
|
2989
|
+
const jsonPath = resolve7(outputDir, `${name}.json`);
|
|
1979
2990
|
writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
1980
2991
|
if (isTTY()) {
|
|
1981
2992
|
process.stdout.write(
|
|
@@ -1999,13 +3010,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1999
3010
|
workers.push(worker());
|
|
2000
3011
|
}
|
|
2001
3012
|
await Promise.all(workers);
|
|
2002
|
-
await
|
|
3013
|
+
await shutdownPool3();
|
|
2003
3014
|
process.stderr.write("\n");
|
|
2004
3015
|
const summary = formatSummaryText(results, outputDir);
|
|
2005
3016
|
process.stderr.write(`${summary}
|
|
2006
3017
|
`);
|
|
2007
3018
|
} catch (err) {
|
|
2008
|
-
await
|
|
3019
|
+
await shutdownPool3();
|
|
2009
3020
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2010
3021
|
`);
|
|
2011
3022
|
process.exit(1);
|
|
@@ -2038,7 +3049,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
2038
3049
|
return "json";
|
|
2039
3050
|
}
|
|
2040
3051
|
function createRenderCommand() {
|
|
2041
|
-
const renderCmd = new
|
|
3052
|
+
const renderCmd = new Command5("render").description(
|
|
2042
3053
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
2043
3054
|
);
|
|
2044
3055
|
registerRenderSingle(renderCmd);
|
|
@@ -2049,31 +3060,31 @@ function createRenderCommand() {
|
|
|
2049
3060
|
|
|
2050
3061
|
// src/report/baseline.ts
|
|
2051
3062
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2052
|
-
import { resolve as
|
|
3063
|
+
import { resolve as resolve8 } from "path";
|
|
2053
3064
|
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
2054
|
-
import { BrowserPool as
|
|
3065
|
+
import { BrowserPool as BrowserPool4, safeRender as safeRender2 } from "@agent-scope/render";
|
|
2055
3066
|
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
2056
3067
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2057
|
-
var
|
|
2058
|
-
async function
|
|
2059
|
-
if (
|
|
2060
|
-
|
|
3068
|
+
var _pool4 = null;
|
|
3069
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
3070
|
+
if (_pool4 === null) {
|
|
3071
|
+
_pool4 = new BrowserPool4({
|
|
2061
3072
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2062
3073
|
viewportWidth,
|
|
2063
3074
|
viewportHeight
|
|
2064
3075
|
});
|
|
2065
|
-
await
|
|
3076
|
+
await _pool4.init();
|
|
2066
3077
|
}
|
|
2067
|
-
return
|
|
3078
|
+
return _pool4;
|
|
2068
3079
|
}
|
|
2069
|
-
async function
|
|
2070
|
-
if (
|
|
2071
|
-
await
|
|
2072
|
-
|
|
3080
|
+
async function shutdownPool4() {
|
|
3081
|
+
if (_pool4 !== null) {
|
|
3082
|
+
await _pool4.close();
|
|
3083
|
+
_pool4 = null;
|
|
2073
3084
|
}
|
|
2074
3085
|
}
|
|
2075
3086
|
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2076
|
-
const pool = await
|
|
3087
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2077
3088
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2078
3089
|
const slot = await pool.acquire();
|
|
2079
3090
|
const { page } = slot;
|
|
@@ -2198,20 +3209,20 @@ async function runBaseline(options = {}) {
|
|
|
2198
3209
|
} = options;
|
|
2199
3210
|
const startTime = performance.now();
|
|
2200
3211
|
const rootDir = process.cwd();
|
|
2201
|
-
const baselineDir =
|
|
2202
|
-
const rendersDir =
|
|
3212
|
+
const baselineDir = resolve8(rootDir, outputDir);
|
|
3213
|
+
const rendersDir = resolve8(baselineDir, "renders");
|
|
2203
3214
|
if (existsSync5(baselineDir)) {
|
|
2204
3215
|
rmSync(baselineDir, { recursive: true, force: true });
|
|
2205
3216
|
}
|
|
2206
3217
|
mkdirSync4(rendersDir, { recursive: true });
|
|
2207
3218
|
let manifest;
|
|
2208
3219
|
if (manifestPath !== void 0) {
|
|
2209
|
-
const { readFileSync:
|
|
2210
|
-
const absPath =
|
|
3220
|
+
const { readFileSync: readFileSync8 } = await import("fs");
|
|
3221
|
+
const absPath = resolve8(rootDir, manifestPath);
|
|
2211
3222
|
if (!existsSync5(absPath)) {
|
|
2212
3223
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2213
3224
|
}
|
|
2214
|
-
manifest = JSON.parse(
|
|
3225
|
+
manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2215
3226
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2216
3227
|
`);
|
|
2217
3228
|
} else {
|
|
@@ -2221,7 +3232,7 @@ async function runBaseline(options = {}) {
|
|
|
2221
3232
|
process.stderr.write(`Found ${count} components.
|
|
2222
3233
|
`);
|
|
2223
3234
|
}
|
|
2224
|
-
writeFileSync5(
|
|
3235
|
+
writeFileSync5(resolve8(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2225
3236
|
let componentNames = Object.keys(manifest.components);
|
|
2226
3237
|
if (componentsGlob !== void 0) {
|
|
2227
3238
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -2242,7 +3253,7 @@ async function runBaseline(options = {}) {
|
|
|
2242
3253
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2243
3254
|
};
|
|
2244
3255
|
writeFileSync5(
|
|
2245
|
-
|
|
3256
|
+
resolve8(baselineDir, "compliance.json"),
|
|
2246
3257
|
JSON.stringify(emptyReport, null, 2),
|
|
2247
3258
|
"utf-8"
|
|
2248
3259
|
);
|
|
@@ -2263,7 +3274,7 @@ async function runBaseline(options = {}) {
|
|
|
2263
3274
|
const renderOne = async (name) => {
|
|
2264
3275
|
const descriptor = manifest.components[name];
|
|
2265
3276
|
if (descriptor === void 0) return;
|
|
2266
|
-
const filePath =
|
|
3277
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2267
3278
|
const outcome = await safeRender2(
|
|
2268
3279
|
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2269
3280
|
{
|
|
@@ -2282,7 +3293,7 @@ async function runBaseline(options = {}) {
|
|
|
2282
3293
|
}
|
|
2283
3294
|
if (outcome.crashed) {
|
|
2284
3295
|
failureCount++;
|
|
2285
|
-
const errPath =
|
|
3296
|
+
const errPath = resolve8(rendersDir, `${name}.error.json`);
|
|
2286
3297
|
writeFileSync5(
|
|
2287
3298
|
errPath,
|
|
2288
3299
|
JSON.stringify(
|
|
@@ -2300,10 +3311,10 @@ async function runBaseline(options = {}) {
|
|
|
2300
3311
|
return;
|
|
2301
3312
|
}
|
|
2302
3313
|
const result = outcome.result;
|
|
2303
|
-
writeFileSync5(
|
|
3314
|
+
writeFileSync5(resolve8(rendersDir, `${name}.png`), result.screenshot);
|
|
2304
3315
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2305
3316
|
writeFileSync5(
|
|
2306
|
-
|
|
3317
|
+
resolve8(rendersDir, `${name}.json`),
|
|
2307
3318
|
JSON.stringify(jsonOutput, null, 2),
|
|
2308
3319
|
"utf-8"
|
|
2309
3320
|
);
|
|
@@ -2323,7 +3334,7 @@ async function runBaseline(options = {}) {
|
|
|
2323
3334
|
workers.push(worker());
|
|
2324
3335
|
}
|
|
2325
3336
|
await Promise.all(workers);
|
|
2326
|
-
await
|
|
3337
|
+
await shutdownPool4();
|
|
2327
3338
|
if (isTTY()) {
|
|
2328
3339
|
process.stderr.write("\n");
|
|
2329
3340
|
}
|
|
@@ -2331,7 +3342,7 @@ async function runBaseline(options = {}) {
|
|
|
2331
3342
|
const engine = new ComplianceEngine(resolver);
|
|
2332
3343
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2333
3344
|
writeFileSync5(
|
|
2334
|
-
|
|
3345
|
+
resolve8(baselineDir, "compliance.json"),
|
|
2335
3346
|
JSON.stringify(batchReport, null, 2),
|
|
2336
3347
|
"utf-8"
|
|
2337
3348
|
);
|
|
@@ -2374,10 +3385,10 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
2374
3385
|
}
|
|
2375
3386
|
|
|
2376
3387
|
// src/tree-formatter.ts
|
|
2377
|
-
var
|
|
2378
|
-
var
|
|
2379
|
-
var
|
|
2380
|
-
var
|
|
3388
|
+
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3389
|
+
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
3390
|
+
var VERTICAL2 = "\u2502 ";
|
|
3391
|
+
var EMPTY2 = " ";
|
|
2381
3392
|
function buildLabel(node, options) {
|
|
2382
3393
|
const parts = [node.name];
|
|
2383
3394
|
if (node.type === "memo") {
|
|
@@ -2425,19 +3436,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
|
|
|
2425
3436
|
}
|
|
2426
3437
|
return;
|
|
2427
3438
|
}
|
|
2428
|
-
const connector = isLast ?
|
|
3439
|
+
const connector = isLast ? LAST_BRANCH2 : BRANCH2;
|
|
2429
3440
|
const label = buildLabel(node, options);
|
|
2430
3441
|
lines.push(`${prefix}${connector}${label}`);
|
|
2431
3442
|
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
2432
3443
|
const childCount = countVisibleDescendants(node, options);
|
|
2433
3444
|
if (childCount > 0) {
|
|
2434
|
-
const nextPrefix2 = prefix + (isLast ?
|
|
2435
|
-
lines.push(`${nextPrefix2}${
|
|
3445
|
+
const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3446
|
+
lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
2436
3447
|
}
|
|
2437
3448
|
return;
|
|
2438
3449
|
}
|
|
2439
3450
|
const visibleChildren = getVisibleChildren(node, options);
|
|
2440
|
-
const nextPrefix = prefix + (isLast ?
|
|
3451
|
+
const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
2441
3452
|
for (let i = 0; i < visibleChildren.length; i++) {
|
|
2442
3453
|
const child = visibleChildren[i];
|
|
2443
3454
|
if (child !== void 0) {
|
|
@@ -2479,7 +3490,7 @@ function formatTree(root, options = {}) {
|
|
|
2479
3490
|
if (options.maxDepth === 0) {
|
|
2480
3491
|
const childCount = countVisibleDescendants(root, options);
|
|
2481
3492
|
if (childCount > 0) {
|
|
2482
|
-
lines.push(`${
|
|
3493
|
+
lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
2483
3494
|
}
|
|
2484
3495
|
} else {
|
|
2485
3496
|
const visibleChildren = getVisibleChildren(root, options);
|
|
@@ -2653,18 +3664,132 @@ function buildStructuredReport(report) {
|
|
|
2653
3664
|
}
|
|
2654
3665
|
|
|
2655
3666
|
// src/tokens/commands.ts
|
|
2656
|
-
import { existsSync as
|
|
2657
|
-
import { resolve as
|
|
3667
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
3668
|
+
import { resolve as resolve10 } from "path";
|
|
2658
3669
|
import {
|
|
2659
|
-
parseTokenFileSync,
|
|
3670
|
+
parseTokenFileSync as parseTokenFileSync2,
|
|
2660
3671
|
TokenParseError,
|
|
2661
|
-
TokenResolver as
|
|
3672
|
+
TokenResolver as TokenResolver3,
|
|
2662
3673
|
TokenValidationError,
|
|
2663
3674
|
validateTokenFile
|
|
2664
3675
|
} from "@agent-scope/tokens";
|
|
2665
|
-
import { Command as
|
|
3676
|
+
import { Command as Command7 } from "commander";
|
|
3677
|
+
|
|
3678
|
+
// src/tokens/export.ts
|
|
3679
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3680
|
+
import { resolve as resolve9 } from "path";
|
|
3681
|
+
import {
|
|
3682
|
+
exportTokens,
|
|
3683
|
+
parseTokenFileSync,
|
|
3684
|
+
ThemeResolver,
|
|
3685
|
+
TokenResolver as TokenResolver2
|
|
3686
|
+
} from "@agent-scope/tokens";
|
|
3687
|
+
import { Command as Command6 } from "commander";
|
|
2666
3688
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2667
3689
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3690
|
+
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3691
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3692
|
+
if (fileFlag !== void 0) {
|
|
3693
|
+
return resolve9(process.cwd(), fileFlag);
|
|
3694
|
+
}
|
|
3695
|
+
const configPath = resolve9(process.cwd(), CONFIG_FILE);
|
|
3696
|
+
if (existsSync6(configPath)) {
|
|
3697
|
+
try {
|
|
3698
|
+
const raw = readFileSync5(configPath, "utf-8");
|
|
3699
|
+
const config = JSON.parse(raw);
|
|
3700
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3701
|
+
const file = config.tokens.file;
|
|
3702
|
+
return resolve9(process.cwd(), file);
|
|
3703
|
+
}
|
|
3704
|
+
} catch {
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
return resolve9(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3708
|
+
}
|
|
3709
|
+
function createTokensExportCommand() {
|
|
3710
|
+
return new Command6("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(
|
|
3711
|
+
"--theme <name>",
|
|
3712
|
+
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3713
|
+
).action(
|
|
3714
|
+
(opts) => {
|
|
3715
|
+
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
3716
|
+
process.stderr.write(
|
|
3717
|
+
`Error: unsupported format "${opts.format}".
|
|
3718
|
+
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
3719
|
+
`
|
|
3720
|
+
);
|
|
3721
|
+
process.exit(1);
|
|
3722
|
+
}
|
|
3723
|
+
const format = opts.format;
|
|
3724
|
+
try {
|
|
3725
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3726
|
+
if (!existsSync6(filePath)) {
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
`Token file not found at ${filePath}.
|
|
3729
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
3733
|
+
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3734
|
+
let themesMap;
|
|
3735
|
+
if (opts.theme !== void 0) {
|
|
3736
|
+
if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
|
|
3737
|
+
const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
|
|
3738
|
+
throw new Error(
|
|
3739
|
+
`Theme "${opts.theme}" not found in token file.
|
|
3740
|
+
Available themes: ${available}`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
const baseResolver = new TokenResolver2(tokens);
|
|
3744
|
+
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3745
|
+
baseResolver,
|
|
3746
|
+
rawFile
|
|
3747
|
+
);
|
|
3748
|
+
const themeNames = themeResolver.listThemes();
|
|
3749
|
+
if (!themeNames.includes(opts.theme)) {
|
|
3750
|
+
throw new Error(
|
|
3751
|
+
`Theme "${opts.theme}" could not be resolved.
|
|
3752
|
+
Available themes: ${themeNames.join(", ")}`
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
const themedTokens = themeResolver.buildThemedTokens(opts.theme);
|
|
3756
|
+
const overrideMap = /* @__PURE__ */ new Map();
|
|
3757
|
+
for (const themedToken of themedTokens) {
|
|
3758
|
+
const baseToken = tokens.find((t) => t.path === themedToken.path);
|
|
3759
|
+
if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
|
|
3760
|
+
overrideMap.set(themedToken.path, themedToken.resolvedValue);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
|
|
3764
|
+
}
|
|
3765
|
+
const output = exportTokens(tokens, format, {
|
|
3766
|
+
prefix: opts.prefix,
|
|
3767
|
+
rootSelector: opts.selector,
|
|
3768
|
+
themes: themesMap
|
|
3769
|
+
});
|
|
3770
|
+
if (opts.out !== void 0) {
|
|
3771
|
+
const outPath = resolve9(process.cwd(), opts.out);
|
|
3772
|
+
writeFileSync6(outPath, output, "utf-8");
|
|
3773
|
+
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3774
|
+
`);
|
|
3775
|
+
} else {
|
|
3776
|
+
process.stdout.write(output);
|
|
3777
|
+
if (!output.endsWith("\n")) {
|
|
3778
|
+
process.stdout.write("\n");
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
} catch (err) {
|
|
3782
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3783
|
+
`);
|
|
3784
|
+
process.exit(1);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// src/tokens/commands.ts
|
|
3791
|
+
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3792
|
+
var CONFIG_FILE2 = "reactscope.config.json";
|
|
2668
3793
|
function isTTY2() {
|
|
2669
3794
|
return process.stdout.isTTY === true;
|
|
2670
3795
|
}
|
|
@@ -2682,33 +3807,33 @@ function buildTable2(headers, rows) {
|
|
|
2682
3807
|
);
|
|
2683
3808
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2684
3809
|
}
|
|
2685
|
-
function
|
|
3810
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2686
3811
|
if (fileFlag !== void 0) {
|
|
2687
|
-
return
|
|
3812
|
+
return resolve10(process.cwd(), fileFlag);
|
|
2688
3813
|
}
|
|
2689
|
-
const configPath =
|
|
2690
|
-
if (
|
|
3814
|
+
const configPath = resolve10(process.cwd(), CONFIG_FILE2);
|
|
3815
|
+
if (existsSync7(configPath)) {
|
|
2691
3816
|
try {
|
|
2692
|
-
const raw =
|
|
3817
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
2693
3818
|
const config = JSON.parse(raw);
|
|
2694
3819
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
2695
3820
|
const file = config.tokens.file;
|
|
2696
|
-
return
|
|
3821
|
+
return resolve10(process.cwd(), file);
|
|
2697
3822
|
}
|
|
2698
3823
|
} catch {
|
|
2699
3824
|
}
|
|
2700
3825
|
}
|
|
2701
|
-
return
|
|
3826
|
+
return resolve10(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2702
3827
|
}
|
|
2703
3828
|
function loadTokens(absPath) {
|
|
2704
|
-
if (!
|
|
3829
|
+
if (!existsSync7(absPath)) {
|
|
2705
3830
|
throw new Error(
|
|
2706
3831
|
`Token file not found at ${absPath}.
|
|
2707
3832
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2708
3833
|
);
|
|
2709
3834
|
}
|
|
2710
|
-
const raw =
|
|
2711
|
-
return
|
|
3835
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
3836
|
+
return parseTokenFileSync2(raw);
|
|
2712
3837
|
}
|
|
2713
3838
|
function getRawValue(node, segments) {
|
|
2714
3839
|
const [head, ...rest] = segments;
|
|
@@ -2745,9 +3870,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2745
3870
|
function registerGet2(tokensCmd) {
|
|
2746
3871
|
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) => {
|
|
2747
3872
|
try {
|
|
2748
|
-
const filePath =
|
|
3873
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2749
3874
|
const { tokens } = loadTokens(filePath);
|
|
2750
|
-
const resolver = new
|
|
3875
|
+
const resolver = new TokenResolver3(tokens);
|
|
2751
3876
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
2752
3877
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2753
3878
|
if (useJson) {
|
|
@@ -2771,9 +3896,9 @@ function registerList2(tokensCmd) {
|
|
|
2771
3896
|
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(
|
|
2772
3897
|
(category, opts) => {
|
|
2773
3898
|
try {
|
|
2774
|
-
const filePath =
|
|
3899
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2775
3900
|
const { tokens } = loadTokens(filePath);
|
|
2776
|
-
const resolver = new
|
|
3901
|
+
const resolver = new TokenResolver3(tokens);
|
|
2777
3902
|
const filtered = resolver.list(opts.type, category);
|
|
2778
3903
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2779
3904
|
if (useJson) {
|
|
@@ -2801,9 +3926,9 @@ function registerSearch(tokensCmd) {
|
|
|
2801
3926
|
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(
|
|
2802
3927
|
(value, opts) => {
|
|
2803
3928
|
try {
|
|
2804
|
-
const filePath =
|
|
3929
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2805
3930
|
const { tokens } = loadTokens(filePath);
|
|
2806
|
-
const resolver = new
|
|
3931
|
+
const resolver = new TokenResolver3(tokens);
|
|
2807
3932
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2808
3933
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
2809
3934
|
"color",
|
|
@@ -2883,10 +4008,10 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2883
4008
|
function registerResolve(tokensCmd) {
|
|
2884
4009
|
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) => {
|
|
2885
4010
|
try {
|
|
2886
|
-
const filePath =
|
|
4011
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2887
4012
|
const absFilePath = filePath;
|
|
2888
4013
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
2889
|
-
const resolver = new
|
|
4014
|
+
const resolver = new TokenResolver3(tokens);
|
|
2890
4015
|
resolver.resolve(tokenPath);
|
|
2891
4016
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
2892
4017
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -2920,14 +4045,14 @@ function registerValidate(tokensCmd) {
|
|
|
2920
4045
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2921
4046
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2922
4047
|
try {
|
|
2923
|
-
const filePath =
|
|
2924
|
-
if (!
|
|
4048
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
4049
|
+
if (!existsSync7(filePath)) {
|
|
2925
4050
|
throw new Error(
|
|
2926
4051
|
`Token file not found at ${filePath}.
|
|
2927
4052
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2928
4053
|
);
|
|
2929
4054
|
}
|
|
2930
|
-
const raw =
|
|
4055
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
2931
4056
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2932
4057
|
const errors = [];
|
|
2933
4058
|
let parsed;
|
|
@@ -2954,7 +4079,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
2954
4079
|
throw err;
|
|
2955
4080
|
}
|
|
2956
4081
|
try {
|
|
2957
|
-
|
|
4082
|
+
parseTokenFileSync2(raw);
|
|
2958
4083
|
} catch (err) {
|
|
2959
4084
|
if (err instanceof TokenParseError) {
|
|
2960
4085
|
errors.push({ code: err.code, path: err.path, message: err.message });
|
|
@@ -2995,7 +4120,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
2995
4120
|
}
|
|
2996
4121
|
}
|
|
2997
4122
|
function createTokensCommand() {
|
|
2998
|
-
const tokensCmd = new
|
|
4123
|
+
const tokensCmd = new Command7("tokens").description(
|
|
2999
4124
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
3000
4125
|
);
|
|
3001
4126
|
registerGet2(tokensCmd);
|
|
@@ -3003,12 +4128,13 @@ function createTokensCommand() {
|
|
|
3003
4128
|
registerSearch(tokensCmd);
|
|
3004
4129
|
registerResolve(tokensCmd);
|
|
3005
4130
|
registerValidate(tokensCmd);
|
|
4131
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
3006
4132
|
return tokensCmd;
|
|
3007
4133
|
}
|
|
3008
4134
|
|
|
3009
4135
|
// src/program.ts
|
|
3010
4136
|
function createProgram(options = {}) {
|
|
3011
|
-
const program2 = new
|
|
4137
|
+
const program2 = new Command8("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
3012
4138
|
program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
3013
4139
|
async (url, opts) => {
|
|
3014
4140
|
try {
|
|
@@ -3081,7 +4207,7 @@ function createProgram(options = {}) {
|
|
|
3081
4207
|
}
|
|
3082
4208
|
);
|
|
3083
4209
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
3084
|
-
const raw =
|
|
4210
|
+
const raw = readFileSync7(tracePath, "utf-8");
|
|
3085
4211
|
const trace = loadTrace(raw);
|
|
3086
4212
|
const source = generateTest(trace, {
|
|
3087
4213
|
description: opts.description,
|