@agent-scope/cli 1.9.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 +1164 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1245 -178
- 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 +1245 -181
- 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(
|
|
@@ -2047,6 +2694,332 @@ function createRenderCommand() {
|
|
|
2047
2694
|
return renderCmd;
|
|
2048
2695
|
}
|
|
2049
2696
|
|
|
2697
|
+
// src/report/baseline.ts
|
|
2698
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2699
|
+
import { resolve as resolve7 } from "path";
|
|
2700
|
+
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
2701
|
+
import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
|
|
2702
|
+
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
2703
|
+
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2704
|
+
var _pool3 = null;
|
|
2705
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2706
|
+
if (_pool3 === null) {
|
|
2707
|
+
_pool3 = new BrowserPool3({
|
|
2708
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2709
|
+
viewportWidth,
|
|
2710
|
+
viewportHeight
|
|
2711
|
+
});
|
|
2712
|
+
await _pool3.init();
|
|
2713
|
+
}
|
|
2714
|
+
return _pool3;
|
|
2715
|
+
}
|
|
2716
|
+
async function shutdownPool3() {
|
|
2717
|
+
if (_pool3 !== null) {
|
|
2718
|
+
await _pool3.close();
|
|
2719
|
+
_pool3 = null;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2723
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2724
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2725
|
+
const slot = await pool.acquire();
|
|
2726
|
+
const { page } = slot;
|
|
2727
|
+
try {
|
|
2728
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
2729
|
+
await page.waitForFunction(
|
|
2730
|
+
() => {
|
|
2731
|
+
const w = window;
|
|
2732
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
2733
|
+
},
|
|
2734
|
+
{ timeout: 15e3 }
|
|
2735
|
+
);
|
|
2736
|
+
const renderError = await page.evaluate(() => {
|
|
2737
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
2738
|
+
});
|
|
2739
|
+
if (renderError !== null) {
|
|
2740
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
2741
|
+
}
|
|
2742
|
+
const rootDir = process.cwd();
|
|
2743
|
+
const classes = await page.evaluate(() => {
|
|
2744
|
+
const set = /* @__PURE__ */ new Set();
|
|
2745
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
2746
|
+
for (const c of el.className.split(/\s+/)) {
|
|
2747
|
+
if (c) set.add(c);
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
return [...set];
|
|
2751
|
+
});
|
|
2752
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
2753
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
2754
|
+
await page.addStyleTag({ content: projectCss });
|
|
2755
|
+
}
|
|
2756
|
+
const startMs = performance.now();
|
|
2757
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
2758
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
2759
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
2760
|
+
throw new Error(
|
|
2761
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
const PAD = 24;
|
|
2765
|
+
const MIN_W = 320;
|
|
2766
|
+
const MIN_H = 200;
|
|
2767
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
2768
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
2769
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
2770
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
2771
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
2772
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
2773
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
2774
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
2775
|
+
const screenshot = await page.screenshot({
|
|
2776
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
2777
|
+
type: "png"
|
|
2778
|
+
});
|
|
2779
|
+
const computedStylesRaw = {};
|
|
2780
|
+
const styles = await page.evaluate((sel) => {
|
|
2781
|
+
const el = document.querySelector(sel);
|
|
2782
|
+
if (el === null) return {};
|
|
2783
|
+
const computed = window.getComputedStyle(el);
|
|
2784
|
+
const out = {};
|
|
2785
|
+
for (const prop of [
|
|
2786
|
+
"display",
|
|
2787
|
+
"width",
|
|
2788
|
+
"height",
|
|
2789
|
+
"color",
|
|
2790
|
+
"backgroundColor",
|
|
2791
|
+
"fontSize",
|
|
2792
|
+
"fontFamily",
|
|
2793
|
+
"padding",
|
|
2794
|
+
"margin"
|
|
2795
|
+
]) {
|
|
2796
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
2797
|
+
}
|
|
2798
|
+
return out;
|
|
2799
|
+
}, "[data-reactscope-root] > *");
|
|
2800
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
2801
|
+
const renderTimeMs = performance.now() - startMs;
|
|
2802
|
+
return {
|
|
2803
|
+
screenshot,
|
|
2804
|
+
width: Math.round(safeW),
|
|
2805
|
+
height: Math.round(safeH),
|
|
2806
|
+
renderTimeMs,
|
|
2807
|
+
computedStyles: computedStylesRaw
|
|
2808
|
+
};
|
|
2809
|
+
} finally {
|
|
2810
|
+
pool.release(slot);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
2814
|
+
const flat = {};
|
|
2815
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
2816
|
+
Object.assign(flat, styles);
|
|
2817
|
+
}
|
|
2818
|
+
const colors = {};
|
|
2819
|
+
const spacing = {};
|
|
2820
|
+
const typography = {};
|
|
2821
|
+
const borders = {};
|
|
2822
|
+
const shadows = {};
|
|
2823
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
2824
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
2825
|
+
colors[prop] = value;
|
|
2826
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
2827
|
+
spacing[prop] = value;
|
|
2828
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
2829
|
+
typography[prop] = value;
|
|
2830
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
2831
|
+
borders[prop] = value;
|
|
2832
|
+
} else if (prop === "boxShadow") {
|
|
2833
|
+
shadows[prop] = value;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return { colors, spacing, typography, borders, shadows };
|
|
2837
|
+
}
|
|
2838
|
+
async function runBaseline(options = {}) {
|
|
2839
|
+
const {
|
|
2840
|
+
outputDir = DEFAULT_BASELINE_DIR,
|
|
2841
|
+
componentsGlob,
|
|
2842
|
+
manifestPath,
|
|
2843
|
+
viewportWidth = 375,
|
|
2844
|
+
viewportHeight = 812
|
|
2845
|
+
} = options;
|
|
2846
|
+
const startTime = performance.now();
|
|
2847
|
+
const rootDir = process.cwd();
|
|
2848
|
+
const baselineDir = resolve7(rootDir, outputDir);
|
|
2849
|
+
const rendersDir = resolve7(baselineDir, "renders");
|
|
2850
|
+
if (existsSync5(baselineDir)) {
|
|
2851
|
+
rmSync(baselineDir, { recursive: true, force: true });
|
|
2852
|
+
}
|
|
2853
|
+
mkdirSync4(rendersDir, { recursive: true });
|
|
2854
|
+
let manifest;
|
|
2855
|
+
if (manifestPath !== void 0) {
|
|
2856
|
+
const { readFileSync: readFileSync8 } = await import("fs");
|
|
2857
|
+
const absPath = resolve7(rootDir, manifestPath);
|
|
2858
|
+
if (!existsSync5(absPath)) {
|
|
2859
|
+
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2860
|
+
}
|
|
2861
|
+
manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2862
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2863
|
+
`);
|
|
2864
|
+
} else {
|
|
2865
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
2866
|
+
manifest = await generateManifest2({ rootDir });
|
|
2867
|
+
const count = Object.keys(manifest.components).length;
|
|
2868
|
+
process.stderr.write(`Found ${count} components.
|
|
2869
|
+
`);
|
|
2870
|
+
}
|
|
2871
|
+
writeFileSync5(resolve7(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2872
|
+
let componentNames = Object.keys(manifest.components);
|
|
2873
|
+
if (componentsGlob !== void 0) {
|
|
2874
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
2875
|
+
process.stderr.write(
|
|
2876
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
2877
|
+
`
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
const total = componentNames.length;
|
|
2881
|
+
if (total === 0) {
|
|
2882
|
+
process.stderr.write("No components to baseline.\n");
|
|
2883
|
+
const emptyReport = {
|
|
2884
|
+
components: {},
|
|
2885
|
+
totalProperties: 0,
|
|
2886
|
+
totalOnSystem: 0,
|
|
2887
|
+
totalOffSystem: 0,
|
|
2888
|
+
aggregateCompliance: 1,
|
|
2889
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2890
|
+
};
|
|
2891
|
+
writeFileSync5(
|
|
2892
|
+
resolve7(baselineDir, "compliance.json"),
|
|
2893
|
+
JSON.stringify(emptyReport, null, 2),
|
|
2894
|
+
"utf-8"
|
|
2895
|
+
);
|
|
2896
|
+
return {
|
|
2897
|
+
baselineDir,
|
|
2898
|
+
componentCount: 0,
|
|
2899
|
+
failureCount: 0,
|
|
2900
|
+
wallClockMs: performance.now() - startTime
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
2904
|
+
`);
|
|
2905
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
2906
|
+
let completed = 0;
|
|
2907
|
+
let failureCount = 0;
|
|
2908
|
+
const CONCURRENCY = 4;
|
|
2909
|
+
let nextIdx = 0;
|
|
2910
|
+
const renderOne = async (name) => {
|
|
2911
|
+
const descriptor = manifest.components[name];
|
|
2912
|
+
if (descriptor === void 0) return;
|
|
2913
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2914
|
+
const outcome = await safeRender2(
|
|
2915
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2916
|
+
{
|
|
2917
|
+
props: {},
|
|
2918
|
+
sourceLocation: {
|
|
2919
|
+
file: descriptor.filePath,
|
|
2920
|
+
line: descriptor.loc.start,
|
|
2921
|
+
column: 0
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
);
|
|
2925
|
+
completed++;
|
|
2926
|
+
const pct = Math.round(completed / total * 100);
|
|
2927
|
+
if (isTTY()) {
|
|
2928
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
2929
|
+
}
|
|
2930
|
+
if (outcome.crashed) {
|
|
2931
|
+
failureCount++;
|
|
2932
|
+
const errPath = resolve7(rendersDir, `${name}.error.json`);
|
|
2933
|
+
writeFileSync5(
|
|
2934
|
+
errPath,
|
|
2935
|
+
JSON.stringify(
|
|
2936
|
+
{
|
|
2937
|
+
component: name,
|
|
2938
|
+
errorMessage: outcome.error.message,
|
|
2939
|
+
heuristicFlags: outcome.error.heuristicFlags,
|
|
2940
|
+
propsAtCrash: outcome.error.propsAtCrash
|
|
2941
|
+
},
|
|
2942
|
+
null,
|
|
2943
|
+
2
|
|
2944
|
+
),
|
|
2945
|
+
"utf-8"
|
|
2946
|
+
);
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
const result = outcome.result;
|
|
2950
|
+
writeFileSync5(resolve7(rendersDir, `${name}.png`), result.screenshot);
|
|
2951
|
+
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2952
|
+
writeFileSync5(
|
|
2953
|
+
resolve7(rendersDir, `${name}.json`),
|
|
2954
|
+
JSON.stringify(jsonOutput, null, 2),
|
|
2955
|
+
"utf-8"
|
|
2956
|
+
);
|
|
2957
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
2958
|
+
};
|
|
2959
|
+
const worker = async () => {
|
|
2960
|
+
while (nextIdx < componentNames.length) {
|
|
2961
|
+
const i = nextIdx++;
|
|
2962
|
+
const name = componentNames[i];
|
|
2963
|
+
if (name !== void 0) {
|
|
2964
|
+
await renderOne(name);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
const workers = [];
|
|
2969
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
2970
|
+
workers.push(worker());
|
|
2971
|
+
}
|
|
2972
|
+
await Promise.all(workers);
|
|
2973
|
+
await shutdownPool3();
|
|
2974
|
+
if (isTTY()) {
|
|
2975
|
+
process.stderr.write("\n");
|
|
2976
|
+
}
|
|
2977
|
+
const resolver = new TokenResolver([]);
|
|
2978
|
+
const engine = new ComplianceEngine(resolver);
|
|
2979
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2980
|
+
writeFileSync5(
|
|
2981
|
+
resolve7(baselineDir, "compliance.json"),
|
|
2982
|
+
JSON.stringify(batchReport, null, 2),
|
|
2983
|
+
"utf-8"
|
|
2984
|
+
);
|
|
2985
|
+
const wallClockMs = performance.now() - startTime;
|
|
2986
|
+
const successCount = total - failureCount;
|
|
2987
|
+
process.stderr.write(
|
|
2988
|
+
`
|
|
2989
|
+
Baseline complete: ${successCount}/${total} components rendered` + (failureCount > 0 ? ` (${failureCount} failed)` : "") + ` in ${(wallClockMs / 1e3).toFixed(1)}s
|
|
2990
|
+
`
|
|
2991
|
+
);
|
|
2992
|
+
process.stderr.write(`Snapshot saved to ${baselineDir}
|
|
2993
|
+
`);
|
|
2994
|
+
return { baselineDir, componentCount: total, failureCount, wallClockMs };
|
|
2995
|
+
}
|
|
2996
|
+
function registerBaselineSubCommand(reportCmd) {
|
|
2997
|
+
reportCmd.command("baseline").description("Capture a baseline snapshot (manifest + renders + compliance) for later diffing").option(
|
|
2998
|
+
"-o, --output <dir>",
|
|
2999
|
+
"Output directory for the baseline snapshot",
|
|
3000
|
+
DEFAULT_BASELINE_DIR
|
|
3001
|
+
).option("--components <glob>", "Glob pattern to baseline a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").action(
|
|
3002
|
+
async (opts) => {
|
|
3003
|
+
try {
|
|
3004
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3005
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3006
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3007
|
+
await runBaseline({
|
|
3008
|
+
outputDir: opts.output,
|
|
3009
|
+
componentsGlob: opts.components,
|
|
3010
|
+
manifestPath: opts.manifest,
|
|
3011
|
+
viewportWidth,
|
|
3012
|
+
viewportHeight
|
|
3013
|
+
});
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3016
|
+
`);
|
|
3017
|
+
process.exit(1);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2050
3023
|
// src/tree-formatter.ts
|
|
2051
3024
|
var BRANCH = "\u251C\u2500\u2500 ";
|
|
2052
3025
|
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
@@ -2327,18 +3300,132 @@ function buildStructuredReport(report) {
|
|
|
2327
3300
|
}
|
|
2328
3301
|
|
|
2329
3302
|
// src/tokens/commands.ts
|
|
2330
|
-
import { existsSync as
|
|
2331
|
-
import { resolve as
|
|
3303
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
3304
|
+
import { resolve as resolve9 } from "path";
|
|
2332
3305
|
import {
|
|
2333
|
-
parseTokenFileSync,
|
|
3306
|
+
parseTokenFileSync as parseTokenFileSync2,
|
|
2334
3307
|
TokenParseError,
|
|
2335
|
-
TokenResolver,
|
|
3308
|
+
TokenResolver as TokenResolver3,
|
|
2336
3309
|
TokenValidationError,
|
|
2337
3310
|
validateTokenFile
|
|
2338
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";
|
|
2339
3323
|
import { Command as Command5 } from "commander";
|
|
2340
3324
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2341
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";
|
|
2342
3429
|
function isTTY2() {
|
|
2343
3430
|
return process.stdout.isTTY === true;
|
|
2344
3431
|
}
|
|
@@ -2356,33 +3443,33 @@ function buildTable2(headers, rows) {
|
|
|
2356
3443
|
);
|
|
2357
3444
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2358
3445
|
}
|
|
2359
|
-
function
|
|
3446
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2360
3447
|
if (fileFlag !== void 0) {
|
|
2361
|
-
return
|
|
3448
|
+
return resolve9(process.cwd(), fileFlag);
|
|
2362
3449
|
}
|
|
2363
|
-
const configPath =
|
|
2364
|
-
if (
|
|
3450
|
+
const configPath = resolve9(process.cwd(), CONFIG_FILE2);
|
|
3451
|
+
if (existsSync7(configPath)) {
|
|
2365
3452
|
try {
|
|
2366
|
-
const raw =
|
|
3453
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
2367
3454
|
const config = JSON.parse(raw);
|
|
2368
3455
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
2369
3456
|
const file = config.tokens.file;
|
|
2370
|
-
return
|
|
3457
|
+
return resolve9(process.cwd(), file);
|
|
2371
3458
|
}
|
|
2372
3459
|
} catch {
|
|
2373
3460
|
}
|
|
2374
3461
|
}
|
|
2375
|
-
return
|
|
3462
|
+
return resolve9(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2376
3463
|
}
|
|
2377
3464
|
function loadTokens(absPath) {
|
|
2378
|
-
if (!
|
|
3465
|
+
if (!existsSync7(absPath)) {
|
|
2379
3466
|
throw new Error(
|
|
2380
3467
|
`Token file not found at ${absPath}.
|
|
2381
3468
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2382
3469
|
);
|
|
2383
3470
|
}
|
|
2384
|
-
const raw =
|
|
2385
|
-
return
|
|
3471
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
3472
|
+
return parseTokenFileSync2(raw);
|
|
2386
3473
|
}
|
|
2387
3474
|
function getRawValue(node, segments) {
|
|
2388
3475
|
const [head, ...rest] = segments;
|
|
@@ -2419,9 +3506,9 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2419
3506
|
function registerGet2(tokensCmd) {
|
|
2420
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) => {
|
|
2421
3508
|
try {
|
|
2422
|
-
const filePath =
|
|
3509
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2423
3510
|
const { tokens } = loadTokens(filePath);
|
|
2424
|
-
const resolver = new
|
|
3511
|
+
const resolver = new TokenResolver3(tokens);
|
|
2425
3512
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
2426
3513
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2427
3514
|
if (useJson) {
|
|
@@ -2445,9 +3532,9 @@ function registerList2(tokensCmd) {
|
|
|
2445
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(
|
|
2446
3533
|
(category, opts) => {
|
|
2447
3534
|
try {
|
|
2448
|
-
const filePath =
|
|
3535
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2449
3536
|
const { tokens } = loadTokens(filePath);
|
|
2450
|
-
const resolver = new
|
|
3537
|
+
const resolver = new TokenResolver3(tokens);
|
|
2451
3538
|
const filtered = resolver.list(opts.type, category);
|
|
2452
3539
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2453
3540
|
if (useJson) {
|
|
@@ -2475,9 +3562,9 @@ function registerSearch(tokensCmd) {
|
|
|
2475
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(
|
|
2476
3563
|
(value, opts) => {
|
|
2477
3564
|
try {
|
|
2478
|
-
const filePath =
|
|
3565
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2479
3566
|
const { tokens } = loadTokens(filePath);
|
|
2480
|
-
const resolver = new
|
|
3567
|
+
const resolver = new TokenResolver3(tokens);
|
|
2481
3568
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2482
3569
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
2483
3570
|
"color",
|
|
@@ -2557,10 +3644,10 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2557
3644
|
function registerResolve(tokensCmd) {
|
|
2558
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) => {
|
|
2559
3646
|
try {
|
|
2560
|
-
const filePath =
|
|
3647
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2561
3648
|
const absFilePath = filePath;
|
|
2562
3649
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
2563
|
-
const resolver = new
|
|
3650
|
+
const resolver = new TokenResolver3(tokens);
|
|
2564
3651
|
resolver.resolve(tokenPath);
|
|
2565
3652
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
2566
3653
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -2594,14 +3681,14 @@ function registerValidate(tokensCmd) {
|
|
|
2594
3681
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2595
3682
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2596
3683
|
try {
|
|
2597
|
-
const filePath =
|
|
2598
|
-
if (!
|
|
3684
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
3685
|
+
if (!existsSync7(filePath)) {
|
|
2599
3686
|
throw new Error(
|
|
2600
3687
|
`Token file not found at ${filePath}.
|
|
2601
3688
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2602
3689
|
);
|
|
2603
3690
|
}
|
|
2604
|
-
const raw =
|
|
3691
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
2605
3692
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2606
3693
|
const errors = [];
|
|
2607
3694
|
let parsed;
|
|
@@ -2628,7 +3715,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
2628
3715
|
throw err;
|
|
2629
3716
|
}
|
|
2630
3717
|
try {
|
|
2631
|
-
|
|
3718
|
+
parseTokenFileSync2(raw);
|
|
2632
3719
|
} catch (err) {
|
|
2633
3720
|
if (err instanceof TokenParseError) {
|
|
2634
3721
|
errors.push({ code: err.code, path: err.path, message: err.message });
|
|
@@ -2669,7 +3756,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
2669
3756
|
}
|
|
2670
3757
|
}
|
|
2671
3758
|
function createTokensCommand() {
|
|
2672
|
-
const tokensCmd = new
|
|
3759
|
+
const tokensCmd = new Command6("tokens").description(
|
|
2673
3760
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
2674
3761
|
);
|
|
2675
3762
|
registerGet2(tokensCmd);
|
|
@@ -2677,12 +3764,13 @@ function createTokensCommand() {
|
|
|
2677
3764
|
registerSearch(tokensCmd);
|
|
2678
3765
|
registerResolve(tokensCmd);
|
|
2679
3766
|
registerValidate(tokensCmd);
|
|
3767
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
2680
3768
|
return tokensCmd;
|
|
2681
3769
|
}
|
|
2682
3770
|
|
|
2683
3771
|
// src/program.ts
|
|
2684
3772
|
function createProgram(options = {}) {
|
|
2685
|
-
const program2 = new
|
|
3773
|
+
const program2 = new Command7("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
2686
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(
|
|
2687
3775
|
async (url, opts) => {
|
|
2688
3776
|
try {
|
|
@@ -2755,7 +3843,7 @@ function createProgram(options = {}) {
|
|
|
2755
3843
|
}
|
|
2756
3844
|
);
|
|
2757
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) => {
|
|
2758
|
-
const raw =
|
|
3846
|
+
const raw = readFileSync7(tracePath, "utf-8");
|
|
2759
3847
|
const trace = loadTrace(raw);
|
|
2760
3848
|
const source = generateTest(trace, {
|
|
2761
3849
|
description: opts.description,
|
|
@@ -2769,6 +3857,10 @@ function createProgram(options = {}) {
|
|
|
2769
3857
|
program2.addCommand(createTokensCommand());
|
|
2770
3858
|
program2.addCommand(createInstrumentCommand());
|
|
2771
3859
|
program2.addCommand(createInitCommand());
|
|
3860
|
+
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
3861
|
+
if (existingReportCmd !== void 0) {
|
|
3862
|
+
registerBaselineSubCommand(existingReportCmd);
|
|
3863
|
+
}
|
|
2772
3864
|
return program2;
|
|
2773
3865
|
}
|
|
2774
3866
|
|