@agent-scope/cli 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +847 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +961 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -1
- package/dist/index.d.ts +264 -1
- package/dist/index.js +960 -219
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/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((resolve10) => {
|
|
232
232
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
233
|
+
resolve10(answer.trim());
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -377,6 +377,114 @@ function createInitCommand() {
|
|
|
377
377
|
}
|
|
378
378
|
});
|
|
379
379
|
}
|
|
380
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
381
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
382
|
+
return wrapInHtml(bundledScript, viewportWidth);
|
|
383
|
+
}
|
|
384
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
385
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
386
|
+
const wrapperCode = (
|
|
387
|
+
/* ts */
|
|
388
|
+
`
|
|
389
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
390
|
+
import { createRoot } from "react-dom/client";
|
|
391
|
+
import { createElement } from "react";
|
|
392
|
+
|
|
393
|
+
(function scopeRenderHarness() {
|
|
394
|
+
var Component =
|
|
395
|
+
__scopeMod["default"] ||
|
|
396
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
397
|
+
(Object.values(__scopeMod).find(
|
|
398
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
399
|
+
));
|
|
400
|
+
|
|
401
|
+
if (!Component) {
|
|
402
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
403
|
+
"No renderable component found. Checked: default, " +
|
|
404
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
405
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
406
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
var props = ${propsJson};
|
|
412
|
+
var rootEl = document.getElementById("scope-root");
|
|
413
|
+
if (!rootEl) {
|
|
414
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
415
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
419
|
+
// Use requestAnimationFrame to let React flush the render
|
|
420
|
+
requestAnimationFrame(function() {
|
|
421
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
422
|
+
});
|
|
423
|
+
} catch (err) {
|
|
424
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
425
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
428
|
+
`
|
|
429
|
+
);
|
|
430
|
+
const result = await esbuild__namespace.build({
|
|
431
|
+
stdin: {
|
|
432
|
+
contents: wrapperCode,
|
|
433
|
+
// Resolve relative imports (within the component's dir)
|
|
434
|
+
resolveDir: path.dirname(filePath),
|
|
435
|
+
loader: "tsx",
|
|
436
|
+
sourcefile: "__scope_harness__.tsx"
|
|
437
|
+
},
|
|
438
|
+
bundle: true,
|
|
439
|
+
format: "iife",
|
|
440
|
+
write: false,
|
|
441
|
+
platform: "browser",
|
|
442
|
+
jsx: "automatic",
|
|
443
|
+
jsxImportSource: "react",
|
|
444
|
+
target: "es2020",
|
|
445
|
+
// Bundle everything — no externals
|
|
446
|
+
external: [],
|
|
447
|
+
define: {
|
|
448
|
+
"process.env.NODE_ENV": '"development"',
|
|
449
|
+
global: "globalThis"
|
|
450
|
+
},
|
|
451
|
+
logLevel: "silent",
|
|
452
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
453
|
+
banner: {
|
|
454
|
+
js: "/* @agent-scope/cli component harness */"
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
if (result.errors.length > 0) {
|
|
458
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
459
|
+
throw new Error(`esbuild failed to bundle component:
|
|
460
|
+
${msg}`);
|
|
461
|
+
}
|
|
462
|
+
const outputFile = result.outputFiles?.[0];
|
|
463
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
464
|
+
throw new Error("esbuild produced no output");
|
|
465
|
+
}
|
|
466
|
+
return outputFile.text;
|
|
467
|
+
}
|
|
468
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
469
|
+
const projectStyleBlock = "";
|
|
470
|
+
return `<!DOCTYPE html>
|
|
471
|
+
<html lang="en">
|
|
472
|
+
<head>
|
|
473
|
+
<meta charset="UTF-8" />
|
|
474
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
475
|
+
<style>
|
|
476
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
477
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
478
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
479
|
+
</style>
|
|
480
|
+
${projectStyleBlock}
|
|
481
|
+
</head>
|
|
482
|
+
<body>
|
|
483
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
484
|
+
<script>${bundledScript}</script>
|
|
485
|
+
</body>
|
|
486
|
+
</html>`;
|
|
487
|
+
}
|
|
380
488
|
|
|
381
489
|
// src/manifest-formatter.ts
|
|
382
490
|
function isTTY() {
|
|
@@ -644,154 +752,6 @@ function createManifestCommand() {
|
|
|
644
752
|
registerGenerate(manifestCmd);
|
|
645
753
|
return manifestCmd;
|
|
646
754
|
}
|
|
647
|
-
async function browserCapture(options) {
|
|
648
|
-
const { url, timeout = 1e4, wait = 0 } = options;
|
|
649
|
-
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
650
|
-
try {
|
|
651
|
-
const context = await browser.newContext();
|
|
652
|
-
const page = await context.newPage();
|
|
653
|
-
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
654
|
-
await page.goto(url, {
|
|
655
|
-
waitUntil: "networkidle",
|
|
656
|
-
timeout: timeout + 5e3
|
|
657
|
-
});
|
|
658
|
-
await page.waitForFunction(
|
|
659
|
-
() => {
|
|
660
|
-
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
661
|
-
},
|
|
662
|
-
{ timeout }
|
|
663
|
-
);
|
|
664
|
-
if (wait > 0) {
|
|
665
|
-
await page.waitForTimeout(wait);
|
|
666
|
-
}
|
|
667
|
-
const raw = await page.evaluate(async () => {
|
|
668
|
-
const win = window;
|
|
669
|
-
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
670
|
-
throw new Error("Scope runtime not injected");
|
|
671
|
-
}
|
|
672
|
-
return win.__SCOPE_CAPTURE__();
|
|
673
|
-
});
|
|
674
|
-
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
675
|
-
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
676
|
-
}
|
|
677
|
-
const report = { ...raw, route: null };
|
|
678
|
-
return { report };
|
|
679
|
-
} finally {
|
|
680
|
-
await browser.close();
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function writeReportToFile(report, outputPath, pretty) {
|
|
684
|
-
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
685
|
-
fs.writeFileSync(outputPath, json, "utf-8");
|
|
686
|
-
}
|
|
687
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
688
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
689
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
690
|
-
}
|
|
691
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
692
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
693
|
-
const wrapperCode = (
|
|
694
|
-
/* ts */
|
|
695
|
-
`
|
|
696
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
697
|
-
import { createRoot } from "react-dom/client";
|
|
698
|
-
import { createElement } from "react";
|
|
699
|
-
|
|
700
|
-
(function scopeRenderHarness() {
|
|
701
|
-
var Component =
|
|
702
|
-
__scopeMod["default"] ||
|
|
703
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
704
|
-
(Object.values(__scopeMod).find(
|
|
705
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
706
|
-
));
|
|
707
|
-
|
|
708
|
-
if (!Component) {
|
|
709
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
710
|
-
"No renderable component found. Checked: default, " +
|
|
711
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
712
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
713
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
var props = ${propsJson};
|
|
719
|
-
var rootEl = document.getElementById("scope-root");
|
|
720
|
-
if (!rootEl) {
|
|
721
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
722
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
726
|
-
// Use requestAnimationFrame to let React flush the render
|
|
727
|
-
requestAnimationFrame(function() {
|
|
728
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
729
|
-
});
|
|
730
|
-
} catch (err) {
|
|
731
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
732
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
733
|
-
}
|
|
734
|
-
})();
|
|
735
|
-
`
|
|
736
|
-
);
|
|
737
|
-
const result = await esbuild__namespace.build({
|
|
738
|
-
stdin: {
|
|
739
|
-
contents: wrapperCode,
|
|
740
|
-
// Resolve relative imports (within the component's dir)
|
|
741
|
-
resolveDir: path.dirname(filePath),
|
|
742
|
-
loader: "tsx",
|
|
743
|
-
sourcefile: "__scope_harness__.tsx"
|
|
744
|
-
},
|
|
745
|
-
bundle: true,
|
|
746
|
-
format: "iife",
|
|
747
|
-
write: false,
|
|
748
|
-
platform: "browser",
|
|
749
|
-
jsx: "automatic",
|
|
750
|
-
jsxImportSource: "react",
|
|
751
|
-
target: "es2020",
|
|
752
|
-
// Bundle everything — no externals
|
|
753
|
-
external: [],
|
|
754
|
-
define: {
|
|
755
|
-
"process.env.NODE_ENV": '"development"',
|
|
756
|
-
global: "globalThis"
|
|
757
|
-
},
|
|
758
|
-
logLevel: "silent",
|
|
759
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
760
|
-
banner: {
|
|
761
|
-
js: "/* @agent-scope/cli component harness */"
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
if (result.errors.length > 0) {
|
|
765
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
766
|
-
throw new Error(`esbuild failed to bundle component:
|
|
767
|
-
${msg}`);
|
|
768
|
-
}
|
|
769
|
-
const outputFile = result.outputFiles?.[0];
|
|
770
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
771
|
-
throw new Error("esbuild produced no output");
|
|
772
|
-
}
|
|
773
|
-
return outputFile.text;
|
|
774
|
-
}
|
|
775
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
776
|
-
const projectStyleBlock = "";
|
|
777
|
-
return `<!DOCTYPE html>
|
|
778
|
-
<html lang="en">
|
|
779
|
-
<head>
|
|
780
|
-
<meta charset="UTF-8" />
|
|
781
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
782
|
-
<style>
|
|
783
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
784
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
785
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
786
|
-
</style>
|
|
787
|
-
${projectStyleBlock}
|
|
788
|
-
</head>
|
|
789
|
-
<body>
|
|
790
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
791
|
-
<script>${bundledScript}</script>
|
|
792
|
-
</body>
|
|
793
|
-
</html>`;
|
|
794
|
-
}
|
|
795
755
|
|
|
796
756
|
// src/render-formatter.ts
|
|
797
757
|
function parseViewport(spec) {
|
|
@@ -898,63 +858,698 @@ function formatMatrixCsv(componentName, result) {
|
|
|
898
858
|
const val = axis.values[axisIdx];
|
|
899
859
|
return val !== void 0 ? csvEscape(String(val)) : "";
|
|
900
860
|
});
|
|
901
|
-
return [
|
|
902
|
-
csvEscape(componentName),
|
|
903
|
-
...axisVals,
|
|
904
|
-
cell.result.renderTimeMs.toFixed(3),
|
|
905
|
-
String(cell.result.width),
|
|
906
|
-
String(cell.result.height)
|
|
907
|
-
].join(",");
|
|
908
|
-
});
|
|
909
|
-
return `${[headers.join(","), ...rows].join("\n")}
|
|
861
|
+
return [
|
|
862
|
+
csvEscape(componentName),
|
|
863
|
+
...axisVals,
|
|
864
|
+
cell.result.renderTimeMs.toFixed(3),
|
|
865
|
+
String(cell.result.width),
|
|
866
|
+
String(cell.result.height)
|
|
867
|
+
].join(",");
|
|
868
|
+
});
|
|
869
|
+
return `${[headers.join(","), ...rows].join("\n")}
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
872
|
+
function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
|
|
873
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
874
|
+
const empty = barWidth - filled;
|
|
875
|
+
const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
|
|
876
|
+
const nameSlice = currentName.slice(0, 25).padEnd(25);
|
|
877
|
+
return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
|
|
878
|
+
}
|
|
879
|
+
function formatSummaryText(results, outputDir) {
|
|
880
|
+
const total = results.length;
|
|
881
|
+
const passed = results.filter((r) => r.success).length;
|
|
882
|
+
const failed = total - passed;
|
|
883
|
+
const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
|
|
884
|
+
const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
|
|
885
|
+
const lines = [
|
|
886
|
+
"\u2500".repeat(60),
|
|
887
|
+
`Render Summary`,
|
|
888
|
+
"\u2500".repeat(60),
|
|
889
|
+
` Total components : ${total}`,
|
|
890
|
+
` Passed : ${passed}`,
|
|
891
|
+
` Failed : ${failed}`,
|
|
892
|
+
` Avg render time : ${avgMs.toFixed(1)}ms`,
|
|
893
|
+
` Output dir : ${outputDir}`
|
|
894
|
+
];
|
|
895
|
+
if (failed > 0) {
|
|
896
|
+
lines.push("", " Failed components:");
|
|
897
|
+
for (const r of results) {
|
|
898
|
+
if (!r.success) {
|
|
899
|
+
lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
lines.push("\u2500".repeat(60));
|
|
904
|
+
return lines.join("\n");
|
|
905
|
+
}
|
|
906
|
+
function escapeHtml(str) {
|
|
907
|
+
return str.replace(/&/g, "&").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
|
+
})();
|
|
910
1337
|
`;
|
|
911
1338
|
}
|
|
912
|
-
function
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1339
|
+
function buildProfilingCollectScript() {
|
|
1340
|
+
return `
|
|
1341
|
+
(function __scopeProfileCollect() {
|
|
1342
|
+
var data = window.__scopeProfileData;
|
|
1343
|
+
if (!data) return { error: "Profiling not set up" };
|
|
1344
|
+
|
|
1345
|
+
// Estimate wasted renders: use paint entries count as a heuristic for
|
|
1346
|
+
// "components that re-rendered but their subtree output was likely unchanged".
|
|
1347
|
+
// A more accurate method would require React's own wasted-render detection,
|
|
1348
|
+
// which requires React Profiler API. We use a conservative estimate here.
|
|
1349
|
+
var totalCommits = data.commitCount;
|
|
1350
|
+
var uniqueNames = Array.from(data.componentNames);
|
|
1351
|
+
|
|
1352
|
+
// Wasted renders heuristic: if a component is in a subsequent commit (not the initial
|
|
1353
|
+
// mount commit), it *may* have been wasted if it didn't actually need to re-render.
|
|
1354
|
+
// For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
|
|
1355
|
+
// This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
|
|
1356
|
+
var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
|
|
1357
|
+
|
|
1358
|
+
return {
|
|
1359
|
+
commitCount: totalCommits,
|
|
1360
|
+
uniqueComponents: uniqueNames.length,
|
|
1361
|
+
componentNames: uniqueNames,
|
|
1362
|
+
wastedRenders: wastedRenders,
|
|
1363
|
+
layoutTime: window.__scopeLayoutTime || 0,
|
|
1364
|
+
paintTime: window.__scopePaintTime || 0,
|
|
1365
|
+
layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
|
|
1366
|
+
};
|
|
1367
|
+
})();
|
|
1368
|
+
`;
|
|
918
1369
|
}
|
|
919
|
-
function
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1370
|
+
async function replayInteraction(page, steps) {
|
|
1371
|
+
for (const step of steps) {
|
|
1372
|
+
switch (step.action) {
|
|
1373
|
+
case "click":
|
|
1374
|
+
if (step.target) {
|
|
1375
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1376
|
+
process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
|
|
1377
|
+
`);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
case "fill":
|
|
1382
|
+
if (step.target && step.value !== void 0) {
|
|
1383
|
+
await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
|
|
1384
|
+
process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
|
|
1385
|
+
`);
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
break;
|
|
1389
|
+
case "hover":
|
|
1390
|
+
if (step.target) {
|
|
1391
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1392
|
+
process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
|
|
1393
|
+
`);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
break;
|
|
1397
|
+
case "press":
|
|
1398
|
+
if (step.value) {
|
|
1399
|
+
await page.keyboard.press(step.value);
|
|
1400
|
+
}
|
|
1401
|
+
break;
|
|
1402
|
+
case "wait":
|
|
1403
|
+
await page.waitForTimeout(step.delay ?? 500);
|
|
1404
|
+
break;
|
|
941
1405
|
}
|
|
942
1406
|
}
|
|
943
|
-
lines.push("\u2500".repeat(60));
|
|
944
|
-
return lines.join("\n");
|
|
945
1407
|
}
|
|
946
|
-
function
|
|
947
|
-
|
|
1408
|
+
function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
|
|
1409
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1410
|
+
if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
|
|
1411
|
+
flags.add("WASTED_RENDER");
|
|
1412
|
+
}
|
|
1413
|
+
if (totalRenders > 10) {
|
|
1414
|
+
flags.add("HIGH_RENDER_COUNT");
|
|
1415
|
+
}
|
|
1416
|
+
if (layoutShifts.cumulativeScore > 0.1) {
|
|
1417
|
+
flags.add("LAYOUT_SHIFT_DETECTED");
|
|
1418
|
+
}
|
|
1419
|
+
if (timing.js > 100) {
|
|
1420
|
+
flags.add("SLOW_INTERACTION");
|
|
1421
|
+
}
|
|
1422
|
+
return [...flags];
|
|
948
1423
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
1424
|
+
async function runInteractionProfile(componentName, filePath, props, interaction) {
|
|
1425
|
+
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
1426
|
+
try {
|
|
1427
|
+
const context = await browser.newContext();
|
|
1428
|
+
const page = await context.newPage();
|
|
1429
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1430
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1431
|
+
await page.waitForFunction(
|
|
1432
|
+
() => {
|
|
1433
|
+
const w = window;
|
|
1434
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1435
|
+
},
|
|
1436
|
+
{ timeout: 15e3 }
|
|
1437
|
+
);
|
|
1438
|
+
const renderError = await page.evaluate(() => {
|
|
1439
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1440
|
+
});
|
|
1441
|
+
if (renderError !== null) {
|
|
1442
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1443
|
+
}
|
|
1444
|
+
const setupResult = await page.evaluate(buildProfilingSetupScript());
|
|
1445
|
+
const setupData = setupResult;
|
|
1446
|
+
if (setupData.error) {
|
|
1447
|
+
throw new Error(`Profiling setup failed: ${setupData.error}`);
|
|
1448
|
+
}
|
|
1449
|
+
const jsStart = Date.now();
|
|
1450
|
+
if (interaction.length > 0) {
|
|
1451
|
+
process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
|
|
1452
|
+
`);
|
|
1453
|
+
await replayInteraction(page, interaction);
|
|
1454
|
+
await page.waitForTimeout(300);
|
|
1455
|
+
}
|
|
1456
|
+
const jsDuration = Date.now() - jsStart;
|
|
1457
|
+
const collected = await page.evaluate(buildProfilingCollectScript());
|
|
1458
|
+
const profileData = collected;
|
|
1459
|
+
if (profileData.error) {
|
|
1460
|
+
throw new Error(`Profile collection failed: ${profileData.error}`);
|
|
1461
|
+
}
|
|
1462
|
+
const timing = {
|
|
1463
|
+
js: jsDuration,
|
|
1464
|
+
layout: profileData.layoutTime ?? 0,
|
|
1465
|
+
paint: profileData.paintTime ?? 0
|
|
1466
|
+
};
|
|
1467
|
+
const layoutShifts = {
|
|
1468
|
+
count: profileData.layoutShifts?.count ?? 0,
|
|
1469
|
+
cumulativeScore: profileData.layoutShifts?.score ?? 0
|
|
1470
|
+
};
|
|
1471
|
+
const totalRenders = profileData.commitCount ?? 0;
|
|
1472
|
+
const uniqueComponents = profileData.uniqueComponents ?? 0;
|
|
1473
|
+
const wastedRenders = profileData.wastedRenders ?? 0;
|
|
1474
|
+
const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
|
|
1475
|
+
return {
|
|
1476
|
+
component: componentName,
|
|
1477
|
+
totalRenders,
|
|
1478
|
+
uniqueComponents,
|
|
1479
|
+
wastedRenders,
|
|
1480
|
+
timing,
|
|
1481
|
+
layoutShifts,
|
|
1482
|
+
flags,
|
|
1483
|
+
interaction
|
|
1484
|
+
};
|
|
1485
|
+
} finally {
|
|
1486
|
+
await browser.close();
|
|
952
1487
|
}
|
|
953
|
-
|
|
1488
|
+
}
|
|
1489
|
+
function createInstrumentProfileCommand() {
|
|
1490
|
+
const cmd = new commander.Command("profile").description(
|
|
1491
|
+
"Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
|
|
1492
|
+
).argument("<component>", "Component name (must exist in the manifest)").option(
|
|
1493
|
+
"--interaction <json>",
|
|
1494
|
+
`Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
|
|
1495
|
+
"[]"
|
|
1496
|
+
).option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|text (default: json)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
|
|
1497
|
+
async (componentName, opts) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const manifest = loadManifest(opts.manifest);
|
|
1500
|
+
const descriptor = manifest.components[componentName];
|
|
1501
|
+
if (descriptor === void 0) {
|
|
1502
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
`Component "${componentName}" not found in manifest.
|
|
1505
|
+
Available: ${available}`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
let props = {};
|
|
1509
|
+
try {
|
|
1510
|
+
props = JSON.parse(opts.props);
|
|
1511
|
+
} catch {
|
|
1512
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1513
|
+
}
|
|
1514
|
+
let interaction = [];
|
|
1515
|
+
try {
|
|
1516
|
+
interaction = JSON.parse(opts.interaction);
|
|
1517
|
+
if (!Array.isArray(interaction)) {
|
|
1518
|
+
throw new Error("Interaction must be a JSON array");
|
|
1519
|
+
}
|
|
1520
|
+
} catch {
|
|
1521
|
+
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
1522
|
+
}
|
|
1523
|
+
const rootDir = process.cwd();
|
|
1524
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1525
|
+
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
1526
|
+
`);
|
|
1527
|
+
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
1528
|
+
if (opts.showFlags) {
|
|
1529
|
+
if (result.flags.length === 0) {
|
|
1530
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1531
|
+
} else {
|
|
1532
|
+
for (const flag of result.flags) {
|
|
1533
|
+
process.stdout.write(`${flag}
|
|
1534
|
+
`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1540
|
+
`);
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1543
|
+
`);
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1548
|
+
return cmd;
|
|
954
1549
|
}
|
|
955
1550
|
|
|
956
1551
|
// src/instrument/renders.ts
|
|
957
|
-
var
|
|
1552
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
958
1553
|
function determineTrigger(event) {
|
|
959
1554
|
if (event.forceUpdate) return "force_update";
|
|
960
1555
|
if (event.stateChanged) return "state_change";
|
|
@@ -1199,7 +1794,7 @@ function buildInstrumentationScript() {
|
|
|
1199
1794
|
`
|
|
1200
1795
|
);
|
|
1201
1796
|
}
|
|
1202
|
-
async function
|
|
1797
|
+
async function replayInteraction2(page, steps) {
|
|
1203
1798
|
for (const step of steps) {
|
|
1204
1799
|
switch (step.action) {
|
|
1205
1800
|
case "click":
|
|
@@ -1272,7 +1867,7 @@ async function shutdownPool() {
|
|
|
1272
1867
|
}
|
|
1273
1868
|
}
|
|
1274
1869
|
async function analyzeRenders(options) {
|
|
1275
|
-
const manifestPath = options.manifestPath ??
|
|
1870
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
|
|
1276
1871
|
const manifest = loadManifest(manifestPath);
|
|
1277
1872
|
const descriptor = manifest.components[options.componentName];
|
|
1278
1873
|
if (descriptor === void 0) {
|
|
@@ -1301,7 +1896,7 @@ Available: ${available}`
|
|
|
1301
1896
|
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1302
1897
|
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1303
1898
|
});
|
|
1304
|
-
await
|
|
1899
|
+
await replayInteraction2(page, options.interaction);
|
|
1305
1900
|
await page.waitForTimeout(200);
|
|
1306
1901
|
const interactionDurationMs = performance.now() - startMs;
|
|
1307
1902
|
const rawEvents = await page.evaluate(() => {
|
|
@@ -1370,7 +1965,7 @@ function createInstrumentRendersCommand() {
|
|
|
1370
1965
|
"--interaction <json>",
|
|
1371
1966
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1372
1967
|
"[]"
|
|
1373
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
1968
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
|
|
1374
1969
|
async (componentName, opts) => {
|
|
1375
1970
|
let interaction = [];
|
|
1376
1971
|
try {
|
|
@@ -1415,8 +2010,50 @@ function createInstrumentCommand() {
|
|
|
1415
2010
|
"Structured instrumentation commands for React component analysis"
|
|
1416
2011
|
);
|
|
1417
2012
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2013
|
+
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2014
|
+
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
1418
2015
|
return instrumentCmd;
|
|
1419
2016
|
}
|
|
2017
|
+
async function browserCapture(options) {
|
|
2018
|
+
const { url, timeout = 1e4, wait = 0 } = options;
|
|
2019
|
+
const browser = await playwright$1.chromium.launch({ headless: true });
|
|
2020
|
+
try {
|
|
2021
|
+
const context = await browser.newContext();
|
|
2022
|
+
const page = await context.newPage();
|
|
2023
|
+
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
2024
|
+
await page.goto(url, {
|
|
2025
|
+
waitUntil: "networkidle",
|
|
2026
|
+
timeout: timeout + 5e3
|
|
2027
|
+
});
|
|
2028
|
+
await page.waitForFunction(
|
|
2029
|
+
() => {
|
|
2030
|
+
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
2031
|
+
},
|
|
2032
|
+
{ timeout }
|
|
2033
|
+
);
|
|
2034
|
+
if (wait > 0) {
|
|
2035
|
+
await page.waitForTimeout(wait);
|
|
2036
|
+
}
|
|
2037
|
+
const raw = await page.evaluate(async () => {
|
|
2038
|
+
const win = window;
|
|
2039
|
+
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
2040
|
+
throw new Error("Scope runtime not injected");
|
|
2041
|
+
}
|
|
2042
|
+
return win.__SCOPE_CAPTURE__();
|
|
2043
|
+
});
|
|
2044
|
+
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
2045
|
+
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
2046
|
+
}
|
|
2047
|
+
const report = { ...raw, route: null };
|
|
2048
|
+
return { report };
|
|
2049
|
+
} finally {
|
|
2050
|
+
await browser.close();
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
2054
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2055
|
+
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2056
|
+
}
|
|
1420
2057
|
var CONFIG_FILENAMES = [
|
|
1421
2058
|
".reactscope/config.json",
|
|
1422
2059
|
".reactscope/config.js",
|
|
@@ -1534,7 +2171,7 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
1534
2171
|
}
|
|
1535
2172
|
|
|
1536
2173
|
// src/render-commands.ts
|
|
1537
|
-
var
|
|
2174
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1538
2175
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
1539
2176
|
var _pool2 = null;
|
|
1540
2177
|
async function getPool2(viewportWidth, viewportHeight) {
|
|
@@ -1659,7 +2296,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1659
2296
|
};
|
|
1660
2297
|
}
|
|
1661
2298
|
function registerRenderSingle(renderCmd) {
|
|
1662
|
-
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json",
|
|
2299
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1663
2300
|
async (componentName, opts) => {
|
|
1664
2301
|
try {
|
|
1665
2302
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1758,7 +2395,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
1758
2395
|
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
|
|
1759
2396
|
"--contexts <ids>",
|
|
1760
2397
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
1761
|
-
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json",
|
|
2398
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1762
2399
|
async (componentName, opts) => {
|
|
1763
2400
|
try {
|
|
1764
2401
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1885,7 +2522,7 @@ Available: ${available}`
|
|
|
1885
2522
|
);
|
|
1886
2523
|
}
|
|
1887
2524
|
function registerRenderAll(renderCmd) {
|
|
1888
|
-
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json",
|
|
2525
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
1889
2526
|
async (opts) => {
|
|
1890
2527
|
try {
|
|
1891
2528
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2173,12 +2810,12 @@ async function runBaseline(options = {}) {
|
|
|
2173
2810
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
2174
2811
|
let manifest$1;
|
|
2175
2812
|
if (manifestPath !== void 0) {
|
|
2176
|
-
const { readFileSync:
|
|
2813
|
+
const { readFileSync: readFileSync8 } = await import('fs');
|
|
2177
2814
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
2178
2815
|
if (!fs.existsSync(absPath)) {
|
|
2179
2816
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2180
2817
|
}
|
|
2181
|
-
manifest$1 = JSON.parse(
|
|
2818
|
+
manifest$1 = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2182
2819
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2183
2820
|
`);
|
|
2184
2821
|
} else {
|
|
@@ -2619,6 +3256,109 @@ function buildStructuredReport(report) {
|
|
|
2619
3256
|
}
|
|
2620
3257
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2621
3258
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3259
|
+
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3260
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3261
|
+
if (fileFlag !== void 0) {
|
|
3262
|
+
return path.resolve(process.cwd(), fileFlag);
|
|
3263
|
+
}
|
|
3264
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
3265
|
+
if (fs.existsSync(configPath)) {
|
|
3266
|
+
try {
|
|
3267
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
3268
|
+
const config = JSON.parse(raw);
|
|
3269
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3270
|
+
const file = config.tokens.file;
|
|
3271
|
+
return path.resolve(process.cwd(), file);
|
|
3272
|
+
}
|
|
3273
|
+
} catch {
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3277
|
+
}
|
|
3278
|
+
function createTokensExportCommand() {
|
|
3279
|
+
return new commander.Command("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
3280
|
+
"--theme <name>",
|
|
3281
|
+
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3282
|
+
).action(
|
|
3283
|
+
(opts) => {
|
|
3284
|
+
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
3285
|
+
process.stderr.write(
|
|
3286
|
+
`Error: unsupported format "${opts.format}".
|
|
3287
|
+
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
3288
|
+
`
|
|
3289
|
+
);
|
|
3290
|
+
process.exit(1);
|
|
3291
|
+
}
|
|
3292
|
+
const format = opts.format;
|
|
3293
|
+
try {
|
|
3294
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3295
|
+
if (!fs.existsSync(filePath)) {
|
|
3296
|
+
throw new Error(
|
|
3297
|
+
`Token file not found at ${filePath}.
|
|
3298
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
3302
|
+
const { tokens: tokens$1, rawFile } = tokens.parseTokenFileSync(raw);
|
|
3303
|
+
let themesMap;
|
|
3304
|
+
if (opts.theme !== void 0) {
|
|
3305
|
+
if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
|
|
3306
|
+
const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
|
|
3307
|
+
throw new Error(
|
|
3308
|
+
`Theme "${opts.theme}" not found in token file.
|
|
3309
|
+
Available themes: ${available}`
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
const baseResolver = new tokens.TokenResolver(tokens$1);
|
|
3313
|
+
const themeResolver = tokens.ThemeResolver.fromTokenFile(
|
|
3314
|
+
baseResolver,
|
|
3315
|
+
rawFile
|
|
3316
|
+
);
|
|
3317
|
+
const themeNames = themeResolver.listThemes();
|
|
3318
|
+
if (!themeNames.includes(opts.theme)) {
|
|
3319
|
+
throw new Error(
|
|
3320
|
+
`Theme "${opts.theme}" could not be resolved.
|
|
3321
|
+
Available themes: ${themeNames.join(", ")}`
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
const themedTokens = themeResolver.buildThemedTokens(opts.theme);
|
|
3325
|
+
const overrideMap = /* @__PURE__ */ new Map();
|
|
3326
|
+
for (const themedToken of themedTokens) {
|
|
3327
|
+
const baseToken = tokens$1.find((t) => t.path === themedToken.path);
|
|
3328
|
+
if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
|
|
3329
|
+
overrideMap.set(themedToken.path, themedToken.resolvedValue);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
|
|
3333
|
+
}
|
|
3334
|
+
const output = tokens.exportTokens(tokens$1, format, {
|
|
3335
|
+
prefix: opts.prefix,
|
|
3336
|
+
rootSelector: opts.selector,
|
|
3337
|
+
themes: themesMap
|
|
3338
|
+
});
|
|
3339
|
+
if (opts.out !== void 0) {
|
|
3340
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
3341
|
+
fs.writeFileSync(outPath, output, "utf-8");
|
|
3342
|
+
process.stderr.write(`Exported ${tokens$1.length} tokens to ${outPath}
|
|
3343
|
+
`);
|
|
3344
|
+
} else {
|
|
3345
|
+
process.stdout.write(output);
|
|
3346
|
+
if (!output.endsWith("\n")) {
|
|
3347
|
+
process.stdout.write("\n");
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3352
|
+
`);
|
|
3353
|
+
process.exit(1);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// src/tokens/commands.ts
|
|
3360
|
+
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3361
|
+
var CONFIG_FILE2 = "reactscope.config.json";
|
|
2622
3362
|
function isTTY2() {
|
|
2623
3363
|
return process.stdout.isTTY === true;
|
|
2624
3364
|
}
|
|
@@ -2636,11 +3376,11 @@ function buildTable2(headers, rows) {
|
|
|
2636
3376
|
);
|
|
2637
3377
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2638
3378
|
}
|
|
2639
|
-
function
|
|
3379
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2640
3380
|
if (fileFlag !== void 0) {
|
|
2641
3381
|
return path.resolve(process.cwd(), fileFlag);
|
|
2642
3382
|
}
|
|
2643
|
-
const configPath = path.resolve(process.cwd(),
|
|
3383
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE2);
|
|
2644
3384
|
if (fs.existsSync(configPath)) {
|
|
2645
3385
|
try {
|
|
2646
3386
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -2652,7 +3392,7 @@ function resolveTokenFilePath(fileFlag) {
|
|
|
2652
3392
|
} catch {
|
|
2653
3393
|
}
|
|
2654
3394
|
}
|
|
2655
|
-
return path.resolve(process.cwd(),
|
|
3395
|
+
return path.resolve(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2656
3396
|
}
|
|
2657
3397
|
function loadTokens(absPath) {
|
|
2658
3398
|
if (!fs.existsSync(absPath)) {
|
|
@@ -2699,7 +3439,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2699
3439
|
function registerGet2(tokensCmd) {
|
|
2700
3440
|
tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2701
3441
|
try {
|
|
2702
|
-
const filePath =
|
|
3442
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2703
3443
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2704
3444
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2705
3445
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -2725,7 +3465,7 @@ function registerList2(tokensCmd) {
|
|
|
2725
3465
|
tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2726
3466
|
(category, opts) => {
|
|
2727
3467
|
try {
|
|
2728
|
-
const filePath =
|
|
3468
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2729
3469
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2730
3470
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2731
3471
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -2755,7 +3495,7 @@ function registerSearch(tokensCmd) {
|
|
|
2755
3495
|
tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
|
|
2756
3496
|
(value, opts) => {
|
|
2757
3497
|
try {
|
|
2758
|
-
const filePath =
|
|
3498
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2759
3499
|
const { tokens: tokens$1 } = loadTokens(filePath);
|
|
2760
3500
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
2761
3501
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -2837,7 +3577,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2837
3577
|
function registerResolve(tokensCmd) {
|
|
2838
3578
|
tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
|
|
2839
3579
|
try {
|
|
2840
|
-
const filePath =
|
|
3580
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2841
3581
|
const absFilePath = filePath;
|
|
2842
3582
|
const { tokens: tokens$1, rawFile } = loadTokens(absFilePath);
|
|
2843
3583
|
const resolver = new tokens.TokenResolver(tokens$1);
|
|
@@ -2874,7 +3614,7 @@ function registerValidate(tokensCmd) {
|
|
|
2874
3614
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2875
3615
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2876
3616
|
try {
|
|
2877
|
-
const filePath =
|
|
3617
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2878
3618
|
if (!fs.existsSync(filePath)) {
|
|
2879
3619
|
throw new Error(
|
|
2880
3620
|
`Token file not found at ${filePath}.
|
|
@@ -2957,6 +3697,7 @@ function createTokensCommand() {
|
|
|
2957
3697
|
registerSearch(tokensCmd);
|
|
2958
3698
|
registerResolve(tokensCmd);
|
|
2959
3699
|
registerValidate(tokensCmd);
|
|
3700
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
2960
3701
|
return tokensCmd;
|
|
2961
3702
|
}
|
|
2962
3703
|
|
|
@@ -3057,11 +3798,14 @@ function createProgram(options = {}) {
|
|
|
3057
3798
|
}
|
|
3058
3799
|
|
|
3059
3800
|
exports.createInitCommand = createInitCommand;
|
|
3801
|
+
exports.createInstrumentCommand = createInstrumentCommand;
|
|
3060
3802
|
exports.createManifestCommand = createManifestCommand;
|
|
3061
3803
|
exports.createProgram = createProgram;
|
|
3062
3804
|
exports.createTokensCommand = createTokensCommand;
|
|
3805
|
+
exports.createTokensExportCommand = createTokensExportCommand;
|
|
3063
3806
|
exports.isTTY = isTTY;
|
|
3064
3807
|
exports.matchGlob = matchGlob;
|
|
3808
|
+
exports.resolveTokenFilePath = resolveTokenFilePath;
|
|
3065
3809
|
exports.runInit = runInit;
|
|
3066
3810
|
//# sourceMappingURL=index.cjs.map
|
|
3067
3811
|
//# sourceMappingURL=index.cjs.map
|