@agent-scope/cli 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +847 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +961 -217
- 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 +960 -219
- 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 Command7 } 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((resolve10) => {
|
|
259
259
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
260
|
+
resolve10(answer.trim());
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -406,7 +406,7 @@ function createInitCommand() {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
// src/instrument/renders.ts
|
|
409
|
-
import { resolve as
|
|
409
|
+
import { resolve as resolve4 } from "path";
|
|
410
410
|
import { BrowserPool } from "@agent-scope/render";
|
|
411
411
|
import { Command as Command3 } from "commander";
|
|
412
412
|
|
|
@@ -957,8 +957,653 @@ function csvEscape(value) {
|
|
|
957
957
|
return value;
|
|
958
958
|
}
|
|
959
959
|
|
|
960
|
-
// src/instrument/
|
|
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";
|
|
961
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/renders.ts
|
|
1606
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
962
1607
|
function determineTrigger(event) {
|
|
963
1608
|
if (event.forceUpdate) return "force_update";
|
|
964
1609
|
if (event.stateChanged) return "state_change";
|
|
@@ -1203,7 +1848,7 @@ function buildInstrumentationScript() {
|
|
|
1203
1848
|
`
|
|
1204
1849
|
);
|
|
1205
1850
|
}
|
|
1206
|
-
async function
|
|
1851
|
+
async function replayInteraction2(page, steps) {
|
|
1207
1852
|
for (const step of steps) {
|
|
1208
1853
|
switch (step.action) {
|
|
1209
1854
|
case "click":
|
|
@@ -1278,7 +1923,7 @@ async function shutdownPool() {
|
|
|
1278
1923
|
}
|
|
1279
1924
|
}
|
|
1280
1925
|
async function analyzeRenders(options) {
|
|
1281
|
-
const manifestPath = options.manifestPath ??
|
|
1926
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
|
|
1282
1927
|
const manifest = loadManifest(manifestPath);
|
|
1283
1928
|
const descriptor = manifest.components[options.componentName];
|
|
1284
1929
|
if (descriptor === void 0) {
|
|
@@ -1289,7 +1934,7 @@ Available: ${available}`
|
|
|
1289
1934
|
);
|
|
1290
1935
|
}
|
|
1291
1936
|
const rootDir = process.cwd();
|
|
1292
|
-
const filePath =
|
|
1937
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
1293
1938
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1294
1939
|
const pool = await getPool();
|
|
1295
1940
|
const slot = await pool.acquire();
|
|
@@ -1307,7 +1952,7 @@ Available: ${available}`
|
|
|
1307
1952
|
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1308
1953
|
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1309
1954
|
});
|
|
1310
|
-
await
|
|
1955
|
+
await replayInteraction2(page, options.interaction);
|
|
1311
1956
|
await page.waitForTimeout(200);
|
|
1312
1957
|
const interactionDurationMs = performance.now() - startMs;
|
|
1313
1958
|
const rawEvents = await page.evaluate(() => {
|
|
@@ -1376,7 +2021,7 @@ function createInstrumentRendersCommand() {
|
|
|
1376
2021
|
"--interaction <json>",
|
|
1377
2022
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1378
2023
|
"[]"
|
|
1379
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2024
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
|
|
1380
2025
|
async (componentName, opts) => {
|
|
1381
2026
|
let interaction = [];
|
|
1382
2027
|
try {
|
|
@@ -1421,12 +2066,14 @@ function createInstrumentCommand() {
|
|
|
1421
2066
|
"Structured instrumentation commands for React component analysis"
|
|
1422
2067
|
);
|
|
1423
2068
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2069
|
+
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2070
|
+
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
1424
2071
|
return instrumentCmd;
|
|
1425
2072
|
}
|
|
1426
2073
|
|
|
1427
2074
|
// src/render-commands.ts
|
|
1428
2075
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1429
|
-
import { resolve as
|
|
2076
|
+
import { resolve as resolve6 } from "path";
|
|
1430
2077
|
import {
|
|
1431
2078
|
ALL_CONTEXT_IDS,
|
|
1432
2079
|
ALL_STRESS_IDS,
|
|
@@ -1442,7 +2089,7 @@ import { Command as Command4 } from "commander";
|
|
|
1442
2089
|
// src/tailwind-css.ts
|
|
1443
2090
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1444
2091
|
import { createRequire } from "module";
|
|
1445
|
-
import { resolve as
|
|
2092
|
+
import { resolve as resolve5 } from "path";
|
|
1446
2093
|
var CONFIG_FILENAMES = [
|
|
1447
2094
|
".reactscope/config.json",
|
|
1448
2095
|
".reactscope/config.js",
|
|
@@ -1459,14 +2106,14 @@ var STYLE_ENTRY_CANDIDATES = [
|
|
|
1459
2106
|
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
1460
2107
|
var compilerCache = null;
|
|
1461
2108
|
function getCachedBuild(cwd) {
|
|
1462
|
-
if (compilerCache !== null &&
|
|
2109
|
+
if (compilerCache !== null && resolve5(compilerCache.cwd) === resolve5(cwd)) {
|
|
1463
2110
|
return compilerCache.build;
|
|
1464
2111
|
}
|
|
1465
2112
|
return null;
|
|
1466
2113
|
}
|
|
1467
2114
|
function findStylesEntry(cwd) {
|
|
1468
2115
|
for (const name of CONFIG_FILENAMES) {
|
|
1469
|
-
const p =
|
|
2116
|
+
const p = resolve5(cwd, name);
|
|
1470
2117
|
if (!existsSync4(p)) continue;
|
|
1471
2118
|
try {
|
|
1472
2119
|
if (name.endsWith(".json")) {
|
|
@@ -1475,28 +2122,28 @@ function findStylesEntry(cwd) {
|
|
|
1475
2122
|
const scope = data.scope;
|
|
1476
2123
|
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
1477
2124
|
if (typeof entry === "string") {
|
|
1478
|
-
const full =
|
|
2125
|
+
const full = resolve5(cwd, entry);
|
|
1479
2126
|
if (existsSync4(full)) return full;
|
|
1480
2127
|
}
|
|
1481
2128
|
}
|
|
1482
2129
|
} catch {
|
|
1483
2130
|
}
|
|
1484
2131
|
}
|
|
1485
|
-
const pkgPath =
|
|
2132
|
+
const pkgPath = resolve5(cwd, "package.json");
|
|
1486
2133
|
if (existsSync4(pkgPath)) {
|
|
1487
2134
|
try {
|
|
1488
2135
|
const raw = readFileSync4(pkgPath, "utf-8");
|
|
1489
2136
|
const pkg = JSON.parse(raw);
|
|
1490
2137
|
const entry = pkg.scope?.stylesEntry;
|
|
1491
2138
|
if (typeof entry === "string") {
|
|
1492
|
-
const full =
|
|
2139
|
+
const full = resolve5(cwd, entry);
|
|
1493
2140
|
if (existsSync4(full)) return full;
|
|
1494
2141
|
}
|
|
1495
2142
|
} catch {
|
|
1496
2143
|
}
|
|
1497
2144
|
}
|
|
1498
2145
|
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
1499
|
-
const full =
|
|
2146
|
+
const full = resolve5(cwd, candidate);
|
|
1500
2147
|
if (existsSync4(full)) {
|
|
1501
2148
|
try {
|
|
1502
2149
|
const content = readFileSync4(full, "utf-8");
|
|
@@ -1514,7 +2161,7 @@ async function getTailwindCompiler(cwd) {
|
|
|
1514
2161
|
if (entryPath === null) return null;
|
|
1515
2162
|
let compile;
|
|
1516
2163
|
try {
|
|
1517
|
-
const require2 = createRequire(
|
|
2164
|
+
const require2 = createRequire(resolve5(cwd, "package.json"));
|
|
1518
2165
|
const tailwind = require2("tailwindcss");
|
|
1519
2166
|
const fn = tailwind.compile;
|
|
1520
2167
|
if (typeof fn !== "function") return null;
|
|
@@ -1525,8 +2172,8 @@ async function getTailwindCompiler(cwd) {
|
|
|
1525
2172
|
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
1526
2173
|
const loadStylesheet = async (id, base) => {
|
|
1527
2174
|
if (id === "tailwindcss") {
|
|
1528
|
-
const nodeModules =
|
|
1529
|
-
const tailwindCssPath =
|
|
2175
|
+
const nodeModules = resolve5(cwd, "node_modules");
|
|
2176
|
+
const tailwindCssPath = resolve5(nodeModules, "tailwindcss", "index.css");
|
|
1530
2177
|
if (!existsSync4(tailwindCssPath)) {
|
|
1531
2178
|
throw new Error(
|
|
1532
2179
|
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
@@ -1535,10 +2182,10 @@ async function getTailwindCompiler(cwd) {
|
|
|
1535
2182
|
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
1536
2183
|
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
1537
2184
|
}
|
|
1538
|
-
const full =
|
|
2185
|
+
const full = resolve5(base, id);
|
|
1539
2186
|
if (existsSync4(full)) {
|
|
1540
2187
|
const content = readFileSync4(full, "utf-8");
|
|
1541
|
-
return { path: full, base:
|
|
2188
|
+
return { path: full, base: resolve5(full, ".."), content };
|
|
1542
2189
|
}
|
|
1543
2190
|
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
1544
2191
|
};
|
|
@@ -1560,7 +2207,7 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
1560
2207
|
}
|
|
1561
2208
|
|
|
1562
2209
|
// src/render-commands.ts
|
|
1563
|
-
var
|
|
2210
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1564
2211
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
1565
2212
|
var _pool2 = null;
|
|
1566
2213
|
async function getPool2(viewportWidth, viewportHeight) {
|
|
@@ -1685,7 +2332,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1685
2332
|
};
|
|
1686
2333
|
}
|
|
1687
2334
|
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",
|
|
2335
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1689
2336
|
async (componentName, opts) => {
|
|
1690
2337
|
try {
|
|
1691
2338
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1707,7 +2354,7 @@ Available: ${available}`
|
|
|
1707
2354
|
}
|
|
1708
2355
|
const { width, height } = parseViewport(opts.viewport);
|
|
1709
2356
|
const rootDir = process.cwd();
|
|
1710
|
-
const filePath =
|
|
2357
|
+
const filePath = resolve6(rootDir, descriptor.filePath);
|
|
1711
2358
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
1712
2359
|
process.stderr.write(
|
|
1713
2360
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
@@ -1737,7 +2384,7 @@ Available: ${available}`
|
|
|
1737
2384
|
}
|
|
1738
2385
|
const result = outcome.result;
|
|
1739
2386
|
if (opts.output !== void 0) {
|
|
1740
|
-
const outPath =
|
|
2387
|
+
const outPath = resolve6(process.cwd(), opts.output);
|
|
1741
2388
|
writeFileSync4(outPath, result.screenshot);
|
|
1742
2389
|
process.stdout.write(
|
|
1743
2390
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1751,9 +2398,9 @@ Available: ${available}`
|
|
|
1751
2398
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
1752
2399
|
`);
|
|
1753
2400
|
} else if (fmt === "file") {
|
|
1754
|
-
const dir =
|
|
2401
|
+
const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1755
2402
|
mkdirSync3(dir, { recursive: true });
|
|
1756
|
-
const outPath =
|
|
2403
|
+
const outPath = resolve6(dir, `${componentName}.png`);
|
|
1757
2404
|
writeFileSync4(outPath, result.screenshot);
|
|
1758
2405
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1759
2406
|
process.stdout.write(
|
|
@@ -1761,9 +2408,9 @@ Available: ${available}`
|
|
|
1761
2408
|
`
|
|
1762
2409
|
);
|
|
1763
2410
|
} else {
|
|
1764
|
-
const dir =
|
|
2411
|
+
const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1765
2412
|
mkdirSync3(dir, { recursive: true });
|
|
1766
|
-
const outPath =
|
|
2413
|
+
const outPath = resolve6(dir, `${componentName}.png`);
|
|
1767
2414
|
writeFileSync4(outPath, result.screenshot);
|
|
1768
2415
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1769
2416
|
process.stdout.write(
|
|
@@ -1784,7 +2431,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
1784
2431
|
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
2432
|
"--contexts <ids>",
|
|
1786
2433
|
"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",
|
|
2434
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1788
2435
|
async (componentName, opts) => {
|
|
1789
2436
|
try {
|
|
1790
2437
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1799,7 +2446,7 @@ Available: ${available}`
|
|
|
1799
2446
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
1800
2447
|
const { width, height } = { width: 375, height: 812 };
|
|
1801
2448
|
const rootDir = process.cwd();
|
|
1802
|
-
const filePath =
|
|
2449
|
+
const filePath = resolve6(rootDir, descriptor.filePath);
|
|
1803
2450
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
1804
2451
|
const axes = [];
|
|
1805
2452
|
if (opts.axes !== void 0) {
|
|
@@ -1866,7 +2513,7 @@ Available: ${available}`
|
|
|
1866
2513
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1867
2514
|
const gen = new SpriteSheetGenerator();
|
|
1868
2515
|
const sheet = await gen.generate(result);
|
|
1869
|
-
const spritePath =
|
|
2516
|
+
const spritePath = resolve6(process.cwd(), opts.sprite);
|
|
1870
2517
|
writeFileSync4(spritePath, sheet.png);
|
|
1871
2518
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
1872
2519
|
`);
|
|
@@ -1876,9 +2523,9 @@ Available: ${available}`
|
|
|
1876
2523
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
1877
2524
|
const gen = new SpriteSheetGenerator();
|
|
1878
2525
|
const sheet = await gen.generate(result);
|
|
1879
|
-
const dir =
|
|
2526
|
+
const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1880
2527
|
mkdirSync3(dir, { recursive: true });
|
|
1881
|
-
const outPath =
|
|
2528
|
+
const outPath = resolve6(dir, `${componentName}-matrix.png`);
|
|
1882
2529
|
writeFileSync4(outPath, sheet.png);
|
|
1883
2530
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
1884
2531
|
process.stdout.write(
|
|
@@ -1911,7 +2558,7 @@ Available: ${available}`
|
|
|
1911
2558
|
);
|
|
1912
2559
|
}
|
|
1913
2560
|
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",
|
|
2561
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1915
2562
|
async (opts) => {
|
|
1916
2563
|
try {
|
|
1917
2564
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1922,7 +2569,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1922
2569
|
return;
|
|
1923
2570
|
}
|
|
1924
2571
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
1925
|
-
const outputDir =
|
|
2572
|
+
const outputDir = resolve6(process.cwd(), opts.outputDir);
|
|
1926
2573
|
mkdirSync3(outputDir, { recursive: true });
|
|
1927
2574
|
const rootDir = process.cwd();
|
|
1928
2575
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
@@ -1932,7 +2579,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1932
2579
|
const renderOne = async (name) => {
|
|
1933
2580
|
const descriptor = manifest.components[name];
|
|
1934
2581
|
if (descriptor === void 0) return;
|
|
1935
|
-
const filePath =
|
|
2582
|
+
const filePath = resolve6(rootDir, descriptor.filePath);
|
|
1936
2583
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
1937
2584
|
const outcome = await safeRender(
|
|
1938
2585
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
@@ -1955,7 +2602,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1955
2602
|
success: false,
|
|
1956
2603
|
errorMessage: outcome.error.message
|
|
1957
2604
|
});
|
|
1958
|
-
const errPath =
|
|
2605
|
+
const errPath = resolve6(outputDir, `${name}.error.json`);
|
|
1959
2606
|
writeFileSync4(
|
|
1960
2607
|
errPath,
|
|
1961
2608
|
JSON.stringify(
|
|
@@ -1973,9 +2620,9 @@ function registerRenderAll(renderCmd) {
|
|
|
1973
2620
|
}
|
|
1974
2621
|
const result = outcome.result;
|
|
1975
2622
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
1976
|
-
const pngPath =
|
|
2623
|
+
const pngPath = resolve6(outputDir, `${name}.png`);
|
|
1977
2624
|
writeFileSync4(pngPath, result.screenshot);
|
|
1978
|
-
const jsonPath =
|
|
2625
|
+
const jsonPath = resolve6(outputDir, `${name}.json`);
|
|
1979
2626
|
writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
1980
2627
|
if (isTTY()) {
|
|
1981
2628
|
process.stdout.write(
|
|
@@ -2049,7 +2696,7 @@ function createRenderCommand() {
|
|
|
2049
2696
|
|
|
2050
2697
|
// src/report/baseline.ts
|
|
2051
2698
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2052
|
-
import { resolve as
|
|
2699
|
+
import { resolve as resolve7 } from "path";
|
|
2053
2700
|
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
2054
2701
|
import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
|
|
2055
2702
|
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
@@ -2198,20 +2845,20 @@ async function runBaseline(options = {}) {
|
|
|
2198
2845
|
} = options;
|
|
2199
2846
|
const startTime = performance.now();
|
|
2200
2847
|
const rootDir = process.cwd();
|
|
2201
|
-
const baselineDir =
|
|
2202
|
-
const rendersDir =
|
|
2848
|
+
const baselineDir = resolve7(rootDir, outputDir);
|
|
2849
|
+
const rendersDir = resolve7(baselineDir, "renders");
|
|
2203
2850
|
if (existsSync5(baselineDir)) {
|
|
2204
2851
|
rmSync(baselineDir, { recursive: true, force: true });
|
|
2205
2852
|
}
|
|
2206
2853
|
mkdirSync4(rendersDir, { recursive: true });
|
|
2207
2854
|
let manifest;
|
|
2208
2855
|
if (manifestPath !== void 0) {
|
|
2209
|
-
const { readFileSync:
|
|
2210
|
-
const absPath =
|
|
2856
|
+
const { readFileSync: readFileSync8 } = await import("fs");
|
|
2857
|
+
const absPath = resolve7(rootDir, manifestPath);
|
|
2211
2858
|
if (!existsSync5(absPath)) {
|
|
2212
2859
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2213
2860
|
}
|
|
2214
|
-
manifest = JSON.parse(
|
|
2861
|
+
manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2215
2862
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2216
2863
|
`);
|
|
2217
2864
|
} else {
|
|
@@ -2221,7 +2868,7 @@ async function runBaseline(options = {}) {
|
|
|
2221
2868
|
process.stderr.write(`Found ${count} components.
|
|
2222
2869
|
`);
|
|
2223
2870
|
}
|
|
2224
|
-
writeFileSync5(
|
|
2871
|
+
writeFileSync5(resolve7(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2225
2872
|
let componentNames = Object.keys(manifest.components);
|
|
2226
2873
|
if (componentsGlob !== void 0) {
|
|
2227
2874
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -2242,7 +2889,7 @@ async function runBaseline(options = {}) {
|
|
|
2242
2889
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2243
2890
|
};
|
|
2244
2891
|
writeFileSync5(
|
|
2245
|
-
|
|
2892
|
+
resolve7(baselineDir, "compliance.json"),
|
|
2246
2893
|
JSON.stringify(emptyReport, null, 2),
|
|
2247
2894
|
"utf-8"
|
|
2248
2895
|
);
|
|
@@ -2263,7 +2910,7 @@ async function runBaseline(options = {}) {
|
|
|
2263
2910
|
const renderOne = async (name) => {
|
|
2264
2911
|
const descriptor = manifest.components[name];
|
|
2265
2912
|
if (descriptor === void 0) return;
|
|
2266
|
-
const filePath =
|
|
2913
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2267
2914
|
const outcome = await safeRender2(
|
|
2268
2915
|
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2269
2916
|
{
|
|
@@ -2282,7 +2929,7 @@ async function runBaseline(options = {}) {
|
|
|
2282
2929
|
}
|
|
2283
2930
|
if (outcome.crashed) {
|
|
2284
2931
|
failureCount++;
|
|
2285
|
-
const errPath =
|
|
2932
|
+
const errPath = resolve7(rendersDir, `${name}.error.json`);
|
|
2286
2933
|
writeFileSync5(
|
|
2287
2934
|
errPath,
|
|
2288
2935
|
JSON.stringify(
|
|
@@ -2300,10 +2947,10 @@ async function runBaseline(options = {}) {
|
|
|
2300
2947
|
return;
|
|
2301
2948
|
}
|
|
2302
2949
|
const result = outcome.result;
|
|
2303
|
-
writeFileSync5(
|
|
2950
|
+
writeFileSync5(resolve7(rendersDir, `${name}.png`), result.screenshot);
|
|
2304
2951
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2305
2952
|
writeFileSync5(
|
|
2306
|
-
|
|
2953
|
+
resolve7(rendersDir, `${name}.json`),
|
|
2307
2954
|
JSON.stringify(jsonOutput, null, 2),
|
|
2308
2955
|
"utf-8"
|
|
2309
2956
|
);
|
|
@@ -2331,7 +2978,7 @@ async function runBaseline(options = {}) {
|
|
|
2331
2978
|
const engine = new ComplianceEngine(resolver);
|
|
2332
2979
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2333
2980
|
writeFileSync5(
|
|
2334
|
-
|
|
2981
|
+
resolve7(baselineDir, "compliance.json"),
|
|
2335
2982
|
JSON.stringify(batchReport, null, 2),
|
|
2336
2983
|
"utf-8"
|
|
2337
2984
|
);
|
|
@@ -2653,18 +3300,132 @@ function buildStructuredReport(report) {
|
|
|
2653
3300
|
}
|
|
2654
3301
|
|
|
2655
3302
|
// src/tokens/commands.ts
|
|
2656
|
-
import { existsSync as
|
|
2657
|
-
import { resolve as
|
|
3303
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
3304
|
+
import { resolve as resolve9 } from "path";
|
|
2658
3305
|
import {
|
|
2659
|
-
parseTokenFileSync,
|
|
3306
|
+
parseTokenFileSync as parseTokenFileSync2,
|
|
2660
3307
|
TokenParseError,
|
|
2661
|
-
TokenResolver as
|
|
3308
|
+
TokenResolver as TokenResolver3,
|
|
2662
3309
|
TokenValidationError,
|
|
2663
3310
|
validateTokenFile
|
|
2664
3311
|
} from "@agent-scope/tokens";
|
|
3312
|
+
import { Command as Command6 } from "commander";
|
|
3313
|
+
|
|
3314
|
+
// src/tokens/export.ts
|
|
3315
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3316
|
+
import { resolve as resolve8 } from "path";
|
|
3317
|
+
import {
|
|
3318
|
+
exportTokens,
|
|
3319
|
+
parseTokenFileSync,
|
|
3320
|
+
ThemeResolver,
|
|
3321
|
+
TokenResolver as TokenResolver2
|
|
3322
|
+
} from "@agent-scope/tokens";
|
|
2665
3323
|
import { Command as Command5 } from "commander";
|
|
2666
3324
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2667
3325
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3326
|
+
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3327
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3328
|
+
if (fileFlag !== void 0) {
|
|
3329
|
+
return resolve8(process.cwd(), fileFlag);
|
|
3330
|
+
}
|
|
3331
|
+
const configPath = resolve8(process.cwd(), CONFIG_FILE);
|
|
3332
|
+
if (existsSync6(configPath)) {
|
|
3333
|
+
try {
|
|
3334
|
+
const raw = readFileSync5(configPath, "utf-8");
|
|
3335
|
+
const config = JSON.parse(raw);
|
|
3336
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3337
|
+
const file = config.tokens.file;
|
|
3338
|
+
return resolve8(process.cwd(), file);
|
|
3339
|
+
}
|
|
3340
|
+
} catch {
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return resolve8(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3344
|
+
}
|
|
3345
|
+
function createTokensExportCommand() {
|
|
3346
|
+
return new Command5("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(
|
|
3347
|
+
"--theme <name>",
|
|
3348
|
+
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3349
|
+
).action(
|
|
3350
|
+
(opts) => {
|
|
3351
|
+
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
3352
|
+
process.stderr.write(
|
|
3353
|
+
`Error: unsupported format "${opts.format}".
|
|
3354
|
+
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
3355
|
+
`
|
|
3356
|
+
);
|
|
3357
|
+
process.exit(1);
|
|
3358
|
+
}
|
|
3359
|
+
const format = opts.format;
|
|
3360
|
+
try {
|
|
3361
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3362
|
+
if (!existsSync6(filePath)) {
|
|
3363
|
+
throw new Error(
|
|
3364
|
+
`Token file not found at ${filePath}.
|
|
3365
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
3369
|
+
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3370
|
+
let themesMap;
|
|
3371
|
+
if (opts.theme !== void 0) {
|
|
3372
|
+
if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
|
|
3373
|
+
const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
|
|
3374
|
+
throw new Error(
|
|
3375
|
+
`Theme "${opts.theme}" not found in token file.
|
|
3376
|
+
Available themes: ${available}`
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
const baseResolver = new TokenResolver2(tokens);
|
|
3380
|
+
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3381
|
+
baseResolver,
|
|
3382
|
+
rawFile
|
|
3383
|
+
);
|
|
3384
|
+
const themeNames = themeResolver.listThemes();
|
|
3385
|
+
if (!themeNames.includes(opts.theme)) {
|
|
3386
|
+
throw new Error(
|
|
3387
|
+
`Theme "${opts.theme}" could not be resolved.
|
|
3388
|
+
Available themes: ${themeNames.join(", ")}`
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
const themedTokens = themeResolver.buildThemedTokens(opts.theme);
|
|
3392
|
+
const overrideMap = /* @__PURE__ */ new Map();
|
|
3393
|
+
for (const themedToken of themedTokens) {
|
|
3394
|
+
const baseToken = tokens.find((t) => t.path === themedToken.path);
|
|
3395
|
+
if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
|
|
3396
|
+
overrideMap.set(themedToken.path, themedToken.resolvedValue);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
|
|
3400
|
+
}
|
|
3401
|
+
const output = exportTokens(tokens, format, {
|
|
3402
|
+
prefix: opts.prefix,
|
|
3403
|
+
rootSelector: opts.selector,
|
|
3404
|
+
themes: themesMap
|
|
3405
|
+
});
|
|
3406
|
+
if (opts.out !== void 0) {
|
|
3407
|
+
const outPath = resolve8(process.cwd(), opts.out);
|
|
3408
|
+
writeFileSync6(outPath, output, "utf-8");
|
|
3409
|
+
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3410
|
+
`);
|
|
3411
|
+
} else {
|
|
3412
|
+
process.stdout.write(output);
|
|
3413
|
+
if (!output.endsWith("\n")) {
|
|
3414
|
+
process.stdout.write("\n");
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
} catch (err) {
|
|
3418
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3419
|
+
`);
|
|
3420
|
+
process.exit(1);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// src/tokens/commands.ts
|
|
3427
|
+
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3428
|
+
var CONFIG_FILE2 = "reactscope.config.json";
|
|
2668
3429
|
function isTTY2() {
|
|
2669
3430
|
return process.stdout.isTTY === true;
|
|
2670
3431
|
}
|
|
@@ -2682,33 +3443,33 @@ function buildTable2(headers, rows) {
|
|
|
2682
3443
|
);
|
|
2683
3444
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2684
3445
|
}
|
|
2685
|
-
function
|
|
3446
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2686
3447
|
if (fileFlag !== void 0) {
|
|
2687
|
-
return
|
|
3448
|
+
return resolve9(process.cwd(), fileFlag);
|
|
2688
3449
|
}
|
|
2689
|
-
const configPath =
|
|
2690
|
-
if (
|
|
3450
|
+
const configPath = resolve9(process.cwd(), CONFIG_FILE2);
|
|
3451
|
+
if (existsSync7(configPath)) {
|
|
2691
3452
|
try {
|
|
2692
|
-
const raw =
|
|
3453
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
2693
3454
|
const config = JSON.parse(raw);
|
|
2694
3455
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
2695
3456
|
const file = config.tokens.file;
|
|
2696
|
-
return
|
|
3457
|
+
return resolve9(process.cwd(), file);
|
|
2697
3458
|
}
|
|
2698
3459
|
} catch {
|
|
2699
3460
|
}
|
|
2700
3461
|
}
|
|
2701
|
-
return
|
|
3462
|
+
return resolve9(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2702
3463
|
}
|
|
2703
3464
|
function loadTokens(absPath) {
|
|
2704
|
-
if (!
|
|
3465
|
+
if (!existsSync7(absPath)) {
|
|
2705
3466
|
throw new Error(
|
|
2706
3467
|
`Token file not found at ${absPath}.
|
|
2707
3468
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2708
3469
|
);
|
|
2709
3470
|
}
|
|
2710
|
-
const raw =
|
|
2711
|
-
return
|
|
3471
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
3472
|
+
return parseTokenFileSync2(raw);
|
|
2712
3473
|
}
|
|
2713
3474
|
function getRawValue(node, segments) {
|
|
2714
3475
|
const [head, ...rest] = segments;
|
|
@@ -2745,9 +3506,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2745
3506
|
function registerGet2(tokensCmd) {
|
|
2746
3507
|
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
3508
|
try {
|
|
2748
|
-
const filePath =
|
|
3509
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2749
3510
|
const { tokens } = loadTokens(filePath);
|
|
2750
|
-
const resolver = new
|
|
3511
|
+
const resolver = new TokenResolver3(tokens);
|
|
2751
3512
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
2752
3513
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2753
3514
|
if (useJson) {
|
|
@@ -2771,9 +3532,9 @@ function registerList2(tokensCmd) {
|
|
|
2771
3532
|
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
3533
|
(category, opts) => {
|
|
2773
3534
|
try {
|
|
2774
|
-
const filePath =
|
|
3535
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2775
3536
|
const { tokens } = loadTokens(filePath);
|
|
2776
|
-
const resolver = new
|
|
3537
|
+
const resolver = new TokenResolver3(tokens);
|
|
2777
3538
|
const filtered = resolver.list(opts.type, category);
|
|
2778
3539
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2779
3540
|
if (useJson) {
|
|
@@ -2801,9 +3562,9 @@ function registerSearch(tokensCmd) {
|
|
|
2801
3562
|
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
3563
|
(value, opts) => {
|
|
2803
3564
|
try {
|
|
2804
|
-
const filePath =
|
|
3565
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2805
3566
|
const { tokens } = loadTokens(filePath);
|
|
2806
|
-
const resolver = new
|
|
3567
|
+
const resolver = new TokenResolver3(tokens);
|
|
2807
3568
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2808
3569
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
2809
3570
|
"color",
|
|
@@ -2883,10 +3644,10 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2883
3644
|
function registerResolve(tokensCmd) {
|
|
2884
3645
|
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
3646
|
try {
|
|
2886
|
-
const filePath =
|
|
3647
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2887
3648
|
const absFilePath = filePath;
|
|
2888
3649
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
2889
|
-
const resolver = new
|
|
3650
|
+
const resolver = new TokenResolver3(tokens);
|
|
2890
3651
|
resolver.resolve(tokenPath);
|
|
2891
3652
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
2892
3653
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -2920,14 +3681,14 @@ function registerValidate(tokensCmd) {
|
|
|
2920
3681
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2921
3682
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2922
3683
|
try {
|
|
2923
|
-
const filePath =
|
|
2924
|
-
if (!
|
|
3684
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
3685
|
+
if (!existsSync7(filePath)) {
|
|
2925
3686
|
throw new Error(
|
|
2926
3687
|
`Token file not found at ${filePath}.
|
|
2927
3688
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2928
3689
|
);
|
|
2929
3690
|
}
|
|
2930
|
-
const raw =
|
|
3691
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
2931
3692
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2932
3693
|
const errors = [];
|
|
2933
3694
|
let parsed;
|
|
@@ -2954,7 +3715,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
2954
3715
|
throw err;
|
|
2955
3716
|
}
|
|
2956
3717
|
try {
|
|
2957
|
-
|
|
3718
|
+
parseTokenFileSync2(raw);
|
|
2958
3719
|
} catch (err) {
|
|
2959
3720
|
if (err instanceof TokenParseError) {
|
|
2960
3721
|
errors.push({ code: err.code, path: err.path, message: err.message });
|
|
@@ -2995,7 +3756,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
2995
3756
|
}
|
|
2996
3757
|
}
|
|
2997
3758
|
function createTokensCommand() {
|
|
2998
|
-
const tokensCmd = new
|
|
3759
|
+
const tokensCmd = new Command6("tokens").description(
|
|
2999
3760
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
3000
3761
|
);
|
|
3001
3762
|
registerGet2(tokensCmd);
|
|
@@ -3003,12 +3764,13 @@ function createTokensCommand() {
|
|
|
3003
3764
|
registerSearch(tokensCmd);
|
|
3004
3765
|
registerResolve(tokensCmd);
|
|
3005
3766
|
registerValidate(tokensCmd);
|
|
3767
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
3006
3768
|
return tokensCmd;
|
|
3007
3769
|
}
|
|
3008
3770
|
|
|
3009
3771
|
// src/program.ts
|
|
3010
3772
|
function createProgram(options = {}) {
|
|
3011
|
-
const program2 = new
|
|
3773
|
+
const program2 = new Command7("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
3012
3774
|
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
3775
|
async (url, opts) => {
|
|
3014
3776
|
try {
|
|
@@ -3081,7 +3843,7 @@ function createProgram(options = {}) {
|
|
|
3081
3843
|
}
|
|
3082
3844
|
);
|
|
3083
3845
|
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 =
|
|
3846
|
+
const raw = readFileSync7(tracePath, "utf-8");
|
|
3085
3847
|
const trace = loadTrace(raw);
|
|
3086
3848
|
const source = generateTest(trace, {
|
|
3087
3849
|
description: opts.description,
|