@agent-scope/cli 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1305 -179
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1341 -241
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -1
- package/dist/index.d.ts +264 -1
- package/dist/index.js +1340 -243
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -4,11 +4,11 @@ var fs = require('fs');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var readline = require('readline');
|
|
6
6
|
var commander = require('commander');
|
|
7
|
-
var manifest = require('@agent-scope/manifest');
|
|
8
|
-
var playwright = require('@agent-scope/playwright');
|
|
9
|
-
var playwright$1 = require('playwright');
|
|
10
7
|
var render = require('@agent-scope/render');
|
|
11
8
|
var esbuild = require('esbuild');
|
|
9
|
+
var manifest = require('@agent-scope/manifest');
|
|
10
|
+
var playwright$1 = require('playwright');
|
|
11
|
+
var playwright = require('@agent-scope/playwright');
|
|
12
12
|
var module$1 = require('module');
|
|
13
13
|
var tokens = require('@agent-scope/tokens');
|
|
14
14
|
|
|
@@ -228,9 +228,9 @@ function createRL() {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
async function ask(rl, question) {
|
|
231
|
-
return new Promise((
|
|
231
|
+
return new Promise((resolve11) => {
|
|
232
232
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
233
|
+
resolve11(answer.trim());
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -377,6 +377,114 @@ function createInitCommand() {
|
|
|
377
377
|
}
|
|
378
378
|
});
|
|
379
379
|
}
|
|
380
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
381
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
382
|
+
return wrapInHtml(bundledScript, viewportWidth);
|
|
383
|
+
}
|
|
384
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
385
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
386
|
+
const wrapperCode = (
|
|
387
|
+
/* ts */
|
|
388
|
+
`
|
|
389
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
390
|
+
import { createRoot } from "react-dom/client";
|
|
391
|
+
import { createElement } from "react";
|
|
392
|
+
|
|
393
|
+
(function scopeRenderHarness() {
|
|
394
|
+
var Component =
|
|
395
|
+
__scopeMod["default"] ||
|
|
396
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
397
|
+
(Object.values(__scopeMod).find(
|
|
398
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
399
|
+
));
|
|
400
|
+
|
|
401
|
+
if (!Component) {
|
|
402
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
403
|
+
"No renderable component found. Checked: default, " +
|
|
404
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
405
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
406
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
var props = ${propsJson};
|
|
412
|
+
var rootEl = document.getElementById("scope-root");
|
|
413
|
+
if (!rootEl) {
|
|
414
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
415
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
419
|
+
// Use requestAnimationFrame to let React flush the render
|
|
420
|
+
requestAnimationFrame(function() {
|
|
421
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
422
|
+
});
|
|
423
|
+
} catch (err) {
|
|
424
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
425
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
428
|
+
`
|
|
429
|
+
);
|
|
430
|
+
const result = await esbuild__namespace.build({
|
|
431
|
+
stdin: {
|
|
432
|
+
contents: wrapperCode,
|
|
433
|
+
// Resolve relative imports (within the component's dir)
|
|
434
|
+
resolveDir: path.dirname(filePath),
|
|
435
|
+
loader: "tsx",
|
|
436
|
+
sourcefile: "__scope_harness__.tsx"
|
|
437
|
+
},
|
|
438
|
+
bundle: true,
|
|
439
|
+
format: "iife",
|
|
440
|
+
write: false,
|
|
441
|
+
platform: "browser",
|
|
442
|
+
jsx: "automatic",
|
|
443
|
+
jsxImportSource: "react",
|
|
444
|
+
target: "es2020",
|
|
445
|
+
// Bundle everything — no externals
|
|
446
|
+
external: [],
|
|
447
|
+
define: {
|
|
448
|
+
"process.env.NODE_ENV": '"development"',
|
|
449
|
+
global: "globalThis"
|
|
450
|
+
},
|
|
451
|
+
logLevel: "silent",
|
|
452
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
453
|
+
banner: {
|
|
454
|
+
js: "/* @agent-scope/cli component harness */"
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
if (result.errors.length > 0) {
|
|
458
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
459
|
+
throw new Error(`esbuild failed to bundle component:
|
|
460
|
+
${msg}`);
|
|
461
|
+
}
|
|
462
|
+
const outputFile = result.outputFiles?.[0];
|
|
463
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
464
|
+
throw new Error("esbuild produced no output");
|
|
465
|
+
}
|
|
466
|
+
return outputFile.text;
|
|
467
|
+
}
|
|
468
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
469
|
+
const projectStyleBlock = "";
|
|
470
|
+
return `<!DOCTYPE html>
|
|
471
|
+
<html lang="en">
|
|
472
|
+
<head>
|
|
473
|
+
<meta charset="UTF-8" />
|
|
474
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
475
|
+
<style>
|
|
476
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
477
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
478
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
479
|
+
</style>
|
|
480
|
+
${projectStyleBlock}
|
|
481
|
+
</head>
|
|
482
|
+
<body>
|
|
483
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
484
|
+
<script>${bundledScript}</script>
|
|
485
|
+
</body>
|
|
486
|
+
</html>`;
|
|
487
|
+
}
|
|
380
488
|
|
|
381
489
|
// src/manifest-formatter.ts
|
|
382
490
|
function isTTY() {
|
|
@@ -644,168 +752,20 @@ function createManifestCommand() {
|
|
|
644
752
|
registerGenerate(manifestCmd);
|
|
645
753
|
return manifestCmd;
|
|
646
754
|
}
|
|
647
|
-
async function browserCapture(options) {
|
|
648
|
-
const { url, timeout = 1e4, wait = 0 } = options;
|
|
649
|
-
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
650
|
-
try {
|
|
651
|
-
const context = await browser.newContext();
|
|
652
|
-
const page = await context.newPage();
|
|
653
|
-
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
654
|
-
await page.goto(url, {
|
|
655
|
-
waitUntil: "networkidle",
|
|
656
|
-
timeout: timeout + 5e3
|
|
657
|
-
});
|
|
658
|
-
await page.waitForFunction(
|
|
659
|
-
() => {
|
|
660
|
-
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
661
|
-
},
|
|
662
|
-
{ timeout }
|
|
663
|
-
);
|
|
664
|
-
if (wait > 0) {
|
|
665
|
-
await page.waitForTimeout(wait);
|
|
666
|
-
}
|
|
667
|
-
const raw = await page.evaluate(async () => {
|
|
668
|
-
const win = window;
|
|
669
|
-
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
670
|
-
throw new Error("Scope runtime not injected");
|
|
671
|
-
}
|
|
672
|
-
return win.__SCOPE_CAPTURE__();
|
|
673
|
-
});
|
|
674
|
-
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
675
|
-
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
676
|
-
}
|
|
677
|
-
const report = { ...raw, route: null };
|
|
678
|
-
return { report };
|
|
679
|
-
} finally {
|
|
680
|
-
await browser.close();
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function writeReportToFile(report, outputPath, pretty) {
|
|
684
|
-
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
685
|
-
fs.writeFileSync(outputPath, json, "utf-8");
|
|
686
|
-
}
|
|
687
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
688
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
689
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
690
|
-
}
|
|
691
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
692
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
693
|
-
const wrapperCode = (
|
|
694
|
-
/* ts */
|
|
695
|
-
`
|
|
696
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
697
|
-
import { createRoot } from "react-dom/client";
|
|
698
|
-
import { createElement } from "react";
|
|
699
|
-
|
|
700
|
-
(function scopeRenderHarness() {
|
|
701
|
-
var Component =
|
|
702
|
-
__scopeMod["default"] ||
|
|
703
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
704
|
-
(Object.values(__scopeMod).find(
|
|
705
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
706
|
-
));
|
|
707
|
-
|
|
708
|
-
if (!Component) {
|
|
709
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
710
|
-
"No renderable component found. Checked: default, " +
|
|
711
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
712
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
713
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
755
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
726
|
-
// Use requestAnimationFrame to let React flush the render
|
|
727
|
-
requestAnimationFrame(function() {
|
|
728
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
729
|
-
});
|
|
730
|
-
} catch (err) {
|
|
731
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
732
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
733
|
-
}
|
|
734
|
-
})();
|
|
735
|
-
`
|
|
736
|
-
);
|
|
737
|
-
const result = await esbuild__namespace.build({
|
|
738
|
-
stdin: {
|
|
739
|
-
contents: wrapperCode,
|
|
740
|
-
// Resolve relative imports (within the component's dir)
|
|
741
|
-
resolveDir: path.dirname(filePath),
|
|
742
|
-
loader: "tsx",
|
|
743
|
-
sourcefile: "__scope_harness__.tsx"
|
|
744
|
-
},
|
|
745
|
-
bundle: true,
|
|
746
|
-
format: "iife",
|
|
747
|
-
write: false,
|
|
748
|
-
platform: "browser",
|
|
749
|
-
jsx: "automatic",
|
|
750
|
-
jsxImportSource: "react",
|
|
751
|
-
target: "es2020",
|
|
752
|
-
// Bundle everything — no externals
|
|
753
|
-
external: [],
|
|
754
|
-
define: {
|
|
755
|
-
"process.env.NODE_ENV": '"development"',
|
|
756
|
-
global: "globalThis"
|
|
757
|
-
},
|
|
758
|
-
logLevel: "silent",
|
|
759
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
760
|
-
banner: {
|
|
761
|
-
js: "/* @agent-scope/cli component harness */"
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
if (result.errors.length > 0) {
|
|
765
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
766
|
-
throw new Error(`esbuild failed to bundle component:
|
|
767
|
-
${msg}`);
|
|
756
|
+
// src/render-formatter.ts
|
|
757
|
+
function parseViewport(spec) {
|
|
758
|
+
const lower = spec.toLowerCase();
|
|
759
|
+
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
760
|
+
if (!match) {
|
|
761
|
+
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
768
762
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
763
|
+
const width = parseInt(match[1] ?? "0", 10);
|
|
764
|
+
const height = parseInt(match[2] ?? "0", 10);
|
|
765
|
+
if (width <= 0 || height <= 0) {
|
|
766
|
+
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
772
767
|
}
|
|
773
|
-
return
|
|
774
|
-
}
|
|
775
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
776
|
-
const projectStyleBlock = "";
|
|
777
|
-
return `<!DOCTYPE html>
|
|
778
|
-
<html lang="en">
|
|
779
|
-
<head>
|
|
780
|
-
<meta charset="UTF-8" />
|
|
781
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
782
|
-
<style>
|
|
783
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
784
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
785
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
786
|
-
</style>
|
|
787
|
-
${projectStyleBlock}
|
|
788
|
-
</head>
|
|
789
|
-
<body>
|
|
790
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
791
|
-
<script>${bundledScript}</script>
|
|
792
|
-
</body>
|
|
793
|
-
</html>`;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// src/render-formatter.ts
|
|
797
|
-
function parseViewport(spec) {
|
|
798
|
-
const lower = spec.toLowerCase();
|
|
799
|
-
const match = /^(\d+)[x×](\d+)$/.exec(lower);
|
|
800
|
-
if (!match) {
|
|
801
|
-
throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
|
|
802
|
-
}
|
|
803
|
-
const width = parseInt(match[1] ?? "0", 10);
|
|
804
|
-
const height = parseInt(match[2] ?? "0", 10);
|
|
805
|
-
if (width <= 0 || height <= 0) {
|
|
806
|
-
throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
|
|
807
|
-
}
|
|
808
|
-
return { width, height };
|
|
768
|
+
return { width, height };
|
|
809
769
|
}
|
|
810
770
|
function formatRenderJson(componentName, props, result) {
|
|
811
771
|
return {
|
|
@@ -940,21 +900,1011 @@ function formatSummaryText(results, outputDir) {
|
|
|
940
900
|
}
|
|
941
901
|
}
|
|
942
902
|
}
|
|
943
|
-
lines.push("\u2500".repeat(60));
|
|
903
|
+
lines.push("\u2500".repeat(60));
|
|
904
|
+
return lines.join("\n");
|
|
905
|
+
}
|
|
906
|
+
function escapeHtml(str) {
|
|
907
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
908
|
+
}
|
|
909
|
+
function csvEscape(value) {
|
|
910
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
911
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
912
|
+
}
|
|
913
|
+
return value;
|
|
914
|
+
}
|
|
915
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
916
|
+
function buildHookInstrumentationScript() {
|
|
917
|
+
return `
|
|
918
|
+
(function __scopeHooksInstrument() {
|
|
919
|
+
// Locate the React DevTools hook installed by the browser-entry bundle.
|
|
920
|
+
// We use a lighter approach: walk __REACT_DEVTOOLS_GLOBAL_HOOK__ renderers.
|
|
921
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
922
|
+
if (!hook) return { error: "No React DevTools hook found" };
|
|
923
|
+
|
|
924
|
+
var renderers = hook._renderers || hook.renderers;
|
|
925
|
+
if (!renderers || renderers.size === 0) return { error: "No React renderers registered" };
|
|
926
|
+
|
|
927
|
+
// Get the first renderer's fiber roots
|
|
928
|
+
var renderer = null;
|
|
929
|
+
if (renderers.forEach) {
|
|
930
|
+
renderers.forEach(function(r) { if (!renderer) renderer = r; });
|
|
931
|
+
} else {
|
|
932
|
+
renderer = Object.values(renderers)[0];
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Handle both our wrapped format {id, renderer, fiberRoots} and raw renderer
|
|
936
|
+
var fiberRoots;
|
|
937
|
+
if (renderer && renderer.fiberRoots) {
|
|
938
|
+
fiberRoots = renderer.fiberRoots;
|
|
939
|
+
} else {
|
|
940
|
+
return { error: "No fiber roots found" };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
var rootFiber = null;
|
|
944
|
+
fiberRoots.forEach(function(r) { if (!rootFiber) rootFiber = r; });
|
|
945
|
+
if (!rootFiber) return { error: "No fiber root" };
|
|
946
|
+
|
|
947
|
+
var current = rootFiber.current;
|
|
948
|
+
if (!current) return { error: "No current fiber" };
|
|
949
|
+
|
|
950
|
+
// Walk fiber tree and collect hook data per component
|
|
951
|
+
var components = [];
|
|
952
|
+
|
|
953
|
+
function serializeValue(v) {
|
|
954
|
+
if (v === null) return null;
|
|
955
|
+
if (v === undefined) return undefined;
|
|
956
|
+
var t = typeof v;
|
|
957
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
958
|
+
if (t === "function") return "[function " + (v.name || "anonymous") + "]";
|
|
959
|
+
if (Array.isArray(v)) {
|
|
960
|
+
try { return v.map(serializeValue); } catch(e) { return "[Array]"; }
|
|
961
|
+
}
|
|
962
|
+
if (t === "object") {
|
|
963
|
+
try {
|
|
964
|
+
var keys = Object.keys(v).slice(0, 5);
|
|
965
|
+
var out = {};
|
|
966
|
+
for (var k of keys) { out[k] = serializeValue(v[k]); }
|
|
967
|
+
if (Object.keys(v).length > 5) out["..."] = "(truncated)";
|
|
968
|
+
return out;
|
|
969
|
+
} catch(e) { return "[Object]"; }
|
|
970
|
+
}
|
|
971
|
+
return String(v);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Hook node classifiers (mirrors hooks-extractor.ts logic)
|
|
975
|
+
var HookLayout = 0b00100;
|
|
976
|
+
|
|
977
|
+
function isEffectNode(node) {
|
|
978
|
+
var ms = node.memoizedState;
|
|
979
|
+
if (!ms || typeof ms !== "object") return false;
|
|
980
|
+
return typeof ms.create === "function" && "deps" in ms && typeof ms.tag === "number";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function isRefNode(node) {
|
|
984
|
+
if (node.queue != null) return false;
|
|
985
|
+
var ms = node.memoizedState;
|
|
986
|
+
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return false;
|
|
987
|
+
var keys = Object.keys(ms);
|
|
988
|
+
return keys.length === 1 && keys[0] === "current";
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function isMemoTuple(node) {
|
|
992
|
+
if (node.queue != null) return false;
|
|
993
|
+
var ms = node.memoizedState;
|
|
994
|
+
if (!Array.isArray(ms) || ms.length !== 2) return false;
|
|
995
|
+
return ms[1] === null || Array.isArray(ms[1]);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function isStateOrReducer(node) {
|
|
999
|
+
return node.queue != null &&
|
|
1000
|
+
typeof node.queue === "object" &&
|
|
1001
|
+
typeof node.queue.dispatch === "function";
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function isReducer(node) {
|
|
1005
|
+
if (!isStateOrReducer(node)) return false;
|
|
1006
|
+
var q = node.queue;
|
|
1007
|
+
if (typeof q.reducer === "function") return true;
|
|
1008
|
+
var lrr = q.lastRenderedReducer;
|
|
1009
|
+
if (typeof lrr !== "function") return false;
|
|
1010
|
+
var name = lrr.name || "";
|
|
1011
|
+
return name !== "basicStateReducer" && name !== "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function classifyHookNode(node, index) {
|
|
1015
|
+
var profile = { index: index, type: "custom" };
|
|
1016
|
+
|
|
1017
|
+
if (isEffectNode(node)) {
|
|
1018
|
+
var effect = node.memoizedState;
|
|
1019
|
+
profile.type = (effect.tag & HookLayout) ? "useLayoutEffect" : "useEffect";
|
|
1020
|
+
profile.dependencyValues = effect.deps ? effect.deps.map(serializeValue) : null;
|
|
1021
|
+
profile.cleanupPresence = typeof effect.destroy === "function";
|
|
1022
|
+
profile.fireCount = 1; // We can only observe the mount; runtime tracking would need injection
|
|
1023
|
+
profile.lastDuration = null;
|
|
1024
|
+
return profile;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (isRefNode(node)) {
|
|
1028
|
+
var ref = node.memoizedState;
|
|
1029
|
+
profile.type = "useRef";
|
|
1030
|
+
profile.currentRefValue = serializeValue(ref.current);
|
|
1031
|
+
profile.readCountDuringRender = 0; // static snapshot; read count requires instrumented wrapper
|
|
1032
|
+
return profile;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (isMemoTuple(node)) {
|
|
1036
|
+
var tuple = node.memoizedState;
|
|
1037
|
+
var val = tuple[0];
|
|
1038
|
+
var deps = tuple[1];
|
|
1039
|
+
profile.type = typeof val === "function" ? "useCallback" : "useMemo";
|
|
1040
|
+
profile.currentValue = serializeValue(val);
|
|
1041
|
+
profile.dependencyValues = deps ? deps.map(serializeValue) : null;
|
|
1042
|
+
// recomputeCount cannot be known from a single snapshot; set to 0 for mount
|
|
1043
|
+
profile.recomputeCount = 0;
|
|
1044
|
+
// On mount, first render always computes \u2192 cacheHitRate is 0 (no prior hits)
|
|
1045
|
+
profile.cacheHitRate = 0;
|
|
1046
|
+
return profile;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (isStateOrReducer(node)) {
|
|
1050
|
+
if (isReducer(node)) {
|
|
1051
|
+
profile.type = "useReducer";
|
|
1052
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1053
|
+
profile.updateCount = 0;
|
|
1054
|
+
profile.actionTypesDispatched = [];
|
|
1055
|
+
// stateTransitions: we record current state as first entry
|
|
1056
|
+
profile.stateTransitions = [serializeValue(node.memoizedState)];
|
|
1057
|
+
} else {
|
|
1058
|
+
profile.type = "useState";
|
|
1059
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1060
|
+
profile.updateCount = 0;
|
|
1061
|
+
profile.updateOrigins = [];
|
|
1062
|
+
}
|
|
1063
|
+
return profile;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// useContext: detected via _currentValue / _currentValue2 property (React context internals)
|
|
1067
|
+
// Context consumers in React store the context VALUE in memoizedState directly, not
|
|
1068
|
+
// via a queue. Context objects themselves have _currentValue.
|
|
1069
|
+
// We check if memoizedState could be a context value by seeing if node has no queue
|
|
1070
|
+
// and memoizedState is not an array and not an effect.
|
|
1071
|
+
if (!node.queue && !isRefNode(node) && !isMemoTuple(node) && !isEffectNode(node)) {
|
|
1072
|
+
profile.type = "custom";
|
|
1073
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1074
|
+
return profile;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return profile;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Walk the fiber tree
|
|
1081
|
+
var FunctionComponent = 0;
|
|
1082
|
+
var ClassComponent = 1;
|
|
1083
|
+
var ForwardRef = 11;
|
|
1084
|
+
var MemoComponent = 14;
|
|
1085
|
+
var SimpleMemoComponent = 15;
|
|
1086
|
+
var HostRoot = 3;
|
|
1087
|
+
|
|
1088
|
+
function getFiberName(fiber) {
|
|
1089
|
+
if (!fiber.type) return null;
|
|
1090
|
+
if (typeof fiber.type === "string") return fiber.type;
|
|
1091
|
+
if (typeof fiber.type === "function") return fiber.type.displayName || fiber.type.name || "Anonymous";
|
|
1092
|
+
if (fiber.type.displayName) return fiber.type.displayName;
|
|
1093
|
+
if (fiber.type.render) {
|
|
1094
|
+
return fiber.type.render.displayName || fiber.type.render.name || "ForwardRef";
|
|
1095
|
+
}
|
|
1096
|
+
if (fiber.type.type) {
|
|
1097
|
+
return (fiber.type.type.displayName || fiber.type.type.name || "Memo");
|
|
1098
|
+
}
|
|
1099
|
+
return "Unknown";
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function isComponentFiber(fiber) {
|
|
1103
|
+
var tag = fiber.tag;
|
|
1104
|
+
return tag === FunctionComponent || tag === ClassComponent ||
|
|
1105
|
+
tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function walkFiber(fiber) {
|
|
1109
|
+
if (!fiber) return;
|
|
1110
|
+
|
|
1111
|
+
if (isComponentFiber(fiber)) {
|
|
1112
|
+
var name = getFiberName(fiber);
|
|
1113
|
+
if (name) {
|
|
1114
|
+
var hooks = [];
|
|
1115
|
+
var hookNode = fiber.memoizedState;
|
|
1116
|
+
var idx = 0;
|
|
1117
|
+
while (hookNode !== null && hookNode !== undefined) {
|
|
1118
|
+
hooks.push(classifyHookNode(hookNode, idx));
|
|
1119
|
+
hookNode = hookNode.next;
|
|
1120
|
+
idx++;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
var source = null;
|
|
1124
|
+
if (fiber._debugSource) {
|
|
1125
|
+
source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (hooks.length > 0) {
|
|
1129
|
+
components.push({ name: name, source: source, hooks: hooks });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
walkFiber(fiber.child);
|
|
1135
|
+
walkFiber(fiber.sibling);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
walkFiber(current.child);
|
|
1139
|
+
|
|
1140
|
+
return { components: components };
|
|
1141
|
+
})();
|
|
1142
|
+
`;
|
|
1143
|
+
}
|
|
1144
|
+
function analyzeHookFlags(hooks) {
|
|
1145
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const hook of hooks) {
|
|
1147
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.dependencyValues !== void 0 && hook.dependencyValues === null) {
|
|
1148
|
+
flags.add("EFFECT_EVERY_RENDER");
|
|
1149
|
+
}
|
|
1150
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.cleanupPresence === false && hook.dependencyValues === null) {
|
|
1151
|
+
flags.add("MISSING_CLEANUP");
|
|
1152
|
+
}
|
|
1153
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.dependencyValues === null) {
|
|
1154
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1155
|
+
}
|
|
1156
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.cacheHitRate !== void 0 && hook.cacheHitRate === 0 && hook.recomputeCount !== void 0 && hook.recomputeCount > 1) {
|
|
1157
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1158
|
+
}
|
|
1159
|
+
if ((hook.type === "useState" || hook.type === "useReducer") && hook.updateCount !== void 0 && hook.updateCount > 10) {
|
|
1160
|
+
flags.add("STATE_UPDATE_LOOP");
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return [...flags];
|
|
1164
|
+
}
|
|
1165
|
+
async function runHooksProfiling(componentName, filePath, props) {
|
|
1166
|
+
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
1167
|
+
try {
|
|
1168
|
+
const context = await browser.newContext();
|
|
1169
|
+
const page = await context.newPage();
|
|
1170
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1171
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1172
|
+
await page.waitForFunction(
|
|
1173
|
+
() => {
|
|
1174
|
+
const w = window;
|
|
1175
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1176
|
+
},
|
|
1177
|
+
{ timeout: 15e3 }
|
|
1178
|
+
);
|
|
1179
|
+
const renderError = await page.evaluate(() => {
|
|
1180
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1181
|
+
});
|
|
1182
|
+
if (renderError !== null) {
|
|
1183
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1184
|
+
}
|
|
1185
|
+
const instrumentScript = buildHookInstrumentationScript();
|
|
1186
|
+
const raw = await page.evaluate(instrumentScript);
|
|
1187
|
+
const result = raw;
|
|
1188
|
+
if (result.error) {
|
|
1189
|
+
throw new Error(`Hook instrumentation failed: ${result.error}`);
|
|
1190
|
+
}
|
|
1191
|
+
const rawComponents = result.components ?? [];
|
|
1192
|
+
const components = rawComponents.map((c) => ({
|
|
1193
|
+
name: c.name,
|
|
1194
|
+
source: c.source,
|
|
1195
|
+
hooks: c.hooks,
|
|
1196
|
+
flags: analyzeHookFlags(c.hooks)
|
|
1197
|
+
}));
|
|
1198
|
+
const allFlags = /* @__PURE__ */ new Set();
|
|
1199
|
+
for (const comp of components) {
|
|
1200
|
+
for (const flag of comp.flags) {
|
|
1201
|
+
allFlags.add(flag);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
component: componentName,
|
|
1206
|
+
components,
|
|
1207
|
+
flags: [...allFlags]
|
|
1208
|
+
};
|
|
1209
|
+
} finally {
|
|
1210
|
+
await browser.close();
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function createInstrumentHooksCommand() {
|
|
1214
|
+
const cmd = new commander.Command("hooks").description(
|
|
1215
|
+
"Profile per-hook-instance data for a component: update counts, cache hit rates, effect counts, and more"
|
|
1216
|
+
).argument("<component>", "Component name (must exist in the manifest)").option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|text (default: auto)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
|
|
1217
|
+
async (componentName, opts) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const manifest = loadManifest(opts.manifest);
|
|
1220
|
+
const descriptor = manifest.components[componentName];
|
|
1221
|
+
if (descriptor === void 0) {
|
|
1222
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`Component "${componentName}" not found in manifest.
|
|
1225
|
+
Available: ${available}`
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
let props = {};
|
|
1229
|
+
try {
|
|
1230
|
+
props = JSON.parse(opts.props);
|
|
1231
|
+
} catch {
|
|
1232
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1233
|
+
}
|
|
1234
|
+
const rootDir = process.cwd();
|
|
1235
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1236
|
+
process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
|
|
1237
|
+
`);
|
|
1238
|
+
const result = await runHooksProfiling(componentName, filePath, props);
|
|
1239
|
+
if (opts.showFlags) {
|
|
1240
|
+
if (result.flags.length === 0) {
|
|
1241
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1242
|
+
} else {
|
|
1243
|
+
for (const flag of result.flags) {
|
|
1244
|
+
process.stdout.write(`${flag}
|
|
1245
|
+
`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1251
|
+
`);
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1254
|
+
`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1259
|
+
return cmd;
|
|
1260
|
+
}
|
|
1261
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
1262
|
+
function buildProfilingSetupScript() {
|
|
1263
|
+
return `
|
|
1264
|
+
(function __scopeProfileSetup() {
|
|
1265
|
+
window.__scopeProfileData = {
|
|
1266
|
+
commitCount: 0,
|
|
1267
|
+
componentNames: new Set(),
|
|
1268
|
+
fiberSnapshots: []
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1272
|
+
if (!hook) return { error: "No DevTools hook" };
|
|
1273
|
+
|
|
1274
|
+
// Wrap onCommitFiberRoot to count commits and collect component names
|
|
1275
|
+
var origCommit = hook.onCommitFiberRoot.bind(hook);
|
|
1276
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1277
|
+
origCommit(rendererID, root, priorityLevel);
|
|
1278
|
+
|
|
1279
|
+
window.__scopeProfileData.commitCount++;
|
|
1280
|
+
|
|
1281
|
+
// Walk the committed tree to collect re-rendered component names
|
|
1282
|
+
var current = root && root.current;
|
|
1283
|
+
if (!current) return;
|
|
1284
|
+
|
|
1285
|
+
function walkFiber(fiber) {
|
|
1286
|
+
if (!fiber) return;
|
|
1287
|
+
var tag = fiber.tag;
|
|
1288
|
+
// FunctionComponent=0, ClassComponent=1, ForwardRef=11, Memo=14, SimpleMemo=15
|
|
1289
|
+
if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
|
|
1290
|
+
var name = null;
|
|
1291
|
+
if (fiber.type) {
|
|
1292
|
+
if (typeof fiber.type === "function") name = fiber.type.displayName || fiber.type.name;
|
|
1293
|
+
else if (fiber.type.displayName) name = fiber.type.displayName;
|
|
1294
|
+
else if (fiber.type.render) name = fiber.type.render.displayName || fiber.type.render.name;
|
|
1295
|
+
else if (fiber.type.type) name = fiber.type.type.displayName || fiber.type.type.name;
|
|
1296
|
+
}
|
|
1297
|
+
// Only count fibers with a positive actualDuration (actually re-rendered this commit)
|
|
1298
|
+
if (name && typeof fiber.actualDuration === "number" && fiber.actualDuration >= 0) {
|
|
1299
|
+
window.__scopeProfileData.componentNames.add(name);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
walkFiber(fiber.child);
|
|
1303
|
+
walkFiber(fiber.sibling);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
var wip = root.current.alternate || root.current;
|
|
1307
|
+
walkFiber(wip.child);
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
// Install PerformanceObserver for layout and paint timing
|
|
1311
|
+
window.__scopeLayoutTime = 0;
|
|
1312
|
+
window.__scopePaintTime = 0;
|
|
1313
|
+
window.__scopeLayoutShifts = { count: 0, score: 0 };
|
|
1314
|
+
|
|
1315
|
+
try {
|
|
1316
|
+
var observer = new PerformanceObserver(function(list) {
|
|
1317
|
+
for (var entry of list.getEntries()) {
|
|
1318
|
+
if (entry.entryType === "layout-shift") {
|
|
1319
|
+
window.__scopeLayoutShifts.count++;
|
|
1320
|
+
window.__scopeLayoutShifts.score += entry.value || 0;
|
|
1321
|
+
}
|
|
1322
|
+
if (entry.entryType === "longtask") {
|
|
1323
|
+
window.__scopeLayoutTime += entry.duration || 0;
|
|
1324
|
+
}
|
|
1325
|
+
if (entry.entryType === "paint") {
|
|
1326
|
+
window.__scopePaintTime += entry.startTime || 0;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
observer.observe({ entryTypes: ["layout-shift", "longtask", "paint"] });
|
|
1331
|
+
} catch(e) {
|
|
1332
|
+
// PerformanceObserver may not be available in all Playwright contexts
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return { ok: true };
|
|
1336
|
+
})();
|
|
1337
|
+
`;
|
|
1338
|
+
}
|
|
1339
|
+
function buildProfilingCollectScript() {
|
|
1340
|
+
return `
|
|
1341
|
+
(function __scopeProfileCollect() {
|
|
1342
|
+
var data = window.__scopeProfileData;
|
|
1343
|
+
if (!data) return { error: "Profiling not set up" };
|
|
1344
|
+
|
|
1345
|
+
// Estimate wasted renders: use paint entries count as a heuristic for
|
|
1346
|
+
// "components that re-rendered but their subtree output was likely unchanged".
|
|
1347
|
+
// A more accurate method would require React's own wasted-render detection,
|
|
1348
|
+
// which requires React Profiler API. We use a conservative estimate here.
|
|
1349
|
+
var totalCommits = data.commitCount;
|
|
1350
|
+
var uniqueNames = Array.from(data.componentNames);
|
|
1351
|
+
|
|
1352
|
+
// Wasted renders heuristic: if a component is in a subsequent commit (not the initial
|
|
1353
|
+
// mount commit), it *may* have been wasted if it didn't actually need to re-render.
|
|
1354
|
+
// For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
|
|
1355
|
+
// This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
|
|
1356
|
+
var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
|
|
1357
|
+
|
|
1358
|
+
return {
|
|
1359
|
+
commitCount: totalCommits,
|
|
1360
|
+
uniqueComponents: uniqueNames.length,
|
|
1361
|
+
componentNames: uniqueNames,
|
|
1362
|
+
wastedRenders: wastedRenders,
|
|
1363
|
+
layoutTime: window.__scopeLayoutTime || 0,
|
|
1364
|
+
paintTime: window.__scopePaintTime || 0,
|
|
1365
|
+
layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
|
|
1366
|
+
};
|
|
1367
|
+
})();
|
|
1368
|
+
`;
|
|
1369
|
+
}
|
|
1370
|
+
async function replayInteraction(page, steps) {
|
|
1371
|
+
for (const step of steps) {
|
|
1372
|
+
switch (step.action) {
|
|
1373
|
+
case "click":
|
|
1374
|
+
if (step.target) {
|
|
1375
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1376
|
+
process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
|
|
1377
|
+
`);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
case "fill":
|
|
1382
|
+
if (step.target && step.value !== void 0) {
|
|
1383
|
+
await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
|
|
1384
|
+
process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
|
|
1385
|
+
`);
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
break;
|
|
1389
|
+
case "hover":
|
|
1390
|
+
if (step.target) {
|
|
1391
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1392
|
+
process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
|
|
1393
|
+
`);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
break;
|
|
1397
|
+
case "press":
|
|
1398
|
+
if (step.value) {
|
|
1399
|
+
await page.keyboard.press(step.value);
|
|
1400
|
+
}
|
|
1401
|
+
break;
|
|
1402
|
+
case "wait":
|
|
1403
|
+
await page.waitForTimeout(step.delay ?? 500);
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
|
|
1409
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1410
|
+
if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
|
|
1411
|
+
flags.add("WASTED_RENDER");
|
|
1412
|
+
}
|
|
1413
|
+
if (totalRenders > 10) {
|
|
1414
|
+
flags.add("HIGH_RENDER_COUNT");
|
|
1415
|
+
}
|
|
1416
|
+
if (layoutShifts.cumulativeScore > 0.1) {
|
|
1417
|
+
flags.add("LAYOUT_SHIFT_DETECTED");
|
|
1418
|
+
}
|
|
1419
|
+
if (timing.js > 100) {
|
|
1420
|
+
flags.add("SLOW_INTERACTION");
|
|
1421
|
+
}
|
|
1422
|
+
return [...flags];
|
|
1423
|
+
}
|
|
1424
|
+
async function runInteractionProfile(componentName, filePath, props, interaction) {
|
|
1425
|
+
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
1426
|
+
try {
|
|
1427
|
+
const context = await browser.newContext();
|
|
1428
|
+
const page = await context.newPage();
|
|
1429
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1430
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1431
|
+
await page.waitForFunction(
|
|
1432
|
+
() => {
|
|
1433
|
+
const w = window;
|
|
1434
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1435
|
+
},
|
|
1436
|
+
{ timeout: 15e3 }
|
|
1437
|
+
);
|
|
1438
|
+
const renderError = await page.evaluate(() => {
|
|
1439
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1440
|
+
});
|
|
1441
|
+
if (renderError !== null) {
|
|
1442
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1443
|
+
}
|
|
1444
|
+
const setupResult = await page.evaluate(buildProfilingSetupScript());
|
|
1445
|
+
const setupData = setupResult;
|
|
1446
|
+
if (setupData.error) {
|
|
1447
|
+
throw new Error(`Profiling setup failed: ${setupData.error}`);
|
|
1448
|
+
}
|
|
1449
|
+
const jsStart = Date.now();
|
|
1450
|
+
if (interaction.length > 0) {
|
|
1451
|
+
process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
|
|
1452
|
+
`);
|
|
1453
|
+
await replayInteraction(page, interaction);
|
|
1454
|
+
await page.waitForTimeout(300);
|
|
1455
|
+
}
|
|
1456
|
+
const jsDuration = Date.now() - jsStart;
|
|
1457
|
+
const collected = await page.evaluate(buildProfilingCollectScript());
|
|
1458
|
+
const profileData = collected;
|
|
1459
|
+
if (profileData.error) {
|
|
1460
|
+
throw new Error(`Profile collection failed: ${profileData.error}`);
|
|
1461
|
+
}
|
|
1462
|
+
const timing = {
|
|
1463
|
+
js: jsDuration,
|
|
1464
|
+
layout: profileData.layoutTime ?? 0,
|
|
1465
|
+
paint: profileData.paintTime ?? 0
|
|
1466
|
+
};
|
|
1467
|
+
const layoutShifts = {
|
|
1468
|
+
count: profileData.layoutShifts?.count ?? 0,
|
|
1469
|
+
cumulativeScore: profileData.layoutShifts?.score ?? 0
|
|
1470
|
+
};
|
|
1471
|
+
const totalRenders = profileData.commitCount ?? 0;
|
|
1472
|
+
const uniqueComponents = profileData.uniqueComponents ?? 0;
|
|
1473
|
+
const wastedRenders = profileData.wastedRenders ?? 0;
|
|
1474
|
+
const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
|
|
1475
|
+
return {
|
|
1476
|
+
component: componentName,
|
|
1477
|
+
totalRenders,
|
|
1478
|
+
uniqueComponents,
|
|
1479
|
+
wastedRenders,
|
|
1480
|
+
timing,
|
|
1481
|
+
layoutShifts,
|
|
1482
|
+
flags,
|
|
1483
|
+
interaction
|
|
1484
|
+
};
|
|
1485
|
+
} finally {
|
|
1486
|
+
await browser.close();
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function createInstrumentProfileCommand() {
|
|
1490
|
+
const cmd = new commander.Command("profile").description(
|
|
1491
|
+
"Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
|
|
1492
|
+
).argument("<component>", "Component name (must exist in the manifest)").option(
|
|
1493
|
+
"--interaction <json>",
|
|
1494
|
+
`Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
|
|
1495
|
+
"[]"
|
|
1496
|
+
).option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|text (default: json)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
|
|
1497
|
+
async (componentName, opts) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const manifest = loadManifest(opts.manifest);
|
|
1500
|
+
const descriptor = manifest.components[componentName];
|
|
1501
|
+
if (descriptor === void 0) {
|
|
1502
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
`Component "${componentName}" not found in manifest.
|
|
1505
|
+
Available: ${available}`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
let props = {};
|
|
1509
|
+
try {
|
|
1510
|
+
props = JSON.parse(opts.props);
|
|
1511
|
+
} catch {
|
|
1512
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1513
|
+
}
|
|
1514
|
+
let interaction = [];
|
|
1515
|
+
try {
|
|
1516
|
+
interaction = JSON.parse(opts.interaction);
|
|
1517
|
+
if (!Array.isArray(interaction)) {
|
|
1518
|
+
throw new Error("Interaction must be a JSON array");
|
|
1519
|
+
}
|
|
1520
|
+
} catch {
|
|
1521
|
+
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
1522
|
+
}
|
|
1523
|
+
const rootDir = process.cwd();
|
|
1524
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1525
|
+
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
1526
|
+
`);
|
|
1527
|
+
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
1528
|
+
if (opts.showFlags) {
|
|
1529
|
+
if (result.flags.length === 0) {
|
|
1530
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1531
|
+
} else {
|
|
1532
|
+
for (const flag of result.flags) {
|
|
1533
|
+
process.stdout.write(`${flag}
|
|
1534
|
+
`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1540
|
+
`);
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1543
|
+
`);
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1548
|
+
return cmd;
|
|
1549
|
+
}
|
|
1550
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1551
|
+
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1552
|
+
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1553
|
+
var _pool = null;
|
|
1554
|
+
async function getPool() {
|
|
1555
|
+
if (_pool === null) {
|
|
1556
|
+
_pool = new render.BrowserPool({
|
|
1557
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1558
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1559
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1560
|
+
});
|
|
1561
|
+
await _pool.init();
|
|
1562
|
+
}
|
|
1563
|
+
return _pool;
|
|
1564
|
+
}
|
|
1565
|
+
async function shutdownPool() {
|
|
1566
|
+
if (_pool !== null) {
|
|
1567
|
+
await _pool.close();
|
|
1568
|
+
_pool = null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function mapNodeType(node) {
|
|
1572
|
+
if (node.type === "forward_ref") return "forwardRef";
|
|
1573
|
+
if (node.type === "host") return "host";
|
|
1574
|
+
const name = node.name;
|
|
1575
|
+
if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
|
|
1576
|
+
if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
|
|
1577
|
+
return node.type;
|
|
1578
|
+
}
|
|
1579
|
+
function flattenSerializedValue(sv) {
|
|
1580
|
+
if (sv === null || sv === void 0) return null;
|
|
1581
|
+
const v = sv;
|
|
1582
|
+
switch (v.type) {
|
|
1583
|
+
case "null":
|
|
1584
|
+
case "undefined":
|
|
1585
|
+
return null;
|
|
1586
|
+
case "string":
|
|
1587
|
+
case "number":
|
|
1588
|
+
case "boolean":
|
|
1589
|
+
return v.value;
|
|
1590
|
+
case "object": {
|
|
1591
|
+
if (!Array.isArray(v.entries)) return {};
|
|
1592
|
+
const result = {};
|
|
1593
|
+
for (const entry of v.entries) {
|
|
1594
|
+
result[entry.key] = flattenSerializedValue(entry.value);
|
|
1595
|
+
}
|
|
1596
|
+
return result;
|
|
1597
|
+
}
|
|
1598
|
+
case "array": {
|
|
1599
|
+
if (!Array.isArray(v.items)) return [];
|
|
1600
|
+
return v.items.map(flattenSerializedValue);
|
|
1601
|
+
}
|
|
1602
|
+
case "function":
|
|
1603
|
+
return "[Function]";
|
|
1604
|
+
case "symbol":
|
|
1605
|
+
return `[Symbol: ${v.description ?? ""}]`;
|
|
1606
|
+
case "circular":
|
|
1607
|
+
return "[Circular]";
|
|
1608
|
+
case "truncated":
|
|
1609
|
+
return `[Truncated: ${v.preview ?? ""}]`;
|
|
1610
|
+
default:
|
|
1611
|
+
return v.preview ?? null;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function flattenHookState(hooks) {
|
|
1615
|
+
const result = {};
|
|
1616
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1617
|
+
const hook = hooks[i];
|
|
1618
|
+
if (hook === void 0) continue;
|
|
1619
|
+
const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
|
|
1620
|
+
result[key] = flattenSerializedValue(hook.value);
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
function extractContextNames(contexts) {
|
|
1625
|
+
const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1626
|
+
return names;
|
|
1627
|
+
}
|
|
1628
|
+
function anyContextChanged(contexts) {
|
|
1629
|
+
return contexts.some((c) => c.didTriggerRender);
|
|
1630
|
+
}
|
|
1631
|
+
function convertToInstrumentNode(node, depth = 0) {
|
|
1632
|
+
const contexts = extractContextNames(node.context);
|
|
1633
|
+
const contextChanged = anyContextChanged(node.context);
|
|
1634
|
+
const state = flattenHookState(node.state);
|
|
1635
|
+
const propsFlat = flattenSerializedValue(node.props);
|
|
1636
|
+
const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
|
|
1637
|
+
return {
|
|
1638
|
+
component: node.name,
|
|
1639
|
+
type: mapNodeType(node),
|
|
1640
|
+
renderCount: node.renderCount,
|
|
1641
|
+
lastRenderDuration: node.renderDuration,
|
|
1642
|
+
memoized: node.type === "memo",
|
|
1643
|
+
// memoSkipped requires tracking bail-outs across commits — not available from
|
|
1644
|
+
// a single-shot capture. Defaulted to 0.
|
|
1645
|
+
memoSkipped: 0,
|
|
1646
|
+
props,
|
|
1647
|
+
// propsChanged is not tracked in a single-shot capture — would need a diff
|
|
1648
|
+
// between two renders. Defaulted to false.
|
|
1649
|
+
propsChanged: false,
|
|
1650
|
+
state,
|
|
1651
|
+
stateChanged: false,
|
|
1652
|
+
contextChanged,
|
|
1653
|
+
contexts,
|
|
1654
|
+
depth,
|
|
1655
|
+
children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function filterByContext(node, contextName) {
|
|
1659
|
+
const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
|
|
1660
|
+
const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
|
|
1661
|
+
if (!selfMatches && filteredChildren.length === 0) return null;
|
|
1662
|
+
return { ...node, children: filteredChildren };
|
|
1663
|
+
}
|
|
1664
|
+
function filterWastedRenders(node) {
|
|
1665
|
+
const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
|
|
1666
|
+
const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
|
|
1667
|
+
if (!isWasted && filteredChildren.length === 0) return null;
|
|
1668
|
+
return { ...node, children: filteredChildren };
|
|
1669
|
+
}
|
|
1670
|
+
function sortTree(node, sortBy) {
|
|
1671
|
+
const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
|
|
1672
|
+
if (sortBy === "renderCount") return b.renderCount - a.renderCount;
|
|
1673
|
+
return a.depth - b.depth;
|
|
1674
|
+
});
|
|
1675
|
+
return { ...node, children: sortedChildren };
|
|
1676
|
+
}
|
|
1677
|
+
function annotateProviderDepth(node, providerDepth = 0) {
|
|
1678
|
+
const isProvider = node.type === "context.provider";
|
|
1679
|
+
const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
|
|
1680
|
+
return {
|
|
1681
|
+
...node,
|
|
1682
|
+
_providerDepth: providerDepth,
|
|
1683
|
+
children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function limitNodes(root, limit) {
|
|
1687
|
+
let remaining = limit;
|
|
1688
|
+
const clip = (node) => {
|
|
1689
|
+
if (remaining <= 0) return null;
|
|
1690
|
+
remaining--;
|
|
1691
|
+
const clippedChildren = [];
|
|
1692
|
+
for (const child of node.children) {
|
|
1693
|
+
const clipped = clip(child);
|
|
1694
|
+
if (clipped !== null) clippedChildren.push(clipped);
|
|
1695
|
+
}
|
|
1696
|
+
return { ...node, children: clippedChildren };
|
|
1697
|
+
};
|
|
1698
|
+
return clip(root) ?? root;
|
|
1699
|
+
}
|
|
1700
|
+
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1701
|
+
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
1702
|
+
var VERTICAL = "\u2502 ";
|
|
1703
|
+
var EMPTY = " ";
|
|
1704
|
+
function buildTTYLabel(node, showProviderDepth) {
|
|
1705
|
+
const parts = [node.component];
|
|
1706
|
+
switch (node.type) {
|
|
1707
|
+
case "memo":
|
|
1708
|
+
parts.push("[memo]");
|
|
1709
|
+
break;
|
|
1710
|
+
case "forwardRef":
|
|
1711
|
+
parts.push("[forwardRef]");
|
|
1712
|
+
break;
|
|
1713
|
+
case "class":
|
|
1714
|
+
parts.push("[class]");
|
|
1715
|
+
break;
|
|
1716
|
+
case "context.provider":
|
|
1717
|
+
parts.push("[provider]");
|
|
1718
|
+
break;
|
|
1719
|
+
case "context.consumer":
|
|
1720
|
+
parts.push("[consumer]");
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
if (node.renderCount > 0) {
|
|
1724
|
+
const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
|
|
1725
|
+
parts.push(`(renders:${node.renderCount}${durationStr})`);
|
|
1726
|
+
}
|
|
1727
|
+
if (node.contexts.length > 0) {
|
|
1728
|
+
parts.push(`[ctx:${node.contexts.join(",")}]`);
|
|
1729
|
+
}
|
|
1730
|
+
if (showProviderDepth) {
|
|
1731
|
+
const pd = node._providerDepth;
|
|
1732
|
+
if (pd !== void 0 && pd > 0) {
|
|
1733
|
+
parts.push(`[pd:${pd}]`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return parts.join(" ");
|
|
1737
|
+
}
|
|
1738
|
+
function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
|
|
1739
|
+
if (node.type === "host") {
|
|
1740
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1741
|
+
const child = node.children[i];
|
|
1742
|
+
if (child !== void 0) {
|
|
1743
|
+
renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const connector = isLast ? LAST_BRANCH : BRANCH;
|
|
1749
|
+
lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
|
|
1750
|
+
const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
|
|
1751
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1752
|
+
const child = node.children[i];
|
|
1753
|
+
if (child !== void 0) {
|
|
1754
|
+
renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function formatInstrumentTree(root, showProviderDepth = false) {
|
|
1759
|
+
const lines = [];
|
|
1760
|
+
if (root.type !== "host") {
|
|
1761
|
+
lines.push(buildTTYLabel(root, showProviderDepth));
|
|
1762
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1763
|
+
const child = root.children[i];
|
|
1764
|
+
if (child !== void 0) {
|
|
1765
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1770
|
+
const child = root.children[i];
|
|
1771
|
+
if (child !== void 0) {
|
|
1772
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
944
1776
|
return lines.join("\n");
|
|
945
1777
|
}
|
|
946
|
-
function
|
|
947
|
-
|
|
1778
|
+
async function runInstrumentTree(options) {
|
|
1779
|
+
const { componentName, filePath } = options;
|
|
1780
|
+
const pool = await getPool();
|
|
1781
|
+
const slot = await pool.acquire();
|
|
1782
|
+
const { page } = slot;
|
|
1783
|
+
try {
|
|
1784
|
+
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
1785
|
+
const htmlHarness = await buildComponentHarness(
|
|
1786
|
+
filePath,
|
|
1787
|
+
componentName,
|
|
1788
|
+
{},
|
|
1789
|
+
DEFAULT_VIEWPORT_WIDTH
|
|
1790
|
+
);
|
|
1791
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1792
|
+
await page.waitForFunction(
|
|
1793
|
+
() => {
|
|
1794
|
+
const w = window;
|
|
1795
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1796
|
+
},
|
|
1797
|
+
{ timeout: 15e3 }
|
|
1798
|
+
);
|
|
1799
|
+
const renderError = await page.evaluate(
|
|
1800
|
+
() => window.__SCOPE_RENDER_ERROR__ ?? null
|
|
1801
|
+
);
|
|
1802
|
+
if (renderError !== null) {
|
|
1803
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1804
|
+
}
|
|
1805
|
+
const captureJson = await page.evaluate(async () => {
|
|
1806
|
+
const w = window;
|
|
1807
|
+
if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
|
|
1808
|
+
throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
|
|
1809
|
+
}
|
|
1810
|
+
return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
|
|
1811
|
+
});
|
|
1812
|
+
const captureResult = JSON.parse(captureJson);
|
|
1813
|
+
const componentTree = captureResult.tree;
|
|
1814
|
+
if (componentTree === void 0 || componentTree === null) {
|
|
1815
|
+
throw new Error(`No component tree found for "${componentName}"`);
|
|
1816
|
+
}
|
|
1817
|
+
let instrumentRoot = convertToInstrumentNode(componentTree, 0);
|
|
1818
|
+
if (options.usesContext !== void 0) {
|
|
1819
|
+
const filtered = filterByContext(instrumentRoot, options.usesContext);
|
|
1820
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1821
|
+
}
|
|
1822
|
+
if (options.wastedRenders === true) {
|
|
1823
|
+
const filtered = filterWastedRenders(instrumentRoot);
|
|
1824
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1825
|
+
}
|
|
1826
|
+
if (options.sortBy !== void 0) {
|
|
1827
|
+
instrumentRoot = sortTree(instrumentRoot, options.sortBy);
|
|
1828
|
+
}
|
|
1829
|
+
if (options.providerDepth === true) {
|
|
1830
|
+
instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
|
|
1831
|
+
}
|
|
1832
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
1833
|
+
instrumentRoot = limitNodes(instrumentRoot, options.limit);
|
|
1834
|
+
}
|
|
1835
|
+
return instrumentRoot;
|
|
1836
|
+
} finally {
|
|
1837
|
+
pool.release(slot);
|
|
1838
|
+
}
|
|
948
1839
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
1840
|
+
function createInstrumentTreeCommand() {
|
|
1841
|
+
return new commander.Command("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
1842
|
+
"--wasted-renders",
|
|
1843
|
+
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1844
|
+
false
|
|
1845
|
+
).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
|
|
1846
|
+
try {
|
|
1847
|
+
const manifest = loadManifest(opts.manifest);
|
|
1848
|
+
const descriptor = manifest.components[componentName];
|
|
1849
|
+
if (descriptor === void 0) {
|
|
1850
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`Component "${componentName}" not found in manifest.
|
|
1853
|
+
Available: ${available}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
if (opts.sortBy !== void 0) {
|
|
1857
|
+
const allowed = ["renderCount", "depth"];
|
|
1858
|
+
if (!allowed.includes(opts.sortBy)) {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const rootDir = process.cwd();
|
|
1865
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1866
|
+
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1867
|
+
`);
|
|
1868
|
+
const instrumentRoot = await runInstrumentTree({
|
|
1869
|
+
componentName,
|
|
1870
|
+
filePath,
|
|
1871
|
+
sortBy: opts.sortBy,
|
|
1872
|
+
limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
|
|
1873
|
+
usesContext: opts.usesContext,
|
|
1874
|
+
providerDepth: opts.providerDepth,
|
|
1875
|
+
wastedRenders: opts.wastedRenders
|
|
1876
|
+
});
|
|
1877
|
+
await shutdownPool();
|
|
1878
|
+
const fmt = resolveFormat2(opts.format);
|
|
1879
|
+
if (fmt === "json") {
|
|
1880
|
+
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1881
|
+
`);
|
|
1882
|
+
} else {
|
|
1883
|
+
const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
|
|
1884
|
+
process.stdout.write(`${tree}
|
|
1885
|
+
`);
|
|
1886
|
+
}
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
await shutdownPool();
|
|
1889
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1890
|
+
`);
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
function resolveFormat2(formatFlag) {
|
|
1896
|
+
if (formatFlag !== void 0) {
|
|
1897
|
+
const lower = formatFlag.toLowerCase();
|
|
1898
|
+
if (lower !== "json" && lower !== "tree") {
|
|
1899
|
+
throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
|
|
1900
|
+
}
|
|
1901
|
+
return lower;
|
|
952
1902
|
}
|
|
953
|
-
return
|
|
1903
|
+
return isTTY() ? "tree" : "json";
|
|
954
1904
|
}
|
|
955
1905
|
|
|
956
1906
|
// src/instrument/renders.ts
|
|
957
|
-
var
|
|
1907
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
958
1908
|
function determineTrigger(event) {
|
|
959
1909
|
if (event.forceUpdate) return "force_update";
|
|
960
1910
|
if (event.stateChanged) return "state_change";
|
|
@@ -1199,7 +2149,7 @@ function buildInstrumentationScript() {
|
|
|
1199
2149
|
`
|
|
1200
2150
|
);
|
|
1201
2151
|
}
|
|
1202
|
-
async function
|
|
2152
|
+
async function replayInteraction2(page, steps) {
|
|
1203
2153
|
for (const step of steps) {
|
|
1204
2154
|
switch (step.action) {
|
|
1205
2155
|
case "click":
|
|
@@ -1253,26 +2203,26 @@ async function replayInteraction(page, steps) {
|
|
|
1253
2203
|
}
|
|
1254
2204
|
}
|
|
1255
2205
|
}
|
|
1256
|
-
var
|
|
1257
|
-
async function
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
2206
|
+
var _pool2 = null;
|
|
2207
|
+
async function getPool2() {
|
|
2208
|
+
if (_pool2 === null) {
|
|
2209
|
+
_pool2 = new render.BrowserPool({
|
|
1260
2210
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1261
2211
|
viewportWidth: 1280,
|
|
1262
2212
|
viewportHeight: 800
|
|
1263
2213
|
});
|
|
1264
|
-
await
|
|
2214
|
+
await _pool2.init();
|
|
1265
2215
|
}
|
|
1266
|
-
return
|
|
2216
|
+
return _pool2;
|
|
1267
2217
|
}
|
|
1268
|
-
async function
|
|
1269
|
-
if (
|
|
1270
|
-
await
|
|
1271
|
-
|
|
2218
|
+
async function shutdownPool2() {
|
|
2219
|
+
if (_pool2 !== null) {
|
|
2220
|
+
await _pool2.close();
|
|
2221
|
+
_pool2 = null;
|
|
1272
2222
|
}
|
|
1273
2223
|
}
|
|
1274
2224
|
async function analyzeRenders(options) {
|
|
1275
|
-
const manifestPath = options.manifestPath ??
|
|
2225
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
|
|
1276
2226
|
const manifest = loadManifest(manifestPath);
|
|
1277
2227
|
const descriptor = manifest.components[options.componentName];
|
|
1278
2228
|
if (descriptor === void 0) {
|
|
@@ -1285,7 +2235,7 @@ Available: ${available}`
|
|
|
1285
2235
|
const rootDir = process.cwd();
|
|
1286
2236
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1287
2237
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1288
|
-
const pool = await
|
|
2238
|
+
const pool = await getPool2();
|
|
1289
2239
|
const slot = await pool.acquire();
|
|
1290
2240
|
const { page } = slot;
|
|
1291
2241
|
const startMs = performance.now();
|
|
@@ -1301,7 +2251,7 @@ Available: ${available}`
|
|
|
1301
2251
|
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1302
2252
|
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1303
2253
|
});
|
|
1304
|
-
await
|
|
2254
|
+
await replayInteraction2(page, options.interaction);
|
|
1305
2255
|
await page.waitForTimeout(200);
|
|
1306
2256
|
const interactionDurationMs = performance.now() - startMs;
|
|
1307
2257
|
const rawEvents = await page.evaluate(() => {
|
|
@@ -1370,7 +2320,7 @@ function createInstrumentRendersCommand() {
|
|
|
1370
2320
|
"--interaction <json>",
|
|
1371
2321
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1372
2322
|
"[]"
|
|
1373
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2323
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1374
2324
|
async (componentName, opts) => {
|
|
1375
2325
|
let interaction = [];
|
|
1376
2326
|
try {
|
|
@@ -1393,7 +2343,7 @@ function createInstrumentRendersCommand() {
|
|
|
1393
2343
|
interaction,
|
|
1394
2344
|
manifestPath: opts.manifest
|
|
1395
2345
|
});
|
|
1396
|
-
await
|
|
2346
|
+
await shutdownPool2();
|
|
1397
2347
|
if (opts.json || !isTTY()) {
|
|
1398
2348
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1399
2349
|
`);
|
|
@@ -1402,7 +2352,7 @@ function createInstrumentRendersCommand() {
|
|
|
1402
2352
|
`);
|
|
1403
2353
|
}
|
|
1404
2354
|
} catch (err) {
|
|
1405
|
-
await
|
|
2355
|
+
await shutdownPool2();
|
|
1406
2356
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1407
2357
|
`);
|
|
1408
2358
|
process.exit(1);
|
|
@@ -1415,8 +2365,51 @@ function createInstrumentCommand() {
|
|
|
1415
2365
|
"Structured instrumentation commands for React component analysis"
|
|
1416
2366
|
);
|
|
1417
2367
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2368
|
+
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2369
|
+
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
2370
|
+
instrumentCmd.addCommand(createInstrumentTreeCommand());
|
|
1418
2371
|
return instrumentCmd;
|
|
1419
2372
|
}
|
|
2373
|
+
async function browserCapture(options) {
|
|
2374
|
+
const { url, timeout = 1e4, wait = 0 } = options;
|
|
2375
|
+
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
2376
|
+
try {
|
|
2377
|
+
const context = await browser.newContext();
|
|
2378
|
+
const page = await context.newPage();
|
|
2379
|
+
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
2380
|
+
await page.goto(url, {
|
|
2381
|
+
waitUntil: "networkidle",
|
|
2382
|
+
timeout: timeout + 5e3
|
|
2383
|
+
});
|
|
2384
|
+
await page.waitForFunction(
|
|
2385
|
+
() => {
|
|
2386
|
+
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
2387
|
+
},
|
|
2388
|
+
{ timeout }
|
|
2389
|
+
);
|
|
2390
|
+
if (wait > 0) {
|
|
2391
|
+
await page.waitForTimeout(wait);
|
|
2392
|
+
}
|
|
2393
|
+
const raw = await page.evaluate(async () => {
|
|
2394
|
+
const win = window;
|
|
2395
|
+
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
2396
|
+
throw new Error("Scope runtime not injected");
|
|
2397
|
+
}
|
|
2398
|
+
return win.__SCOPE_CAPTURE__();
|
|
2399
|
+
});
|
|
2400
|
+
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
2401
|
+
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
2402
|
+
}
|
|
2403
|
+
const report = { ...raw, route: null };
|
|
2404
|
+
return { report };
|
|
2405
|
+
} finally {
|
|
2406
|
+
await browser.close();
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
2410
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2411
|
+
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2412
|
+
}
|
|
1420
2413
|
var CONFIG_FILENAMES = [
|
|
1421
2414
|
".reactscope/config.json",
|
|
1422
2415
|
".reactscope/config.js",
|
|
@@ -1534,24 +2527,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
1534
2527
|
}
|
|
1535
2528
|
|
|
1536
2529
|
// src/render-commands.ts
|
|
1537
|
-
var
|
|
2530
|
+
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
1538
2531
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
1539
|
-
var
|
|
1540
|
-
async function
|
|
1541
|
-
if (
|
|
1542
|
-
|
|
2532
|
+
var _pool3 = null;
|
|
2533
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2534
|
+
if (_pool3 === null) {
|
|
2535
|
+
_pool3 = new render.BrowserPool({
|
|
1543
2536
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
1544
2537
|
viewportWidth,
|
|
1545
2538
|
viewportHeight
|
|
1546
2539
|
});
|
|
1547
|
-
await
|
|
2540
|
+
await _pool3.init();
|
|
1548
2541
|
}
|
|
1549
|
-
return
|
|
2542
|
+
return _pool3;
|
|
1550
2543
|
}
|
|
1551
|
-
async function
|
|
1552
|
-
if (
|
|
1553
|
-
await
|
|
1554
|
-
|
|
2544
|
+
async function shutdownPool3() {
|
|
2545
|
+
if (_pool3 !== null) {
|
|
2546
|
+
await _pool3.close();
|
|
2547
|
+
_pool3 = null;
|
|
1555
2548
|
}
|
|
1556
2549
|
}
|
|
1557
2550
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -1562,7 +2555,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1562
2555
|
_satori: satori,
|
|
1563
2556
|
async renderCell(props, _complexityClass) {
|
|
1564
2557
|
const startMs = performance.now();
|
|
1565
|
-
const pool = await
|
|
2558
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
1566
2559
|
const htmlHarness = await buildComponentHarness(
|
|
1567
2560
|
filePath,
|
|
1568
2561
|
componentName,
|
|
@@ -1659,7 +2652,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1659
2652
|
};
|
|
1660
2653
|
}
|
|
1661
2654
|
function registerRenderSingle(renderCmd) {
|
|
1662
|
-
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json",
|
|
2655
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
1663
2656
|
async (componentName, opts) => {
|
|
1664
2657
|
try {
|
|
1665
2658
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1698,7 +2691,7 @@ Available: ${available}`
|
|
|
1698
2691
|
}
|
|
1699
2692
|
}
|
|
1700
2693
|
);
|
|
1701
|
-
await
|
|
2694
|
+
await shutdownPool3();
|
|
1702
2695
|
if (outcome.crashed) {
|
|
1703
2696
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
1704
2697
|
`);
|
|
@@ -1746,7 +2739,7 @@ Available: ${available}`
|
|
|
1746
2739
|
);
|
|
1747
2740
|
}
|
|
1748
2741
|
} catch (err) {
|
|
1749
|
-
await
|
|
2742
|
+
await shutdownPool3();
|
|
1750
2743
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1751
2744
|
`);
|
|
1752
2745
|
process.exit(1);
|
|
@@ -1758,7 +2751,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
1758
2751
|
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
|
|
1759
2752
|
"--contexts <ids>",
|
|
1760
2753
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
1761
|
-
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json",
|
|
2754
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
1762
2755
|
async (componentName, opts) => {
|
|
1763
2756
|
try {
|
|
1764
2757
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1831,7 +2824,7 @@ Available: ${available}`
|
|
|
1831
2824
|
concurrency
|
|
1832
2825
|
});
|
|
1833
2826
|
const result = await matrix.render();
|
|
1834
|
-
await
|
|
2827
|
+
await shutdownPool3();
|
|
1835
2828
|
process.stderr.write(
|
|
1836
2829
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
1837
2830
|
`
|
|
@@ -1876,7 +2869,7 @@ Available: ${available}`
|
|
|
1876
2869
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
1877
2870
|
}
|
|
1878
2871
|
} catch (err) {
|
|
1879
|
-
await
|
|
2872
|
+
await shutdownPool3();
|
|
1880
2873
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1881
2874
|
`);
|
|
1882
2875
|
process.exit(1);
|
|
@@ -1885,7 +2878,7 @@ Available: ${available}`
|
|
|
1885
2878
|
);
|
|
1886
2879
|
}
|
|
1887
2880
|
function registerRenderAll(renderCmd) {
|
|
1888
|
-
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json",
|
|
2881
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1889
2882
|
async (opts) => {
|
|
1890
2883
|
try {
|
|
1891
2884
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1973,13 +2966,13 @@ function registerRenderAll(renderCmd) {
|
|
|
1973
2966
|
workers.push(worker());
|
|
1974
2967
|
}
|
|
1975
2968
|
await Promise.all(workers);
|
|
1976
|
-
await
|
|
2969
|
+
await shutdownPool3();
|
|
1977
2970
|
process.stderr.write("\n");
|
|
1978
2971
|
const summary = formatSummaryText(results, outputDir);
|
|
1979
2972
|
process.stderr.write(`${summary}
|
|
1980
2973
|
`);
|
|
1981
2974
|
} catch (err) {
|
|
1982
|
-
await
|
|
2975
|
+
await shutdownPool3();
|
|
1983
2976
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1984
2977
|
`);
|
|
1985
2978
|
process.exit(1);
|
|
@@ -2021,26 +3014,26 @@ function createRenderCommand() {
|
|
|
2021
3014
|
return renderCmd;
|
|
2022
3015
|
}
|
|
2023
3016
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2024
|
-
var
|
|
2025
|
-
async function
|
|
2026
|
-
if (
|
|
2027
|
-
|
|
3017
|
+
var _pool4 = null;
|
|
3018
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
3019
|
+
if (_pool4 === null) {
|
|
3020
|
+
_pool4 = new render.BrowserPool({
|
|
2028
3021
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2029
3022
|
viewportWidth,
|
|
2030
3023
|
viewportHeight
|
|
2031
3024
|
});
|
|
2032
|
-
await
|
|
3025
|
+
await _pool4.init();
|
|
2033
3026
|
}
|
|
2034
|
-
return
|
|
3027
|
+
return _pool4;
|
|
2035
3028
|
}
|
|
2036
|
-
async function
|
|
2037
|
-
if (
|
|
2038
|
-
await
|
|
2039
|
-
|
|
3029
|
+
async function shutdownPool4() {
|
|
3030
|
+
if (_pool4 !== null) {
|
|
3031
|
+
await _pool4.close();
|
|
3032
|
+
_pool4 = null;
|
|
2040
3033
|
}
|
|
2041
3034
|
}
|
|
2042
3035
|
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2043
|
-
const pool = await
|
|
3036
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2044
3037
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2045
3038
|
const slot = await pool.acquire();
|
|
2046
3039
|
const { page } = slot;
|
|
@@ -2173,12 +3166,12 @@ async function runBaseline(options = {}) {
|
|
|
2173
3166
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
2174
3167
|
let manifest$1;
|
|
2175
3168
|
if (manifestPath !== void 0) {
|
|
2176
|
-
const { readFileSync:
|
|
3169
|
+
const { readFileSync: readFileSync8 } = await import('fs');
|
|
2177
3170
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
2178
3171
|
if (!fs.existsSync(absPath)) {
|
|
2179
3172
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2180
3173
|
}
|
|
2181
|
-
manifest$1 = JSON.parse(
|
|
3174
|
+
manifest$1 = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2182
3175
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2183
3176
|
`);
|
|
2184
3177
|
} else {
|
|
@@ -2290,7 +3283,7 @@ async function runBaseline(options = {}) {
|
|
|
2290
3283
|
workers.push(worker());
|
|
2291
3284
|
}
|
|
2292
3285
|
await Promise.all(workers);
|
|
2293
|
-
await
|
|
3286
|
+
await shutdownPool4();
|
|
2294
3287
|
if (isTTY()) {
|
|
2295
3288
|
process.stderr.write("\n");
|
|
2296
3289
|
}
|
|
@@ -2341,10 +3334,10 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
2341
3334
|
}
|
|
2342
3335
|
|
|
2343
3336
|
// src/tree-formatter.ts
|
|
2344
|
-
var
|
|
2345
|
-
var
|
|
2346
|
-
var
|
|
2347
|
-
var
|
|
3337
|
+
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3338
|
+
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
3339
|
+
var VERTICAL2 = "\u2502 ";
|
|
3340
|
+
var EMPTY2 = " ";
|
|
2348
3341
|
function buildLabel(node, options) {
|
|
2349
3342
|
const parts = [node.name];
|
|
2350
3343
|
if (node.type === "memo") {
|
|
@@ -2391,19 +3384,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
|
|
|
2391
3384
|
}
|
|
2392
3385
|
return;
|
|
2393
3386
|
}
|
|
2394
|
-
const connector = isLast ?
|
|
3387
|
+
const connector = isLast ? LAST_BRANCH2 : BRANCH2;
|
|
2395
3388
|
const label = buildLabel(node, options);
|
|
2396
3389
|
lines.push(`${prefix}${connector}${label}`);
|
|
2397
3390
|
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
2398
3391
|
const childCount = countVisibleDescendants(node, options);
|
|
2399
3392
|
if (childCount > 0) {
|
|
2400
|
-
const nextPrefix2 = prefix + (isLast ?
|
|
2401
|
-
lines.push(`${nextPrefix2}${
|
|
3393
|
+
const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3394
|
+
lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
2402
3395
|
}
|
|
2403
3396
|
return;
|
|
2404
3397
|
}
|
|
2405
3398
|
const visibleChildren = getVisibleChildren(node, options);
|
|
2406
|
-
const nextPrefix = prefix + (isLast ?
|
|
3399
|
+
const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
2407
3400
|
for (let i = 0; i < visibleChildren.length; i++) {
|
|
2408
3401
|
const child = visibleChildren[i];
|
|
2409
3402
|
if (child !== void 0) {
|
|
@@ -2445,7 +3438,7 @@ function formatTree(root, options = {}) {
|
|
|
2445
3438
|
if (options.maxDepth === 0) {
|
|
2446
3439
|
const childCount = countVisibleDescendants(root, options);
|
|
2447
3440
|
if (childCount > 0) {
|
|
2448
|
-
lines.push(`${
|
|
3441
|
+
lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
2449
3442
|
}
|
|
2450
3443
|
} else {
|
|
2451
3444
|
const visibleChildren = getVisibleChildren(root, options);
|
|
@@ -2619,6 +3612,109 @@ function buildStructuredReport(report) {
|
|
|
2619
3612
|
}
|
|
2620
3613
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2621
3614
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3615
|
+
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3616
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3617
|
+
if (fileFlag !== void 0) {
|
|
3618
|
+
return path.resolve(process.cwd(), fileFlag);
|
|
3619
|
+
}
|
|
3620
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
3621
|
+
if (fs.existsSync(configPath)) {
|
|
3622
|
+
try {
|
|
3623
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
3624
|
+
const config = JSON.parse(raw);
|
|
3625
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3626
|
+
const file = config.tokens.file;
|
|
3627
|
+
return path.resolve(process.cwd(), file);
|
|
3628
|
+
}
|
|
3629
|
+
} catch {
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3633
|
+
}
|
|
3634
|
+
function createTokensExportCommand() {
|
|
3635
|
+
return new commander.Command("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
3636
|
+
"--theme <name>",
|
|
3637
|
+
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3638
|
+
).action(
|
|
3639
|
+
(opts) => {
|
|
3640
|
+
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
3641
|
+
process.stderr.write(
|
|
3642
|
+
`Error: unsupported format "${opts.format}".
|
|
3643
|
+
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
3644
|
+
`
|
|
3645
|
+
);
|
|
3646
|
+
process.exit(1);
|
|
3647
|
+
}
|
|
3648
|
+
const format = opts.format;
|
|
3649
|
+
try {
|
|
3650
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3651
|
+
if (!fs.existsSync(filePath)) {
|
|
3652
|
+
throw new Error(
|
|
3653
|
+
`Token file not found at ${filePath}.
|
|
3654
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
3658
|
+
const { tokens: tokens$1, rawFile } = tokens.parseTokenFileSync(raw);
|
|
3659
|
+
let themesMap;
|
|
3660
|
+
if (opts.theme !== void 0) {
|
|
3661
|
+
if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
|
|
3662
|
+
const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
|
|
3663
|
+
throw new Error(
|
|
3664
|
+
`Theme "${opts.theme}" not found in token file.
|
|
3665
|
+
Available themes: ${available}`
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
const baseResolver = new tokens.TokenResolver(tokens$1);
|
|
3669
|
+
const themeResolver = tokens.ThemeResolver.fromTokenFile(
|
|
3670
|
+
baseResolver,
|
|
3671
|
+
rawFile
|
|
3672
|
+
);
|
|
3673
|
+
const themeNames = themeResolver.listThemes();
|
|
3674
|
+
if (!themeNames.includes(opts.theme)) {
|
|
3675
|
+
throw new Error(
|
|
3676
|
+
`Theme "${opts.theme}" could not be resolved.
|
|
3677
|
+
Available themes: ${themeNames.join(", ")}`
|
|
3678
|
+
);
|
|
3679
|
+
}
|
|
3680
|
+
const themedTokens = themeResolver.buildThemedTokens(opts.theme);
|
|
3681
|
+
const overrideMap = /* @__PURE__ */ new Map();
|
|
3682
|
+
for (const themedToken of themedTokens) {
|
|
3683
|
+
const baseToken = tokens$1.find((t) => t.path === themedToken.path);
|
|
3684
|
+
if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
|
|
3685
|
+
overrideMap.set(themedToken.path, themedToken.resolvedValue);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
|
|
3689
|
+
}
|
|
3690
|
+
const output = tokens.exportTokens(tokens$1, format, {
|
|
3691
|
+
prefix: opts.prefix,
|
|
3692
|
+
rootSelector: opts.selector,
|
|
3693
|
+
themes: themesMap
|
|
3694
|
+
});
|
|
3695
|
+
if (opts.out !== void 0) {
|
|
3696
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
3697
|
+
fs.writeFileSync(outPath, output, "utf-8");
|
|
3698
|
+
process.stderr.write(`Exported ${tokens$1.length} tokens to ${outPath}
|
|
3699
|
+
`);
|
|
3700
|
+
} else {
|
|
3701
|
+
process.stdout.write(output);
|
|
3702
|
+
if (!output.endsWith("\n")) {
|
|
3703
|
+
process.stdout.write("\n");
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
} catch (err) {
|
|
3707
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3708
|
+
`);
|
|
3709
|
+
process.exit(1);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// src/tokens/commands.ts
|
|
3716
|
+
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3717
|
+
var CONFIG_FILE2 = "reactscope.config.json";
|
|
2622
3718
|
function isTTY2() {
|
|
2623
3719
|
return process.stdout.isTTY === true;
|
|
2624
3720
|
}
|
|
@@ -2636,11 +3732,11 @@ function buildTable2(headers, rows) {
|
|
|
2636
3732
|
);
|
|
2637
3733
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2638
3734
|
}
|
|
2639
|
-
function
|
|
3735
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2640
3736
|
if (fileFlag !== void 0) {
|
|
2641
3737
|
return path.resolve(process.cwd(), fileFlag);
|
|
2642
3738
|
}
|
|
2643
|
-
const configPath = path.resolve(process.cwd(),
|
|
3739
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE2);
|
|
2644
3740
|
if (fs.existsSync(configPath)) {
|
|
2645
3741
|
try {
|
|
2646
3742
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -2652,7 +3748,7 @@ function resolveTokenFilePath(fileFlag) {
|
|
|
2652
3748
|
} catch {
|
|
2653
3749
|
}
|
|
2654
3750
|
}
|
|
2655
|
-
return path.resolve(process.cwd(),
|
|
3751
|
+
return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2656
3752
|
}
|
|
2657
3753
|
function loadTokens(absPath) {
|
|
2658
3754
|
if (!fs.existsSync(absPath)) {
|
|
@@ -2699,7 +3795,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2699
3795
|
function registerGet2(tokensCmd) {
|
|
2700
3796
|
tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2701
3797
|
try {
|
|
2702
|
-
const filePath =
|
|
3798
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2703
3799
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2704
3800
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2705
3801
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -2725,7 +3821,7 @@ function registerList2(tokensCmd) {
|
|
|
2725
3821
|
tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2726
3822
|
(category, opts) => {
|
|
2727
3823
|
try {
|
|
2728
|
-
const filePath =
|
|
3824
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2729
3825
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2730
3826
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2731
3827
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -2755,7 +3851,7 @@ function registerSearch(tokensCmd) {
|
|
|
2755
3851
|
tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2756
3852
|
(value, opts) => {
|
|
2757
3853
|
try {
|
|
2758
|
-
const filePath =
|
|
3854
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2759
3855
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2760
3856
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2761
3857
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -2837,7 +3933,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2837
3933
|
function registerResolve(tokensCmd) {
|
|
2838
3934
|
tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2839
3935
|
try {
|
|
2840
|
-
const filePath =
|
|
3936
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2841
3937
|
const absFilePath = filePath;
|
|
2842
3938
|
const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
|
|
2843
3939
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
@@ -2874,7 +3970,7 @@ function registerValidate(tokensCmd) {
|
|
|
2874
3970
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2875
3971
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2876
3972
|
try {
|
|
2877
|
-
const filePath =
|
|
3973
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2878
3974
|
if (!fs.existsSync(filePath)) {
|
|
2879
3975
|
throw new Error(
|
|
2880
3976
|
`Token file not found at ${filePath}.
|
|
@@ -2957,6 +4053,7 @@ function createTokensCommand() {
|
|
|
2957
4053
|
registerSearch(tokensCmd);
|
|
2958
4054
|
registerResolve(tokensCmd);
|
|
2959
4055
|
registerValidate(tokensCmd);
|
|
4056
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
2960
4057
|
return tokensCmd;
|
|
2961
4058
|
}
|
|
2962
4059
|
|
|
@@ -3057,11 +4154,14 @@ function createProgram(options = {}) {
|
|
|
3057
4154
|
}
|
|
3058
4155
|
|
|
3059
4156
|
exports.createInitCommand = createInitCommand;
|
|
4157
|
+
exports.createInstrumentCommand = createInstrumentCommand;
|
|
3060
4158
|
exports.createManifestCommand = createManifestCommand;
|
|
3061
4159
|
exports.createProgram = createProgram;
|
|
3062
4160
|
exports.createTokensCommand = createTokensCommand;
|
|
4161
|
+
exports.createTokensExportCommand = createTokensExportCommand;
|
|
3063
4162
|
exports.isTTY = isTTY;
|
|
3064
4163
|
exports.matchGlob = matchGlob;
|
|
4164
|
+
exports.resolveTokenFilePath = resolveTokenFilePath;
|
|
3065
4165
|
exports.runInit = runInit;
|
|
3066
4166
|
//# sourceMappingURL=index.cjs.map
|
|
3067
4167
|
//# sourceMappingURL=index.cjs.map
|