@agent-scope/cli 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1164 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1245 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -1
- package/dist/index.d.ts +264 -1
- package/dist/index.js +1245 -181
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync, rmSync } from 'fs';
|
|
2
2
|
import { join, resolve, dirname } from 'path';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, BrowserPool, SatoriRenderer } from '@agent-scope/render';
|
|
6
|
+
import * as esbuild from 'esbuild';
|
|
5
7
|
import { generateManifest } from '@agent-scope/manifest';
|
|
6
|
-
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
7
8
|
import { chromium } from 'playwright';
|
|
8
|
-
import {
|
|
9
|
-
import * as esbuild from 'esbuild';
|
|
9
|
+
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
|
-
import { TokenResolver, validateTokenFile, TokenValidationError,
|
|
11
|
+
import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine } from '@agent-scope/tokens';
|
|
12
12
|
|
|
13
13
|
// src/init/index.ts
|
|
14
14
|
function hasConfigFile(dir, stem) {
|
|
@@ -205,9 +205,9 @@ function createRL() {
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
async function ask(rl, question) {
|
|
208
|
-
return new Promise((
|
|
208
|
+
return new Promise((resolve10) => {
|
|
209
209
|
rl.question(question, (answer) => {
|
|
210
|
-
|
|
210
|
+
resolve10(answer.trim());
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
}
|
|
@@ -354,6 +354,114 @@ function createInitCommand() {
|
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
358
|
+
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
359
|
+
return wrapInHtml(bundledScript, viewportWidth);
|
|
360
|
+
}
|
|
361
|
+
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
362
|
+
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
363
|
+
const wrapperCode = (
|
|
364
|
+
/* ts */
|
|
365
|
+
`
|
|
366
|
+
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
367
|
+
import { createRoot } from "react-dom/client";
|
|
368
|
+
import { createElement } from "react";
|
|
369
|
+
|
|
370
|
+
(function scopeRenderHarness() {
|
|
371
|
+
var Component =
|
|
372
|
+
__scopeMod["default"] ||
|
|
373
|
+
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
374
|
+
(Object.values(__scopeMod).find(
|
|
375
|
+
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
376
|
+
));
|
|
377
|
+
|
|
378
|
+
if (!Component) {
|
|
379
|
+
window.__SCOPE_RENDER_ERROR__ =
|
|
380
|
+
"No renderable component found. Checked: default, " +
|
|
381
|
+
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
382
|
+
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
383
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
var props = ${propsJson};
|
|
389
|
+
var rootEl = document.getElementById("scope-root");
|
|
390
|
+
if (!rootEl) {
|
|
391
|
+
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
392
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
createRoot(rootEl).render(createElement(Component, props));
|
|
396
|
+
// Use requestAnimationFrame to let React flush the render
|
|
397
|
+
requestAnimationFrame(function() {
|
|
398
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
399
|
+
});
|
|
400
|
+
} catch (err) {
|
|
401
|
+
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
402
|
+
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
403
|
+
}
|
|
404
|
+
})();
|
|
405
|
+
`
|
|
406
|
+
);
|
|
407
|
+
const result = await esbuild.build({
|
|
408
|
+
stdin: {
|
|
409
|
+
contents: wrapperCode,
|
|
410
|
+
// Resolve relative imports (within the component's dir)
|
|
411
|
+
resolveDir: dirname(filePath),
|
|
412
|
+
loader: "tsx",
|
|
413
|
+
sourcefile: "__scope_harness__.tsx"
|
|
414
|
+
},
|
|
415
|
+
bundle: true,
|
|
416
|
+
format: "iife",
|
|
417
|
+
write: false,
|
|
418
|
+
platform: "browser",
|
|
419
|
+
jsx: "automatic",
|
|
420
|
+
jsxImportSource: "react",
|
|
421
|
+
target: "es2020",
|
|
422
|
+
// Bundle everything — no externals
|
|
423
|
+
external: [],
|
|
424
|
+
define: {
|
|
425
|
+
"process.env.NODE_ENV": '"development"',
|
|
426
|
+
global: "globalThis"
|
|
427
|
+
},
|
|
428
|
+
logLevel: "silent",
|
|
429
|
+
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
430
|
+
banner: {
|
|
431
|
+
js: "/* @agent-scope/cli component harness */"
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (result.errors.length > 0) {
|
|
435
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
436
|
+
throw new Error(`esbuild failed to bundle component:
|
|
437
|
+
${msg}`);
|
|
438
|
+
}
|
|
439
|
+
const outputFile = result.outputFiles?.[0];
|
|
440
|
+
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
441
|
+
throw new Error("esbuild produced no output");
|
|
442
|
+
}
|
|
443
|
+
return outputFile.text;
|
|
444
|
+
}
|
|
445
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
446
|
+
const projectStyleBlock = "";
|
|
447
|
+
return `<!DOCTYPE html>
|
|
448
|
+
<html lang="en">
|
|
449
|
+
<head>
|
|
450
|
+
<meta charset="UTF-8" />
|
|
451
|
+
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
452
|
+
<style>
|
|
453
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
454
|
+
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
455
|
+
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
456
|
+
</style>
|
|
457
|
+
${projectStyleBlock}
|
|
458
|
+
</head>
|
|
459
|
+
<body>
|
|
460
|
+
<div id="scope-root" data-reactscope-root></div>
|
|
461
|
+
<script>${bundledScript}</script>
|
|
462
|
+
</body>
|
|
463
|
+
</html>`;
|
|
464
|
+
}
|
|
357
465
|
|
|
358
466
|
// src/manifest-formatter.ts
|
|
359
467
|
function isTTY() {
|
|
@@ -621,154 +729,6 @@ function createManifestCommand() {
|
|
|
621
729
|
registerGenerate(manifestCmd);
|
|
622
730
|
return manifestCmd;
|
|
623
731
|
}
|
|
624
|
-
async function browserCapture(options) {
|
|
625
|
-
const { url, timeout = 1e4, wait = 0 } = options;
|
|
626
|
-
const browser = await chromium.launch({ headless: true });
|
|
627
|
-
try {
|
|
628
|
-
const context = await browser.newContext();
|
|
629
|
-
const page = await context.newPage();
|
|
630
|
-
await page.addInitScript({ content: getBrowserEntryScript() });
|
|
631
|
-
await page.goto(url, {
|
|
632
|
-
waitUntil: "networkidle",
|
|
633
|
-
timeout: timeout + 5e3
|
|
634
|
-
});
|
|
635
|
-
await page.waitForFunction(
|
|
636
|
-
() => {
|
|
637
|
-
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
638
|
-
},
|
|
639
|
-
{ timeout }
|
|
640
|
-
);
|
|
641
|
-
if (wait > 0) {
|
|
642
|
-
await page.waitForTimeout(wait);
|
|
643
|
-
}
|
|
644
|
-
const raw = await page.evaluate(async () => {
|
|
645
|
-
const win = window;
|
|
646
|
-
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
647
|
-
throw new Error("Scope runtime not injected");
|
|
648
|
-
}
|
|
649
|
-
return win.__SCOPE_CAPTURE__();
|
|
650
|
-
});
|
|
651
|
-
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
652
|
-
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
653
|
-
}
|
|
654
|
-
const report = { ...raw, route: null };
|
|
655
|
-
return { report };
|
|
656
|
-
} finally {
|
|
657
|
-
await browser.close();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function writeReportToFile(report, outputPath, pretty) {
|
|
661
|
-
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
662
|
-
writeFileSync(outputPath, json, "utf-8");
|
|
663
|
-
}
|
|
664
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
|
|
665
|
-
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
666
|
-
return wrapInHtml(bundledScript, viewportWidth);
|
|
667
|
-
}
|
|
668
|
-
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
669
|
-
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
670
|
-
const wrapperCode = (
|
|
671
|
-
/* ts */
|
|
672
|
-
`
|
|
673
|
-
import * as __scopeMod from ${JSON.stringify(filePath)};
|
|
674
|
-
import { createRoot } from "react-dom/client";
|
|
675
|
-
import { createElement } from "react";
|
|
676
|
-
|
|
677
|
-
(function scopeRenderHarness() {
|
|
678
|
-
var Component =
|
|
679
|
-
__scopeMod["default"] ||
|
|
680
|
-
__scopeMod[${JSON.stringify(componentName)}] ||
|
|
681
|
-
(Object.values(__scopeMod).find(
|
|
682
|
-
function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
|
|
683
|
-
));
|
|
684
|
-
|
|
685
|
-
if (!Component) {
|
|
686
|
-
window.__SCOPE_RENDER_ERROR__ =
|
|
687
|
-
"No renderable component found. Checked: default, " +
|
|
688
|
-
${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
|
|
689
|
-
"Available exports: " + Object.keys(__scopeMod).join(", ");
|
|
690
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
var props = ${propsJson};
|
|
696
|
-
var rootEl = document.getElementById("scope-root");
|
|
697
|
-
if (!rootEl) {
|
|
698
|
-
window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
|
|
699
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
createRoot(rootEl).render(createElement(Component, props));
|
|
703
|
-
// Use requestAnimationFrame to let React flush the render
|
|
704
|
-
requestAnimationFrame(function() {
|
|
705
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
706
|
-
});
|
|
707
|
-
} catch (err) {
|
|
708
|
-
window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
|
|
709
|
-
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
710
|
-
}
|
|
711
|
-
})();
|
|
712
|
-
`
|
|
713
|
-
);
|
|
714
|
-
const result = await esbuild.build({
|
|
715
|
-
stdin: {
|
|
716
|
-
contents: wrapperCode,
|
|
717
|
-
// Resolve relative imports (within the component's dir)
|
|
718
|
-
resolveDir: dirname(filePath),
|
|
719
|
-
loader: "tsx",
|
|
720
|
-
sourcefile: "__scope_harness__.tsx"
|
|
721
|
-
},
|
|
722
|
-
bundle: true,
|
|
723
|
-
format: "iife",
|
|
724
|
-
write: false,
|
|
725
|
-
platform: "browser",
|
|
726
|
-
jsx: "automatic",
|
|
727
|
-
jsxImportSource: "react",
|
|
728
|
-
target: "es2020",
|
|
729
|
-
// Bundle everything — no externals
|
|
730
|
-
external: [],
|
|
731
|
-
define: {
|
|
732
|
-
"process.env.NODE_ENV": '"development"',
|
|
733
|
-
global: "globalThis"
|
|
734
|
-
},
|
|
735
|
-
logLevel: "silent",
|
|
736
|
-
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
737
|
-
banner: {
|
|
738
|
-
js: "/* @agent-scope/cli component harness */"
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
if (result.errors.length > 0) {
|
|
742
|
-
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
743
|
-
throw new Error(`esbuild failed to bundle component:
|
|
744
|
-
${msg}`);
|
|
745
|
-
}
|
|
746
|
-
const outputFile = result.outputFiles?.[0];
|
|
747
|
-
if (outputFile === void 0 || outputFile.text.length === 0) {
|
|
748
|
-
throw new Error("esbuild produced no output");
|
|
749
|
-
}
|
|
750
|
-
return outputFile.text;
|
|
751
|
-
}
|
|
752
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss) {
|
|
753
|
-
const projectStyleBlock = "";
|
|
754
|
-
return `<!DOCTYPE html>
|
|
755
|
-
<html lang="en">
|
|
756
|
-
<head>
|
|
757
|
-
<meta charset="UTF-8" />
|
|
758
|
-
<meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
|
|
759
|
-
<style>
|
|
760
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
761
|
-
html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
762
|
-
#scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
|
|
763
|
-
</style>
|
|
764
|
-
${projectStyleBlock}
|
|
765
|
-
</head>
|
|
766
|
-
<body>
|
|
767
|
-
<div id="scope-root" data-reactscope-root></div>
|
|
768
|
-
<script>${bundledScript}</script>
|
|
769
|
-
</body>
|
|
770
|
-
</html>`;
|
|
771
|
-
}
|
|
772
732
|
|
|
773
733
|
// src/render-formatter.ts
|
|
774
734
|
function parseViewport(spec) {
|
|
@@ -917,21 +877,656 @@ function formatSummaryText(results, outputDir) {
|
|
|
917
877
|
}
|
|
918
878
|
}
|
|
919
879
|
}
|
|
920
|
-
lines.push("\u2500".repeat(60));
|
|
921
|
-
return lines.join("\n");
|
|
880
|
+
lines.push("\u2500".repeat(60));
|
|
881
|
+
return lines.join("\n");
|
|
882
|
+
}
|
|
883
|
+
function escapeHtml(str) {
|
|
884
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
885
|
+
}
|
|
886
|
+
function csvEscape(value) {
|
|
887
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
888
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
889
|
+
}
|
|
890
|
+
return value;
|
|
891
|
+
}
|
|
892
|
+
var MANIFEST_PATH2 = ".reactscope/manifest.json";
|
|
893
|
+
function buildHookInstrumentationScript() {
|
|
894
|
+
return `
|
|
895
|
+
(function __scopeHooksInstrument() {
|
|
896
|
+
// Locate the React DevTools hook installed by the browser-entry bundle.
|
|
897
|
+
// We use a lighter approach: walk __REACT_DEVTOOLS_GLOBAL_HOOK__ renderers.
|
|
898
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
899
|
+
if (!hook) return { error: "No React DevTools hook found" };
|
|
900
|
+
|
|
901
|
+
var renderers = hook._renderers || hook.renderers;
|
|
902
|
+
if (!renderers || renderers.size === 0) return { error: "No React renderers registered" };
|
|
903
|
+
|
|
904
|
+
// Get the first renderer's fiber roots
|
|
905
|
+
var renderer = null;
|
|
906
|
+
if (renderers.forEach) {
|
|
907
|
+
renderers.forEach(function(r) { if (!renderer) renderer = r; });
|
|
908
|
+
} else {
|
|
909
|
+
renderer = Object.values(renderers)[0];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Handle both our wrapped format {id, renderer, fiberRoots} and raw renderer
|
|
913
|
+
var fiberRoots;
|
|
914
|
+
if (renderer && renderer.fiberRoots) {
|
|
915
|
+
fiberRoots = renderer.fiberRoots;
|
|
916
|
+
} else {
|
|
917
|
+
return { error: "No fiber roots found" };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
var rootFiber = null;
|
|
921
|
+
fiberRoots.forEach(function(r) { if (!rootFiber) rootFiber = r; });
|
|
922
|
+
if (!rootFiber) return { error: "No fiber root" };
|
|
923
|
+
|
|
924
|
+
var current = rootFiber.current;
|
|
925
|
+
if (!current) return { error: "No current fiber" };
|
|
926
|
+
|
|
927
|
+
// Walk fiber tree and collect hook data per component
|
|
928
|
+
var components = [];
|
|
929
|
+
|
|
930
|
+
function serializeValue(v) {
|
|
931
|
+
if (v === null) return null;
|
|
932
|
+
if (v === undefined) return undefined;
|
|
933
|
+
var t = typeof v;
|
|
934
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
935
|
+
if (t === "function") return "[function " + (v.name || "anonymous") + "]";
|
|
936
|
+
if (Array.isArray(v)) {
|
|
937
|
+
try { return v.map(serializeValue); } catch(e) { return "[Array]"; }
|
|
938
|
+
}
|
|
939
|
+
if (t === "object") {
|
|
940
|
+
try {
|
|
941
|
+
var keys = Object.keys(v).slice(0, 5);
|
|
942
|
+
var out = {};
|
|
943
|
+
for (var k of keys) { out[k] = serializeValue(v[k]); }
|
|
944
|
+
if (Object.keys(v).length > 5) out["..."] = "(truncated)";
|
|
945
|
+
return out;
|
|
946
|
+
} catch(e) { return "[Object]"; }
|
|
947
|
+
}
|
|
948
|
+
return String(v);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Hook node classifiers (mirrors hooks-extractor.ts logic)
|
|
952
|
+
var HookLayout = 0b00100;
|
|
953
|
+
|
|
954
|
+
function isEffectNode(node) {
|
|
955
|
+
var ms = node.memoizedState;
|
|
956
|
+
if (!ms || typeof ms !== "object") return false;
|
|
957
|
+
return typeof ms.create === "function" && "deps" in ms && typeof ms.tag === "number";
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function isRefNode(node) {
|
|
961
|
+
if (node.queue != null) return false;
|
|
962
|
+
var ms = node.memoizedState;
|
|
963
|
+
if (!ms || typeof ms !== "object" || Array.isArray(ms)) return false;
|
|
964
|
+
var keys = Object.keys(ms);
|
|
965
|
+
return keys.length === 1 && keys[0] === "current";
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function isMemoTuple(node) {
|
|
969
|
+
if (node.queue != null) return false;
|
|
970
|
+
var ms = node.memoizedState;
|
|
971
|
+
if (!Array.isArray(ms) || ms.length !== 2) return false;
|
|
972
|
+
return ms[1] === null || Array.isArray(ms[1]);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function isStateOrReducer(node) {
|
|
976
|
+
return node.queue != null &&
|
|
977
|
+
typeof node.queue === "object" &&
|
|
978
|
+
typeof node.queue.dispatch === "function";
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function isReducer(node) {
|
|
982
|
+
if (!isStateOrReducer(node)) return false;
|
|
983
|
+
var q = node.queue;
|
|
984
|
+
if (typeof q.reducer === "function") return true;
|
|
985
|
+
var lrr = q.lastRenderedReducer;
|
|
986
|
+
if (typeof lrr !== "function") return false;
|
|
987
|
+
var name = lrr.name || "";
|
|
988
|
+
return name !== "basicStateReducer" && name !== "";
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function classifyHookNode(node, index) {
|
|
992
|
+
var profile = { index: index, type: "custom" };
|
|
993
|
+
|
|
994
|
+
if (isEffectNode(node)) {
|
|
995
|
+
var effect = node.memoizedState;
|
|
996
|
+
profile.type = (effect.tag & HookLayout) ? "useLayoutEffect" : "useEffect";
|
|
997
|
+
profile.dependencyValues = effect.deps ? effect.deps.map(serializeValue) : null;
|
|
998
|
+
profile.cleanupPresence = typeof effect.destroy === "function";
|
|
999
|
+
profile.fireCount = 1; // We can only observe the mount; runtime tracking would need injection
|
|
1000
|
+
profile.lastDuration = null;
|
|
1001
|
+
return profile;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (isRefNode(node)) {
|
|
1005
|
+
var ref = node.memoizedState;
|
|
1006
|
+
profile.type = "useRef";
|
|
1007
|
+
profile.currentRefValue = serializeValue(ref.current);
|
|
1008
|
+
profile.readCountDuringRender = 0; // static snapshot; read count requires instrumented wrapper
|
|
1009
|
+
return profile;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (isMemoTuple(node)) {
|
|
1013
|
+
var tuple = node.memoizedState;
|
|
1014
|
+
var val = tuple[0];
|
|
1015
|
+
var deps = tuple[1];
|
|
1016
|
+
profile.type = typeof val === "function" ? "useCallback" : "useMemo";
|
|
1017
|
+
profile.currentValue = serializeValue(val);
|
|
1018
|
+
profile.dependencyValues = deps ? deps.map(serializeValue) : null;
|
|
1019
|
+
// recomputeCount cannot be known from a single snapshot; set to 0 for mount
|
|
1020
|
+
profile.recomputeCount = 0;
|
|
1021
|
+
// On mount, first render always computes \u2192 cacheHitRate is 0 (no prior hits)
|
|
1022
|
+
profile.cacheHitRate = 0;
|
|
1023
|
+
return profile;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (isStateOrReducer(node)) {
|
|
1027
|
+
if (isReducer(node)) {
|
|
1028
|
+
profile.type = "useReducer";
|
|
1029
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1030
|
+
profile.updateCount = 0;
|
|
1031
|
+
profile.actionTypesDispatched = [];
|
|
1032
|
+
// stateTransitions: we record current state as first entry
|
|
1033
|
+
profile.stateTransitions = [serializeValue(node.memoizedState)];
|
|
1034
|
+
} else {
|
|
1035
|
+
profile.type = "useState";
|
|
1036
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1037
|
+
profile.updateCount = 0;
|
|
1038
|
+
profile.updateOrigins = [];
|
|
1039
|
+
}
|
|
1040
|
+
return profile;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// useContext: detected via _currentValue / _currentValue2 property (React context internals)
|
|
1044
|
+
// Context consumers in React store the context VALUE in memoizedState directly, not
|
|
1045
|
+
// via a queue. Context objects themselves have _currentValue.
|
|
1046
|
+
// We check if memoizedState could be a context value by seeing if node has no queue
|
|
1047
|
+
// and memoizedState is not an array and not an effect.
|
|
1048
|
+
if (!node.queue && !isRefNode(node) && !isMemoTuple(node) && !isEffectNode(node)) {
|
|
1049
|
+
profile.type = "custom";
|
|
1050
|
+
profile.currentValue = serializeValue(node.memoizedState);
|
|
1051
|
+
return profile;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return profile;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Walk the fiber tree
|
|
1058
|
+
var FunctionComponent = 0;
|
|
1059
|
+
var ClassComponent = 1;
|
|
1060
|
+
var ForwardRef = 11;
|
|
1061
|
+
var MemoComponent = 14;
|
|
1062
|
+
var SimpleMemoComponent = 15;
|
|
1063
|
+
var HostRoot = 3;
|
|
1064
|
+
|
|
1065
|
+
function getFiberName(fiber) {
|
|
1066
|
+
if (!fiber.type) return null;
|
|
1067
|
+
if (typeof fiber.type === "string") return fiber.type;
|
|
1068
|
+
if (typeof fiber.type === "function") return fiber.type.displayName || fiber.type.name || "Anonymous";
|
|
1069
|
+
if (fiber.type.displayName) return fiber.type.displayName;
|
|
1070
|
+
if (fiber.type.render) {
|
|
1071
|
+
return fiber.type.render.displayName || fiber.type.render.name || "ForwardRef";
|
|
1072
|
+
}
|
|
1073
|
+
if (fiber.type.type) {
|
|
1074
|
+
return (fiber.type.type.displayName || fiber.type.type.name || "Memo");
|
|
1075
|
+
}
|
|
1076
|
+
return "Unknown";
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function isComponentFiber(fiber) {
|
|
1080
|
+
var tag = fiber.tag;
|
|
1081
|
+
return tag === FunctionComponent || tag === ClassComponent ||
|
|
1082
|
+
tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function walkFiber(fiber) {
|
|
1086
|
+
if (!fiber) return;
|
|
1087
|
+
|
|
1088
|
+
if (isComponentFiber(fiber)) {
|
|
1089
|
+
var name = getFiberName(fiber);
|
|
1090
|
+
if (name) {
|
|
1091
|
+
var hooks = [];
|
|
1092
|
+
var hookNode = fiber.memoizedState;
|
|
1093
|
+
var idx = 0;
|
|
1094
|
+
while (hookNode !== null && hookNode !== undefined) {
|
|
1095
|
+
hooks.push(classifyHookNode(hookNode, idx));
|
|
1096
|
+
hookNode = hookNode.next;
|
|
1097
|
+
idx++;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
var source = null;
|
|
1101
|
+
if (fiber._debugSource) {
|
|
1102
|
+
source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (hooks.length > 0) {
|
|
1106
|
+
components.push({ name: name, source: source, hooks: hooks });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
walkFiber(fiber.child);
|
|
1112
|
+
walkFiber(fiber.sibling);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
walkFiber(current.child);
|
|
1116
|
+
|
|
1117
|
+
return { components: components };
|
|
1118
|
+
})();
|
|
1119
|
+
`;
|
|
1120
|
+
}
|
|
1121
|
+
function analyzeHookFlags(hooks) {
|
|
1122
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1123
|
+
for (const hook of hooks) {
|
|
1124
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.dependencyValues !== void 0 && hook.dependencyValues === null) {
|
|
1125
|
+
flags.add("EFFECT_EVERY_RENDER");
|
|
1126
|
+
}
|
|
1127
|
+
if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.cleanupPresence === false && hook.dependencyValues === null) {
|
|
1128
|
+
flags.add("MISSING_CLEANUP");
|
|
1129
|
+
}
|
|
1130
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.dependencyValues === null) {
|
|
1131
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1132
|
+
}
|
|
1133
|
+
if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.cacheHitRate !== void 0 && hook.cacheHitRate === 0 && hook.recomputeCount !== void 0 && hook.recomputeCount > 1) {
|
|
1134
|
+
flags.add("MEMO_INEFFECTIVE");
|
|
1135
|
+
}
|
|
1136
|
+
if ((hook.type === "useState" || hook.type === "useReducer") && hook.updateCount !== void 0 && hook.updateCount > 10) {
|
|
1137
|
+
flags.add("STATE_UPDATE_LOOP");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return [...flags];
|
|
1141
|
+
}
|
|
1142
|
+
async function runHooksProfiling(componentName, filePath, props) {
|
|
1143
|
+
const browser = await chromium.launch({ headless: true });
|
|
1144
|
+
try {
|
|
1145
|
+
const context = await browser.newContext();
|
|
1146
|
+
const page = await context.newPage();
|
|
1147
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1148
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1149
|
+
await page.waitForFunction(
|
|
1150
|
+
() => {
|
|
1151
|
+
const w = window;
|
|
1152
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1153
|
+
},
|
|
1154
|
+
{ timeout: 15e3 }
|
|
1155
|
+
);
|
|
1156
|
+
const renderError = await page.evaluate(() => {
|
|
1157
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1158
|
+
});
|
|
1159
|
+
if (renderError !== null) {
|
|
1160
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1161
|
+
}
|
|
1162
|
+
const instrumentScript = buildHookInstrumentationScript();
|
|
1163
|
+
const raw = await page.evaluate(instrumentScript);
|
|
1164
|
+
const result = raw;
|
|
1165
|
+
if (result.error) {
|
|
1166
|
+
throw new Error(`Hook instrumentation failed: ${result.error}`);
|
|
1167
|
+
}
|
|
1168
|
+
const rawComponents = result.components ?? [];
|
|
1169
|
+
const components = rawComponents.map((c) => ({
|
|
1170
|
+
name: c.name,
|
|
1171
|
+
source: c.source,
|
|
1172
|
+
hooks: c.hooks,
|
|
1173
|
+
flags: analyzeHookFlags(c.hooks)
|
|
1174
|
+
}));
|
|
1175
|
+
const allFlags = /* @__PURE__ */ new Set();
|
|
1176
|
+
for (const comp of components) {
|
|
1177
|
+
for (const flag of comp.flags) {
|
|
1178
|
+
allFlags.add(flag);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
component: componentName,
|
|
1183
|
+
components,
|
|
1184
|
+
flags: [...allFlags]
|
|
1185
|
+
};
|
|
1186
|
+
} finally {
|
|
1187
|
+
await browser.close();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function createInstrumentHooksCommand() {
|
|
1191
|
+
const cmd = new Command("hooks").description(
|
|
1192
|
+
"Profile per-hook-instance data for a component: update counts, cache hit rates, effect counts, and more"
|
|
1193
|
+
).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(
|
|
1194
|
+
async (componentName, opts) => {
|
|
1195
|
+
try {
|
|
1196
|
+
const manifest = loadManifest(opts.manifest);
|
|
1197
|
+
const descriptor = manifest.components[componentName];
|
|
1198
|
+
if (descriptor === void 0) {
|
|
1199
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`Component "${componentName}" not found in manifest.
|
|
1202
|
+
Available: ${available}`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
let props = {};
|
|
1206
|
+
try {
|
|
1207
|
+
props = JSON.parse(opts.props);
|
|
1208
|
+
} catch {
|
|
1209
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1210
|
+
}
|
|
1211
|
+
const rootDir = process.cwd();
|
|
1212
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
1213
|
+
process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
|
|
1214
|
+
`);
|
|
1215
|
+
const result = await runHooksProfiling(componentName, filePath, props);
|
|
1216
|
+
if (opts.showFlags) {
|
|
1217
|
+
if (result.flags.length === 0) {
|
|
1218
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1219
|
+
} else {
|
|
1220
|
+
for (const flag of result.flags) {
|
|
1221
|
+
process.stdout.write(`${flag}
|
|
1222
|
+
`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1228
|
+
`);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1231
|
+
`);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
return cmd;
|
|
1237
|
+
}
|
|
1238
|
+
var MANIFEST_PATH3 = ".reactscope/manifest.json";
|
|
1239
|
+
function buildProfilingSetupScript() {
|
|
1240
|
+
return `
|
|
1241
|
+
(function __scopeProfileSetup() {
|
|
1242
|
+
window.__scopeProfileData = {
|
|
1243
|
+
commitCount: 0,
|
|
1244
|
+
componentNames: new Set(),
|
|
1245
|
+
fiberSnapshots: []
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1249
|
+
if (!hook) return { error: "No DevTools hook" };
|
|
1250
|
+
|
|
1251
|
+
// Wrap onCommitFiberRoot to count commits and collect component names
|
|
1252
|
+
var origCommit = hook.onCommitFiberRoot.bind(hook);
|
|
1253
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
1254
|
+
origCommit(rendererID, root, priorityLevel);
|
|
1255
|
+
|
|
1256
|
+
window.__scopeProfileData.commitCount++;
|
|
1257
|
+
|
|
1258
|
+
// Walk the committed tree to collect re-rendered component names
|
|
1259
|
+
var current = root && root.current;
|
|
1260
|
+
if (!current) return;
|
|
1261
|
+
|
|
1262
|
+
function walkFiber(fiber) {
|
|
1263
|
+
if (!fiber) return;
|
|
1264
|
+
var tag = fiber.tag;
|
|
1265
|
+
// FunctionComponent=0, ClassComponent=1, ForwardRef=11, Memo=14, SimpleMemo=15
|
|
1266
|
+
if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
|
|
1267
|
+
var name = null;
|
|
1268
|
+
if (fiber.type) {
|
|
1269
|
+
if (typeof fiber.type === "function") name = fiber.type.displayName || fiber.type.name;
|
|
1270
|
+
else if (fiber.type.displayName) name = fiber.type.displayName;
|
|
1271
|
+
else if (fiber.type.render) name = fiber.type.render.displayName || fiber.type.render.name;
|
|
1272
|
+
else if (fiber.type.type) name = fiber.type.type.displayName || fiber.type.type.name;
|
|
1273
|
+
}
|
|
1274
|
+
// Only count fibers with a positive actualDuration (actually re-rendered this commit)
|
|
1275
|
+
if (name && typeof fiber.actualDuration === "number" && fiber.actualDuration >= 0) {
|
|
1276
|
+
window.__scopeProfileData.componentNames.add(name);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
walkFiber(fiber.child);
|
|
1280
|
+
walkFiber(fiber.sibling);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
var wip = root.current.alternate || root.current;
|
|
1284
|
+
walkFiber(wip.child);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// Install PerformanceObserver for layout and paint timing
|
|
1288
|
+
window.__scopeLayoutTime = 0;
|
|
1289
|
+
window.__scopePaintTime = 0;
|
|
1290
|
+
window.__scopeLayoutShifts = { count: 0, score: 0 };
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
var observer = new PerformanceObserver(function(list) {
|
|
1294
|
+
for (var entry of list.getEntries()) {
|
|
1295
|
+
if (entry.entryType === "layout-shift") {
|
|
1296
|
+
window.__scopeLayoutShifts.count++;
|
|
1297
|
+
window.__scopeLayoutShifts.score += entry.value || 0;
|
|
1298
|
+
}
|
|
1299
|
+
if (entry.entryType === "longtask") {
|
|
1300
|
+
window.__scopeLayoutTime += entry.duration || 0;
|
|
1301
|
+
}
|
|
1302
|
+
if (entry.entryType === "paint") {
|
|
1303
|
+
window.__scopePaintTime += entry.startTime || 0;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
observer.observe({ entryTypes: ["layout-shift", "longtask", "paint"] });
|
|
1308
|
+
} catch(e) {
|
|
1309
|
+
// PerformanceObserver may not be available in all Playwright contexts
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return { ok: true };
|
|
1313
|
+
})();
|
|
1314
|
+
`;
|
|
1315
|
+
}
|
|
1316
|
+
function buildProfilingCollectScript() {
|
|
1317
|
+
return `
|
|
1318
|
+
(function __scopeProfileCollect() {
|
|
1319
|
+
var data = window.__scopeProfileData;
|
|
1320
|
+
if (!data) return { error: "Profiling not set up" };
|
|
1321
|
+
|
|
1322
|
+
// Estimate wasted renders: use paint entries count as a heuristic for
|
|
1323
|
+
// "components that re-rendered but their subtree output was likely unchanged".
|
|
1324
|
+
// A more accurate method would require React's own wasted-render detection,
|
|
1325
|
+
// which requires React Profiler API. We use a conservative estimate here.
|
|
1326
|
+
var totalCommits = data.commitCount;
|
|
1327
|
+
var uniqueNames = Array.from(data.componentNames);
|
|
1328
|
+
|
|
1329
|
+
// Wasted renders heuristic: if a component is in a subsequent commit (not the initial
|
|
1330
|
+
// mount commit), it *may* have been wasted if it didn't actually need to re-render.
|
|
1331
|
+
// For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
|
|
1332
|
+
// This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
|
|
1333
|
+
var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
|
|
1334
|
+
|
|
1335
|
+
return {
|
|
1336
|
+
commitCount: totalCommits,
|
|
1337
|
+
uniqueComponents: uniqueNames.length,
|
|
1338
|
+
componentNames: uniqueNames,
|
|
1339
|
+
wastedRenders: wastedRenders,
|
|
1340
|
+
layoutTime: window.__scopeLayoutTime || 0,
|
|
1341
|
+
paintTime: window.__scopePaintTime || 0,
|
|
1342
|
+
layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
|
|
1343
|
+
};
|
|
1344
|
+
})();
|
|
1345
|
+
`;
|
|
1346
|
+
}
|
|
1347
|
+
async function replayInteraction(page, steps) {
|
|
1348
|
+
for (const step of steps) {
|
|
1349
|
+
switch (step.action) {
|
|
1350
|
+
case "click":
|
|
1351
|
+
if (step.target) {
|
|
1352
|
+
await page.click(step.target, { timeout: 5e3 }).catch(() => {
|
|
1353
|
+
process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
|
|
1354
|
+
`);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
break;
|
|
1358
|
+
case "fill":
|
|
1359
|
+
if (step.target && step.value !== void 0) {
|
|
1360
|
+
await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
|
|
1361
|
+
process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
|
|
1362
|
+
`);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
case "hover":
|
|
1367
|
+
if (step.target) {
|
|
1368
|
+
await page.hover(step.target, { timeout: 5e3 }).catch(() => {
|
|
1369
|
+
process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
|
|
1370
|
+
`);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
break;
|
|
1374
|
+
case "press":
|
|
1375
|
+
if (step.value) {
|
|
1376
|
+
await page.keyboard.press(step.value);
|
|
1377
|
+
}
|
|
1378
|
+
break;
|
|
1379
|
+
case "wait":
|
|
1380
|
+
await page.waitForTimeout(step.delay ?? 500);
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
922
1384
|
}
|
|
923
|
-
function
|
|
924
|
-
|
|
1385
|
+
function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
|
|
1386
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1387
|
+
if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
|
|
1388
|
+
flags.add("WASTED_RENDER");
|
|
1389
|
+
}
|
|
1390
|
+
if (totalRenders > 10) {
|
|
1391
|
+
flags.add("HIGH_RENDER_COUNT");
|
|
1392
|
+
}
|
|
1393
|
+
if (layoutShifts.cumulativeScore > 0.1) {
|
|
1394
|
+
flags.add("LAYOUT_SHIFT_DETECTED");
|
|
1395
|
+
}
|
|
1396
|
+
if (timing.js > 100) {
|
|
1397
|
+
flags.add("SLOW_INTERACTION");
|
|
1398
|
+
}
|
|
1399
|
+
return [...flags];
|
|
925
1400
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
|
|
1401
|
+
async function runInteractionProfile(componentName, filePath, props, interaction) {
|
|
1402
|
+
const browser = await chromium.launch({ headless: true });
|
|
1403
|
+
try {
|
|
1404
|
+
const context = await browser.newContext();
|
|
1405
|
+
const page = await context.newPage();
|
|
1406
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
|
|
1407
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1408
|
+
await page.waitForFunction(
|
|
1409
|
+
() => {
|
|
1410
|
+
const w = window;
|
|
1411
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1412
|
+
},
|
|
1413
|
+
{ timeout: 15e3 }
|
|
1414
|
+
);
|
|
1415
|
+
const renderError = await page.evaluate(() => {
|
|
1416
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
1417
|
+
});
|
|
1418
|
+
if (renderError !== null) {
|
|
1419
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1420
|
+
}
|
|
1421
|
+
const setupResult = await page.evaluate(buildProfilingSetupScript());
|
|
1422
|
+
const setupData = setupResult;
|
|
1423
|
+
if (setupData.error) {
|
|
1424
|
+
throw new Error(`Profiling setup failed: ${setupData.error}`);
|
|
1425
|
+
}
|
|
1426
|
+
const jsStart = Date.now();
|
|
1427
|
+
if (interaction.length > 0) {
|
|
1428
|
+
process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
|
|
1429
|
+
`);
|
|
1430
|
+
await replayInteraction(page, interaction);
|
|
1431
|
+
await page.waitForTimeout(300);
|
|
1432
|
+
}
|
|
1433
|
+
const jsDuration = Date.now() - jsStart;
|
|
1434
|
+
const collected = await page.evaluate(buildProfilingCollectScript());
|
|
1435
|
+
const profileData = collected;
|
|
1436
|
+
if (profileData.error) {
|
|
1437
|
+
throw new Error(`Profile collection failed: ${profileData.error}`);
|
|
1438
|
+
}
|
|
1439
|
+
const timing = {
|
|
1440
|
+
js: jsDuration,
|
|
1441
|
+
layout: profileData.layoutTime ?? 0,
|
|
1442
|
+
paint: profileData.paintTime ?? 0
|
|
1443
|
+
};
|
|
1444
|
+
const layoutShifts = {
|
|
1445
|
+
count: profileData.layoutShifts?.count ?? 0,
|
|
1446
|
+
cumulativeScore: profileData.layoutShifts?.score ?? 0
|
|
1447
|
+
};
|
|
1448
|
+
const totalRenders = profileData.commitCount ?? 0;
|
|
1449
|
+
const uniqueComponents = profileData.uniqueComponents ?? 0;
|
|
1450
|
+
const wastedRenders = profileData.wastedRenders ?? 0;
|
|
1451
|
+
const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
|
|
1452
|
+
return {
|
|
1453
|
+
component: componentName,
|
|
1454
|
+
totalRenders,
|
|
1455
|
+
uniqueComponents,
|
|
1456
|
+
wastedRenders,
|
|
1457
|
+
timing,
|
|
1458
|
+
layoutShifts,
|
|
1459
|
+
flags,
|
|
1460
|
+
interaction
|
|
1461
|
+
};
|
|
1462
|
+
} finally {
|
|
1463
|
+
await browser.close();
|
|
929
1464
|
}
|
|
930
|
-
|
|
1465
|
+
}
|
|
1466
|
+
function createInstrumentProfileCommand() {
|
|
1467
|
+
const cmd = new Command("profile").description(
|
|
1468
|
+
"Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
|
|
1469
|
+
).argument("<component>", "Component name (must exist in the manifest)").option(
|
|
1470
|
+
"--interaction <json>",
|
|
1471
|
+
`Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
|
|
1472
|
+
"[]"
|
|
1473
|
+
).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(
|
|
1474
|
+
async (componentName, opts) => {
|
|
1475
|
+
try {
|
|
1476
|
+
const manifest = loadManifest(opts.manifest);
|
|
1477
|
+
const descriptor = manifest.components[componentName];
|
|
1478
|
+
if (descriptor === void 0) {
|
|
1479
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`Component "${componentName}" not found in manifest.
|
|
1482
|
+
Available: ${available}`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
let props = {};
|
|
1486
|
+
try {
|
|
1487
|
+
props = JSON.parse(opts.props);
|
|
1488
|
+
} catch {
|
|
1489
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1490
|
+
}
|
|
1491
|
+
let interaction = [];
|
|
1492
|
+
try {
|
|
1493
|
+
interaction = JSON.parse(opts.interaction);
|
|
1494
|
+
if (!Array.isArray(interaction)) {
|
|
1495
|
+
throw new Error("Interaction must be a JSON array");
|
|
1496
|
+
}
|
|
1497
|
+
} catch {
|
|
1498
|
+
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
1499
|
+
}
|
|
1500
|
+
const rootDir = process.cwd();
|
|
1501
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
1502
|
+
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
1503
|
+
`);
|
|
1504
|
+
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
1505
|
+
if (opts.showFlags) {
|
|
1506
|
+
if (result.flags.length === 0) {
|
|
1507
|
+
process.stdout.write("No heuristic flags detected.\n");
|
|
1508
|
+
} else {
|
|
1509
|
+
for (const flag of result.flags) {
|
|
1510
|
+
process.stdout.write(`${flag}
|
|
1511
|
+
`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1517
|
+
`);
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1520
|
+
`);
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
);
|
|
1525
|
+
return cmd;
|
|
931
1526
|
}
|
|
932
1527
|
|
|
933
1528
|
// src/instrument/renders.ts
|
|
934
|
-
var
|
|
1529
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
935
1530
|
function determineTrigger(event) {
|
|
936
1531
|
if (event.forceUpdate) return "force_update";
|
|
937
1532
|
if (event.stateChanged) return "state_change";
|
|
@@ -1176,7 +1771,7 @@ function buildInstrumentationScript() {
|
|
|
1176
1771
|
`
|
|
1177
1772
|
);
|
|
1178
1773
|
}
|
|
1179
|
-
async function
|
|
1774
|
+
async function replayInteraction2(page, steps) {
|
|
1180
1775
|
for (const step of steps) {
|
|
1181
1776
|
switch (step.action) {
|
|
1182
1777
|
case "click":
|
|
@@ -1249,7 +1844,7 @@ async function shutdownPool() {
|
|
|
1249
1844
|
}
|
|
1250
1845
|
}
|
|
1251
1846
|
async function analyzeRenders(options) {
|
|
1252
|
-
const manifestPath = options.manifestPath ??
|
|
1847
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
|
|
1253
1848
|
const manifest = loadManifest(manifestPath);
|
|
1254
1849
|
const descriptor = manifest.components[options.componentName];
|
|
1255
1850
|
if (descriptor === void 0) {
|
|
@@ -1278,7 +1873,7 @@ Available: ${available}`
|
|
|
1278
1873
|
window.__SCOPE_RENDER_EVENTS__ = [];
|
|
1279
1874
|
window.__SCOPE_RENDER_INDEX__ = 0;
|
|
1280
1875
|
});
|
|
1281
|
-
await
|
|
1876
|
+
await replayInteraction2(page, options.interaction);
|
|
1282
1877
|
await page.waitForTimeout(200);
|
|
1283
1878
|
const interactionDurationMs = performance.now() - startMs;
|
|
1284
1879
|
const rawEvents = await page.evaluate(() => {
|
|
@@ -1347,7 +1942,7 @@ function createInstrumentRendersCommand() {
|
|
|
1347
1942
|
"--interaction <json>",
|
|
1348
1943
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1349
1944
|
"[]"
|
|
1350
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
1945
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
|
|
1351
1946
|
async (componentName, opts) => {
|
|
1352
1947
|
let interaction = [];
|
|
1353
1948
|
try {
|
|
@@ -1392,8 +1987,50 @@ function createInstrumentCommand() {
|
|
|
1392
1987
|
"Structured instrumentation commands for React component analysis"
|
|
1393
1988
|
);
|
|
1394
1989
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1990
|
+
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
1991
|
+
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
1395
1992
|
return instrumentCmd;
|
|
1396
1993
|
}
|
|
1994
|
+
async function browserCapture(options) {
|
|
1995
|
+
const { url, timeout = 1e4, wait = 0 } = options;
|
|
1996
|
+
const browser = await chromium.launch({ headless: true });
|
|
1997
|
+
try {
|
|
1998
|
+
const context = await browser.newContext();
|
|
1999
|
+
const page = await context.newPage();
|
|
2000
|
+
await page.addInitScript({ content: getBrowserEntryScript() });
|
|
2001
|
+
await page.goto(url, {
|
|
2002
|
+
waitUntil: "networkidle",
|
|
2003
|
+
timeout: timeout + 5e3
|
|
2004
|
+
});
|
|
2005
|
+
await page.waitForFunction(
|
|
2006
|
+
() => {
|
|
2007
|
+
return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
|
|
2008
|
+
},
|
|
2009
|
+
{ timeout }
|
|
2010
|
+
);
|
|
2011
|
+
if (wait > 0) {
|
|
2012
|
+
await page.waitForTimeout(wait);
|
|
2013
|
+
}
|
|
2014
|
+
const raw = await page.evaluate(async () => {
|
|
2015
|
+
const win = window;
|
|
2016
|
+
if (typeof win.__SCOPE_CAPTURE__ !== "function") {
|
|
2017
|
+
throw new Error("Scope runtime not injected");
|
|
2018
|
+
}
|
|
2019
|
+
return win.__SCOPE_CAPTURE__();
|
|
2020
|
+
});
|
|
2021
|
+
if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
|
|
2022
|
+
throw new Error(`Scope capture failed: ${raw.error}`);
|
|
2023
|
+
}
|
|
2024
|
+
const report = { ...raw, route: null };
|
|
2025
|
+
return { report };
|
|
2026
|
+
} finally {
|
|
2027
|
+
await browser.close();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function writeReportToFile(report, outputPath, pretty) {
|
|
2031
|
+
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2032
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
2033
|
+
}
|
|
1397
2034
|
var CONFIG_FILENAMES = [
|
|
1398
2035
|
".reactscope/config.json",
|
|
1399
2036
|
".reactscope/config.js",
|
|
@@ -1511,7 +2148,7 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
1511
2148
|
}
|
|
1512
2149
|
|
|
1513
2150
|
// src/render-commands.ts
|
|
1514
|
-
var
|
|
2151
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1515
2152
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
1516
2153
|
var _pool2 = null;
|
|
1517
2154
|
async function getPool2(viewportWidth, viewportHeight) {
|
|
@@ -1636,7 +2273,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
1636
2273
|
};
|
|
1637
2274
|
}
|
|
1638
2275
|
function registerRenderSingle(renderCmd) {
|
|
1639
|
-
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",
|
|
2276
|
+
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(
|
|
1640
2277
|
async (componentName, opts) => {
|
|
1641
2278
|
try {
|
|
1642
2279
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1735,7 +2372,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
1735
2372
|
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(
|
|
1736
2373
|
"--contexts <ids>",
|
|
1737
2374
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
1738
|
-
).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",
|
|
2375
|
+
).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(
|
|
1739
2376
|
async (componentName, opts) => {
|
|
1740
2377
|
try {
|
|
1741
2378
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1862,7 +2499,7 @@ Available: ${available}`
|
|
|
1862
2499
|
);
|
|
1863
2500
|
}
|
|
1864
2501
|
function registerRenderAll(renderCmd) {
|
|
1865
|
-
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",
|
|
2502
|
+
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(
|
|
1866
2503
|
async (opts) => {
|
|
1867
2504
|
try {
|
|
1868
2505
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1997,6 +2634,325 @@ function createRenderCommand() {
|
|
|
1997
2634
|
registerRenderAll(renderCmd);
|
|
1998
2635
|
return renderCmd;
|
|
1999
2636
|
}
|
|
2637
|
+
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2638
|
+
var _pool3 = null;
|
|
2639
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2640
|
+
if (_pool3 === null) {
|
|
2641
|
+
_pool3 = new BrowserPool({
|
|
2642
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2643
|
+
viewportWidth,
|
|
2644
|
+
viewportHeight
|
|
2645
|
+
});
|
|
2646
|
+
await _pool3.init();
|
|
2647
|
+
}
|
|
2648
|
+
return _pool3;
|
|
2649
|
+
}
|
|
2650
|
+
async function shutdownPool3() {
|
|
2651
|
+
if (_pool3 !== null) {
|
|
2652
|
+
await _pool3.close();
|
|
2653
|
+
_pool3 = null;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2657
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2658
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2659
|
+
const slot = await pool.acquire();
|
|
2660
|
+
const { page } = slot;
|
|
2661
|
+
try {
|
|
2662
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
2663
|
+
await page.waitForFunction(
|
|
2664
|
+
() => {
|
|
2665
|
+
const w = window;
|
|
2666
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
2667
|
+
},
|
|
2668
|
+
{ timeout: 15e3 }
|
|
2669
|
+
);
|
|
2670
|
+
const renderError = await page.evaluate(() => {
|
|
2671
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
2672
|
+
});
|
|
2673
|
+
if (renderError !== null) {
|
|
2674
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
2675
|
+
}
|
|
2676
|
+
const rootDir = process.cwd();
|
|
2677
|
+
const classes = await page.evaluate(() => {
|
|
2678
|
+
const set = /* @__PURE__ */ new Set();
|
|
2679
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
2680
|
+
for (const c of el.className.split(/\s+/)) {
|
|
2681
|
+
if (c) set.add(c);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
return [...set];
|
|
2685
|
+
});
|
|
2686
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
2687
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
2688
|
+
await page.addStyleTag({ content: projectCss });
|
|
2689
|
+
}
|
|
2690
|
+
const startMs = performance.now();
|
|
2691
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
2692
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
2693
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
2694
|
+
throw new Error(
|
|
2695
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
const PAD = 24;
|
|
2699
|
+
const MIN_W = 320;
|
|
2700
|
+
const MIN_H = 200;
|
|
2701
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
2702
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
2703
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
2704
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
2705
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
2706
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
2707
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
2708
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
2709
|
+
const screenshot = await page.screenshot({
|
|
2710
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
2711
|
+
type: "png"
|
|
2712
|
+
});
|
|
2713
|
+
const computedStylesRaw = {};
|
|
2714
|
+
const styles = await page.evaluate((sel) => {
|
|
2715
|
+
const el = document.querySelector(sel);
|
|
2716
|
+
if (el === null) return {};
|
|
2717
|
+
const computed = window.getComputedStyle(el);
|
|
2718
|
+
const out = {};
|
|
2719
|
+
for (const prop of [
|
|
2720
|
+
"display",
|
|
2721
|
+
"width",
|
|
2722
|
+
"height",
|
|
2723
|
+
"color",
|
|
2724
|
+
"backgroundColor",
|
|
2725
|
+
"fontSize",
|
|
2726
|
+
"fontFamily",
|
|
2727
|
+
"padding",
|
|
2728
|
+
"margin"
|
|
2729
|
+
]) {
|
|
2730
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
2731
|
+
}
|
|
2732
|
+
return out;
|
|
2733
|
+
}, "[data-reactscope-root] > *");
|
|
2734
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
2735
|
+
const renderTimeMs = performance.now() - startMs;
|
|
2736
|
+
return {
|
|
2737
|
+
screenshot,
|
|
2738
|
+
width: Math.round(safeW),
|
|
2739
|
+
height: Math.round(safeH),
|
|
2740
|
+
renderTimeMs,
|
|
2741
|
+
computedStyles: computedStylesRaw
|
|
2742
|
+
};
|
|
2743
|
+
} finally {
|
|
2744
|
+
pool.release(slot);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
2748
|
+
const flat = {};
|
|
2749
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
2750
|
+
Object.assign(flat, styles);
|
|
2751
|
+
}
|
|
2752
|
+
const colors = {};
|
|
2753
|
+
const spacing = {};
|
|
2754
|
+
const typography = {};
|
|
2755
|
+
const borders = {};
|
|
2756
|
+
const shadows = {};
|
|
2757
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
2758
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
2759
|
+
colors[prop] = value;
|
|
2760
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
2761
|
+
spacing[prop] = value;
|
|
2762
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
2763
|
+
typography[prop] = value;
|
|
2764
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
2765
|
+
borders[prop] = value;
|
|
2766
|
+
} else if (prop === "boxShadow") {
|
|
2767
|
+
shadows[prop] = value;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
return { colors, spacing, typography, borders, shadows };
|
|
2771
|
+
}
|
|
2772
|
+
async function runBaseline(options = {}) {
|
|
2773
|
+
const {
|
|
2774
|
+
outputDir = DEFAULT_BASELINE_DIR,
|
|
2775
|
+
componentsGlob,
|
|
2776
|
+
manifestPath,
|
|
2777
|
+
viewportWidth = 375,
|
|
2778
|
+
viewportHeight = 812
|
|
2779
|
+
} = options;
|
|
2780
|
+
const startTime = performance.now();
|
|
2781
|
+
const rootDir = process.cwd();
|
|
2782
|
+
const baselineDir = resolve(rootDir, outputDir);
|
|
2783
|
+
const rendersDir = resolve(baselineDir, "renders");
|
|
2784
|
+
if (existsSync(baselineDir)) {
|
|
2785
|
+
rmSync(baselineDir, { recursive: true, force: true });
|
|
2786
|
+
}
|
|
2787
|
+
mkdirSync(rendersDir, { recursive: true });
|
|
2788
|
+
let manifest;
|
|
2789
|
+
if (manifestPath !== void 0) {
|
|
2790
|
+
const { readFileSync: readFileSync8 } = await import('fs');
|
|
2791
|
+
const absPath = resolve(rootDir, manifestPath);
|
|
2792
|
+
if (!existsSync(absPath)) {
|
|
2793
|
+
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2794
|
+
}
|
|
2795
|
+
manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
2796
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2797
|
+
`);
|
|
2798
|
+
} else {
|
|
2799
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
2800
|
+
manifest = await generateManifest({ rootDir });
|
|
2801
|
+
const count = Object.keys(manifest.components).length;
|
|
2802
|
+
process.stderr.write(`Found ${count} components.
|
|
2803
|
+
`);
|
|
2804
|
+
}
|
|
2805
|
+
writeFileSync(resolve(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2806
|
+
let componentNames = Object.keys(manifest.components);
|
|
2807
|
+
if (componentsGlob !== void 0) {
|
|
2808
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
2809
|
+
process.stderr.write(
|
|
2810
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
2811
|
+
`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
const total = componentNames.length;
|
|
2815
|
+
if (total === 0) {
|
|
2816
|
+
process.stderr.write("No components to baseline.\n");
|
|
2817
|
+
const emptyReport = {
|
|
2818
|
+
components: {},
|
|
2819
|
+
totalProperties: 0,
|
|
2820
|
+
totalOnSystem: 0,
|
|
2821
|
+
totalOffSystem: 0,
|
|
2822
|
+
aggregateCompliance: 1,
|
|
2823
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2824
|
+
};
|
|
2825
|
+
writeFileSync(
|
|
2826
|
+
resolve(baselineDir, "compliance.json"),
|
|
2827
|
+
JSON.stringify(emptyReport, null, 2),
|
|
2828
|
+
"utf-8"
|
|
2829
|
+
);
|
|
2830
|
+
return {
|
|
2831
|
+
baselineDir,
|
|
2832
|
+
componentCount: 0,
|
|
2833
|
+
failureCount: 0,
|
|
2834
|
+
wallClockMs: performance.now() - startTime
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
2838
|
+
`);
|
|
2839
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
2840
|
+
let completed = 0;
|
|
2841
|
+
let failureCount = 0;
|
|
2842
|
+
const CONCURRENCY = 4;
|
|
2843
|
+
let nextIdx = 0;
|
|
2844
|
+
const renderOne = async (name) => {
|
|
2845
|
+
const descriptor = manifest.components[name];
|
|
2846
|
+
if (descriptor === void 0) return;
|
|
2847
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
2848
|
+
const outcome = await safeRender(
|
|
2849
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2850
|
+
{
|
|
2851
|
+
props: {},
|
|
2852
|
+
sourceLocation: {
|
|
2853
|
+
file: descriptor.filePath,
|
|
2854
|
+
line: descriptor.loc.start,
|
|
2855
|
+
column: 0
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
);
|
|
2859
|
+
completed++;
|
|
2860
|
+
const pct = Math.round(completed / total * 100);
|
|
2861
|
+
if (isTTY()) {
|
|
2862
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
2863
|
+
}
|
|
2864
|
+
if (outcome.crashed) {
|
|
2865
|
+
failureCount++;
|
|
2866
|
+
const errPath = resolve(rendersDir, `${name}.error.json`);
|
|
2867
|
+
writeFileSync(
|
|
2868
|
+
errPath,
|
|
2869
|
+
JSON.stringify(
|
|
2870
|
+
{
|
|
2871
|
+
component: name,
|
|
2872
|
+
errorMessage: outcome.error.message,
|
|
2873
|
+
heuristicFlags: outcome.error.heuristicFlags,
|
|
2874
|
+
propsAtCrash: outcome.error.propsAtCrash
|
|
2875
|
+
},
|
|
2876
|
+
null,
|
|
2877
|
+
2
|
|
2878
|
+
),
|
|
2879
|
+
"utf-8"
|
|
2880
|
+
);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
const result = outcome.result;
|
|
2884
|
+
writeFileSync(resolve(rendersDir, `${name}.png`), result.screenshot);
|
|
2885
|
+
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2886
|
+
writeFileSync(
|
|
2887
|
+
resolve(rendersDir, `${name}.json`),
|
|
2888
|
+
JSON.stringify(jsonOutput, null, 2),
|
|
2889
|
+
"utf-8"
|
|
2890
|
+
);
|
|
2891
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
2892
|
+
};
|
|
2893
|
+
const worker = async () => {
|
|
2894
|
+
while (nextIdx < componentNames.length) {
|
|
2895
|
+
const i = nextIdx++;
|
|
2896
|
+
const name = componentNames[i];
|
|
2897
|
+
if (name !== void 0) {
|
|
2898
|
+
await renderOne(name);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
const workers = [];
|
|
2903
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
2904
|
+
workers.push(worker());
|
|
2905
|
+
}
|
|
2906
|
+
await Promise.all(workers);
|
|
2907
|
+
await shutdownPool3();
|
|
2908
|
+
if (isTTY()) {
|
|
2909
|
+
process.stderr.write("\n");
|
|
2910
|
+
}
|
|
2911
|
+
const resolver = new TokenResolver([]);
|
|
2912
|
+
const engine = new ComplianceEngine(resolver);
|
|
2913
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2914
|
+
writeFileSync(
|
|
2915
|
+
resolve(baselineDir, "compliance.json"),
|
|
2916
|
+
JSON.stringify(batchReport, null, 2),
|
|
2917
|
+
"utf-8"
|
|
2918
|
+
);
|
|
2919
|
+
const wallClockMs = performance.now() - startTime;
|
|
2920
|
+
const successCount = total - failureCount;
|
|
2921
|
+
process.stderr.write(
|
|
2922
|
+
`
|
|
2923
|
+
Baseline complete: ${successCount}/${total} components rendered` + (failureCount > 0 ? ` (${failureCount} failed)` : "") + ` in ${(wallClockMs / 1e3).toFixed(1)}s
|
|
2924
|
+
`
|
|
2925
|
+
);
|
|
2926
|
+
process.stderr.write(`Snapshot saved to ${baselineDir}
|
|
2927
|
+
`);
|
|
2928
|
+
return { baselineDir, componentCount: total, failureCount, wallClockMs };
|
|
2929
|
+
}
|
|
2930
|
+
function registerBaselineSubCommand(reportCmd) {
|
|
2931
|
+
reportCmd.command("baseline").description("Capture a baseline snapshot (manifest + renders + compliance) for later diffing").option(
|
|
2932
|
+
"-o, --output <dir>",
|
|
2933
|
+
"Output directory for the baseline snapshot",
|
|
2934
|
+
DEFAULT_BASELINE_DIR
|
|
2935
|
+
).option("--components <glob>", "Glob pattern to baseline a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").action(
|
|
2936
|
+
async (opts) => {
|
|
2937
|
+
try {
|
|
2938
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
2939
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
2940
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
2941
|
+
await runBaseline({
|
|
2942
|
+
outputDir: opts.output,
|
|
2943
|
+
componentsGlob: opts.components,
|
|
2944
|
+
manifestPath: opts.manifest,
|
|
2945
|
+
viewportWidth,
|
|
2946
|
+
viewportHeight
|
|
2947
|
+
});
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2950
|
+
`);
|
|
2951
|
+
process.exit(1);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2000
2956
|
|
|
2001
2957
|
// src/tree-formatter.ts
|
|
2002
2958
|
var BRANCH = "\u251C\u2500\u2500 ";
|
|
@@ -2277,6 +3233,109 @@ function buildStructuredReport(report) {
|
|
|
2277
3233
|
}
|
|
2278
3234
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
2279
3235
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3236
|
+
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3237
|
+
function resolveTokenFilePath(fileFlag) {
|
|
3238
|
+
if (fileFlag !== void 0) {
|
|
3239
|
+
return resolve(process.cwd(), fileFlag);
|
|
3240
|
+
}
|
|
3241
|
+
const configPath = resolve(process.cwd(), CONFIG_FILE);
|
|
3242
|
+
if (existsSync(configPath)) {
|
|
3243
|
+
try {
|
|
3244
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
3245
|
+
const config = JSON.parse(raw);
|
|
3246
|
+
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3247
|
+
const file = config.tokens.file;
|
|
3248
|
+
return resolve(process.cwd(), file);
|
|
3249
|
+
}
|
|
3250
|
+
} catch {
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
return resolve(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3254
|
+
}
|
|
3255
|
+
function createTokensExportCommand() {
|
|
3256
|
+
return new 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(
|
|
3257
|
+
"--theme <name>",
|
|
3258
|
+
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3259
|
+
).action(
|
|
3260
|
+
(opts) => {
|
|
3261
|
+
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
3262
|
+
process.stderr.write(
|
|
3263
|
+
`Error: unsupported format "${opts.format}".
|
|
3264
|
+
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
3265
|
+
`
|
|
3266
|
+
);
|
|
3267
|
+
process.exit(1);
|
|
3268
|
+
}
|
|
3269
|
+
const format = opts.format;
|
|
3270
|
+
try {
|
|
3271
|
+
const filePath = resolveTokenFilePath(opts.file);
|
|
3272
|
+
if (!existsSync(filePath)) {
|
|
3273
|
+
throw new Error(
|
|
3274
|
+
`Token file not found at ${filePath}.
|
|
3275
|
+
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
3279
|
+
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3280
|
+
let themesMap;
|
|
3281
|
+
if (opts.theme !== void 0) {
|
|
3282
|
+
if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
|
|
3283
|
+
const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
|
|
3284
|
+
throw new Error(
|
|
3285
|
+
`Theme "${opts.theme}" not found in token file.
|
|
3286
|
+
Available themes: ${available}`
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
const baseResolver = new TokenResolver(tokens);
|
|
3290
|
+
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3291
|
+
baseResolver,
|
|
3292
|
+
rawFile
|
|
3293
|
+
);
|
|
3294
|
+
const themeNames = themeResolver.listThemes();
|
|
3295
|
+
if (!themeNames.includes(opts.theme)) {
|
|
3296
|
+
throw new Error(
|
|
3297
|
+
`Theme "${opts.theme}" could not be resolved.
|
|
3298
|
+
Available themes: ${themeNames.join(", ")}`
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
const themedTokens = themeResolver.buildThemedTokens(opts.theme);
|
|
3302
|
+
const overrideMap = /* @__PURE__ */ new Map();
|
|
3303
|
+
for (const themedToken of themedTokens) {
|
|
3304
|
+
const baseToken = tokens.find((t) => t.path === themedToken.path);
|
|
3305
|
+
if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
|
|
3306
|
+
overrideMap.set(themedToken.path, themedToken.resolvedValue);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
|
|
3310
|
+
}
|
|
3311
|
+
const output = exportTokens(tokens, format, {
|
|
3312
|
+
prefix: opts.prefix,
|
|
3313
|
+
rootSelector: opts.selector,
|
|
3314
|
+
themes: themesMap
|
|
3315
|
+
});
|
|
3316
|
+
if (opts.out !== void 0) {
|
|
3317
|
+
const outPath = resolve(process.cwd(), opts.out);
|
|
3318
|
+
writeFileSync(outPath, output, "utf-8");
|
|
3319
|
+
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3320
|
+
`);
|
|
3321
|
+
} else {
|
|
3322
|
+
process.stdout.write(output);
|
|
3323
|
+
if (!output.endsWith("\n")) {
|
|
3324
|
+
process.stdout.write("\n");
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
} catch (err) {
|
|
3328
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3329
|
+
`);
|
|
3330
|
+
process.exit(1);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// src/tokens/commands.ts
|
|
3337
|
+
var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
|
|
3338
|
+
var CONFIG_FILE2 = "reactscope.config.json";
|
|
2280
3339
|
function isTTY2() {
|
|
2281
3340
|
return process.stdout.isTTY === true;
|
|
2282
3341
|
}
|
|
@@ -2294,11 +3353,11 @@ function buildTable2(headers, rows) {
|
|
|
2294
3353
|
);
|
|
2295
3354
|
return [headerRow, divider, ...dataRows].join("\n");
|
|
2296
3355
|
}
|
|
2297
|
-
function
|
|
3356
|
+
function resolveTokenFilePath2(fileFlag) {
|
|
2298
3357
|
if (fileFlag !== void 0) {
|
|
2299
3358
|
return resolve(process.cwd(), fileFlag);
|
|
2300
3359
|
}
|
|
2301
|
-
const configPath = resolve(process.cwd(),
|
|
3360
|
+
const configPath = resolve(process.cwd(), CONFIG_FILE2);
|
|
2302
3361
|
if (existsSync(configPath)) {
|
|
2303
3362
|
try {
|
|
2304
3363
|
const raw = readFileSync(configPath, "utf-8");
|
|
@@ -2310,7 +3369,7 @@ function resolveTokenFilePath(fileFlag) {
|
|
|
2310
3369
|
} catch {
|
|
2311
3370
|
}
|
|
2312
3371
|
}
|
|
2313
|
-
return resolve(process.cwd(),
|
|
3372
|
+
return resolve(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
2314
3373
|
}
|
|
2315
3374
|
function loadTokens(absPath) {
|
|
2316
3375
|
if (!existsSync(absPath)) {
|
|
@@ -2357,7 +3416,7 @@ function buildResolutionChain(startPath, rawTokens) {
|
|
|
2357
3416
|
function registerGet2(tokensCmd) {
|
|
2358
3417
|
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) => {
|
|
2359
3418
|
try {
|
|
2360
|
-
const filePath =
|
|
3419
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2361
3420
|
const { tokens } = loadTokens(filePath);
|
|
2362
3421
|
const resolver = new TokenResolver(tokens);
|
|
2363
3422
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
@@ -2383,7 +3442,7 @@ function registerList2(tokensCmd) {
|
|
|
2383
3442
|
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(
|
|
2384
3443
|
(category, opts) => {
|
|
2385
3444
|
try {
|
|
2386
|
-
const filePath =
|
|
3445
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2387
3446
|
const { tokens } = loadTokens(filePath);
|
|
2388
3447
|
const resolver = new TokenResolver(tokens);
|
|
2389
3448
|
const filtered = resolver.list(opts.type, category);
|
|
@@ -2413,7 +3472,7 @@ function registerSearch(tokensCmd) {
|
|
|
2413
3472
|
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(
|
|
2414
3473
|
(value, opts) => {
|
|
2415
3474
|
try {
|
|
2416
|
-
const filePath =
|
|
3475
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2417
3476
|
const { tokens } = loadTokens(filePath);
|
|
2418
3477
|
const resolver = new TokenResolver(tokens);
|
|
2419
3478
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
@@ -2495,7 +3554,7 @@ Tip: use --fuzzy for nearest-match search.
|
|
|
2495
3554
|
function registerResolve(tokensCmd) {
|
|
2496
3555
|
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) => {
|
|
2497
3556
|
try {
|
|
2498
|
-
const filePath =
|
|
3557
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2499
3558
|
const absFilePath = filePath;
|
|
2500
3559
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
2501
3560
|
const resolver = new TokenResolver(tokens);
|
|
@@ -2532,7 +3591,7 @@ function registerValidate(tokensCmd) {
|
|
|
2532
3591
|
"Validate the token file for errors (circular refs, missing refs, type mismatches)"
|
|
2533
3592
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2534
3593
|
try {
|
|
2535
|
-
const filePath =
|
|
3594
|
+
const filePath = resolveTokenFilePath2(opts.file);
|
|
2536
3595
|
if (!existsSync(filePath)) {
|
|
2537
3596
|
throw new Error(
|
|
2538
3597
|
`Token file not found at ${filePath}.
|
|
@@ -2615,6 +3674,7 @@ function createTokensCommand() {
|
|
|
2615
3674
|
registerSearch(tokensCmd);
|
|
2616
3675
|
registerResolve(tokensCmd);
|
|
2617
3676
|
registerValidate(tokensCmd);
|
|
3677
|
+
tokensCmd.addCommand(createTokensExportCommand());
|
|
2618
3678
|
return tokensCmd;
|
|
2619
3679
|
}
|
|
2620
3680
|
|
|
@@ -2707,9 +3767,13 @@ function createProgram(options = {}) {
|
|
|
2707
3767
|
program.addCommand(createTokensCommand());
|
|
2708
3768
|
program.addCommand(createInstrumentCommand());
|
|
2709
3769
|
program.addCommand(createInitCommand());
|
|
3770
|
+
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
3771
|
+
if (existingReportCmd !== void 0) {
|
|
3772
|
+
registerBaselineSubCommand(existingReportCmd);
|
|
3773
|
+
}
|
|
2710
3774
|
return program;
|
|
2711
3775
|
}
|
|
2712
3776
|
|
|
2713
|
-
export { createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
|
|
3777
|
+
export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath, runInit };
|
|
2714
3778
|
//# sourceMappingURL=index.js.map
|
|
2715
3779
|
//# sourceMappingURL=index.js.map
|