@agent-scope/cli 1.11.0 → 1.13.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 +995 -150
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +892 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +892 -62
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -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((resolve12) => {
|
|
232
232
|
rl.question(question, (answer) => {
|
|
233
|
-
|
|
233
|
+
resolve12(answer.trim());
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
}
|
|
@@ -1547,9 +1547,364 @@ Available: ${available}`
|
|
|
1547
1547
|
);
|
|
1548
1548
|
return cmd;
|
|
1549
1549
|
}
|
|
1550
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1551
|
+
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1552
|
+
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1553
|
+
var _pool = null;
|
|
1554
|
+
async function getPool() {
|
|
1555
|
+
if (_pool === null) {
|
|
1556
|
+
_pool = new render.BrowserPool({
|
|
1557
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1558
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1559
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1560
|
+
});
|
|
1561
|
+
await _pool.init();
|
|
1562
|
+
}
|
|
1563
|
+
return _pool;
|
|
1564
|
+
}
|
|
1565
|
+
async function shutdownPool() {
|
|
1566
|
+
if (_pool !== null) {
|
|
1567
|
+
await _pool.close();
|
|
1568
|
+
_pool = null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function mapNodeType(node) {
|
|
1572
|
+
if (node.type === "forward_ref") return "forwardRef";
|
|
1573
|
+
if (node.type === "host") return "host";
|
|
1574
|
+
const name = node.name;
|
|
1575
|
+
if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
|
|
1576
|
+
if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
|
|
1577
|
+
return node.type;
|
|
1578
|
+
}
|
|
1579
|
+
function flattenSerializedValue(sv) {
|
|
1580
|
+
if (sv === null || sv === void 0) return null;
|
|
1581
|
+
const v = sv;
|
|
1582
|
+
switch (v.type) {
|
|
1583
|
+
case "null":
|
|
1584
|
+
case "undefined":
|
|
1585
|
+
return null;
|
|
1586
|
+
case "string":
|
|
1587
|
+
case "number":
|
|
1588
|
+
case "boolean":
|
|
1589
|
+
return v.value;
|
|
1590
|
+
case "object": {
|
|
1591
|
+
if (!Array.isArray(v.entries)) return {};
|
|
1592
|
+
const result = {};
|
|
1593
|
+
for (const entry of v.entries) {
|
|
1594
|
+
result[entry.key] = flattenSerializedValue(entry.value);
|
|
1595
|
+
}
|
|
1596
|
+
return result;
|
|
1597
|
+
}
|
|
1598
|
+
case "array": {
|
|
1599
|
+
if (!Array.isArray(v.items)) return [];
|
|
1600
|
+
return v.items.map(flattenSerializedValue);
|
|
1601
|
+
}
|
|
1602
|
+
case "function":
|
|
1603
|
+
return "[Function]";
|
|
1604
|
+
case "symbol":
|
|
1605
|
+
return `[Symbol: ${v.description ?? ""}]`;
|
|
1606
|
+
case "circular":
|
|
1607
|
+
return "[Circular]";
|
|
1608
|
+
case "truncated":
|
|
1609
|
+
return `[Truncated: ${v.preview ?? ""}]`;
|
|
1610
|
+
default:
|
|
1611
|
+
return v.preview ?? null;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function flattenHookState(hooks) {
|
|
1615
|
+
const result = {};
|
|
1616
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1617
|
+
const hook = hooks[i];
|
|
1618
|
+
if (hook === void 0) continue;
|
|
1619
|
+
const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
|
|
1620
|
+
result[key] = flattenSerializedValue(hook.value);
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
function extractContextNames(contexts) {
|
|
1625
|
+
const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1626
|
+
return names;
|
|
1627
|
+
}
|
|
1628
|
+
function anyContextChanged(contexts) {
|
|
1629
|
+
return contexts.some((c) => c.didTriggerRender);
|
|
1630
|
+
}
|
|
1631
|
+
function convertToInstrumentNode(node, depth = 0) {
|
|
1632
|
+
const contexts = extractContextNames(node.context);
|
|
1633
|
+
const contextChanged = anyContextChanged(node.context);
|
|
1634
|
+
const state = flattenHookState(node.state);
|
|
1635
|
+
const propsFlat = flattenSerializedValue(node.props);
|
|
1636
|
+
const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
|
|
1637
|
+
return {
|
|
1638
|
+
component: node.name,
|
|
1639
|
+
type: mapNodeType(node),
|
|
1640
|
+
renderCount: node.renderCount,
|
|
1641
|
+
lastRenderDuration: node.renderDuration,
|
|
1642
|
+
memoized: node.type === "memo",
|
|
1643
|
+
// memoSkipped requires tracking bail-outs across commits — not available from
|
|
1644
|
+
// a single-shot capture. Defaulted to 0.
|
|
1645
|
+
memoSkipped: 0,
|
|
1646
|
+
props,
|
|
1647
|
+
// propsChanged is not tracked in a single-shot capture — would need a diff
|
|
1648
|
+
// between two renders. Defaulted to false.
|
|
1649
|
+
propsChanged: false,
|
|
1650
|
+
state,
|
|
1651
|
+
stateChanged: false,
|
|
1652
|
+
contextChanged,
|
|
1653
|
+
contexts,
|
|
1654
|
+
depth,
|
|
1655
|
+
children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function filterByContext(node, contextName) {
|
|
1659
|
+
const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
|
|
1660
|
+
const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
|
|
1661
|
+
if (!selfMatches && filteredChildren.length === 0) return null;
|
|
1662
|
+
return { ...node, children: filteredChildren };
|
|
1663
|
+
}
|
|
1664
|
+
function filterWastedRenders(node) {
|
|
1665
|
+
const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
|
|
1666
|
+
const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
|
|
1667
|
+
if (!isWasted && filteredChildren.length === 0) return null;
|
|
1668
|
+
return { ...node, children: filteredChildren };
|
|
1669
|
+
}
|
|
1670
|
+
function sortTree(node, sortBy) {
|
|
1671
|
+
const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
|
|
1672
|
+
if (sortBy === "renderCount") return b.renderCount - a.renderCount;
|
|
1673
|
+
return a.depth - b.depth;
|
|
1674
|
+
});
|
|
1675
|
+
return { ...node, children: sortedChildren };
|
|
1676
|
+
}
|
|
1677
|
+
function annotateProviderDepth(node, providerDepth = 0) {
|
|
1678
|
+
const isProvider = node.type === "context.provider";
|
|
1679
|
+
const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
|
|
1680
|
+
return {
|
|
1681
|
+
...node,
|
|
1682
|
+
_providerDepth: providerDepth,
|
|
1683
|
+
children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function limitNodes(root, limit) {
|
|
1687
|
+
let remaining = limit;
|
|
1688
|
+
const clip = (node) => {
|
|
1689
|
+
if (remaining <= 0) return null;
|
|
1690
|
+
remaining--;
|
|
1691
|
+
const clippedChildren = [];
|
|
1692
|
+
for (const child of node.children) {
|
|
1693
|
+
const clipped = clip(child);
|
|
1694
|
+
if (clipped !== null) clippedChildren.push(clipped);
|
|
1695
|
+
}
|
|
1696
|
+
return { ...node, children: clippedChildren };
|
|
1697
|
+
};
|
|
1698
|
+
return clip(root) ?? root;
|
|
1699
|
+
}
|
|
1700
|
+
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1701
|
+
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
1702
|
+
var VERTICAL = "\u2502 ";
|
|
1703
|
+
var EMPTY = " ";
|
|
1704
|
+
function buildTTYLabel(node, showProviderDepth) {
|
|
1705
|
+
const parts = [node.component];
|
|
1706
|
+
switch (node.type) {
|
|
1707
|
+
case "memo":
|
|
1708
|
+
parts.push("[memo]");
|
|
1709
|
+
break;
|
|
1710
|
+
case "forwardRef":
|
|
1711
|
+
parts.push("[forwardRef]");
|
|
1712
|
+
break;
|
|
1713
|
+
case "class":
|
|
1714
|
+
parts.push("[class]");
|
|
1715
|
+
break;
|
|
1716
|
+
case "context.provider":
|
|
1717
|
+
parts.push("[provider]");
|
|
1718
|
+
break;
|
|
1719
|
+
case "context.consumer":
|
|
1720
|
+
parts.push("[consumer]");
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
if (node.renderCount > 0) {
|
|
1724
|
+
const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
|
|
1725
|
+
parts.push(`(renders:${node.renderCount}${durationStr})`);
|
|
1726
|
+
}
|
|
1727
|
+
if (node.contexts.length > 0) {
|
|
1728
|
+
parts.push(`[ctx:${node.contexts.join(",")}]`);
|
|
1729
|
+
}
|
|
1730
|
+
if (showProviderDepth) {
|
|
1731
|
+
const pd = node._providerDepth;
|
|
1732
|
+
if (pd !== void 0 && pd > 0) {
|
|
1733
|
+
parts.push(`[pd:${pd}]`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return parts.join(" ");
|
|
1737
|
+
}
|
|
1738
|
+
function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
|
|
1739
|
+
if (node.type === "host") {
|
|
1740
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1741
|
+
const child = node.children[i];
|
|
1742
|
+
if (child !== void 0) {
|
|
1743
|
+
renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const connector = isLast ? LAST_BRANCH : BRANCH;
|
|
1749
|
+
lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
|
|
1750
|
+
const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
|
|
1751
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1752
|
+
const child = node.children[i];
|
|
1753
|
+
if (child !== void 0) {
|
|
1754
|
+
renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function formatInstrumentTree(root, showProviderDepth = false) {
|
|
1759
|
+
const lines = [];
|
|
1760
|
+
if (root.type !== "host") {
|
|
1761
|
+
lines.push(buildTTYLabel(root, showProviderDepth));
|
|
1762
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1763
|
+
const child = root.children[i];
|
|
1764
|
+
if (child !== void 0) {
|
|
1765
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1770
|
+
const child = root.children[i];
|
|
1771
|
+
if (child !== void 0) {
|
|
1772
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return lines.join("\n");
|
|
1777
|
+
}
|
|
1778
|
+
async function runInstrumentTree(options) {
|
|
1779
|
+
const { componentName, filePath } = options;
|
|
1780
|
+
const pool = await getPool();
|
|
1781
|
+
const slot = await pool.acquire();
|
|
1782
|
+
const { page } = slot;
|
|
1783
|
+
try {
|
|
1784
|
+
await page.addInitScript({ content: playwright.getBrowserEntryScript() });
|
|
1785
|
+
const htmlHarness = await buildComponentHarness(
|
|
1786
|
+
filePath,
|
|
1787
|
+
componentName,
|
|
1788
|
+
{},
|
|
1789
|
+
DEFAULT_VIEWPORT_WIDTH
|
|
1790
|
+
);
|
|
1791
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1792
|
+
await page.waitForFunction(
|
|
1793
|
+
() => {
|
|
1794
|
+
const w = window;
|
|
1795
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1796
|
+
},
|
|
1797
|
+
{ timeout: 15e3 }
|
|
1798
|
+
);
|
|
1799
|
+
const renderError = await page.evaluate(
|
|
1800
|
+
() => window.__SCOPE_RENDER_ERROR__ ?? null
|
|
1801
|
+
);
|
|
1802
|
+
if (renderError !== null) {
|
|
1803
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1804
|
+
}
|
|
1805
|
+
const captureJson = await page.evaluate(async () => {
|
|
1806
|
+
const w = window;
|
|
1807
|
+
if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
|
|
1808
|
+
throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
|
|
1809
|
+
}
|
|
1810
|
+
return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
|
|
1811
|
+
});
|
|
1812
|
+
const captureResult = JSON.parse(captureJson);
|
|
1813
|
+
const componentTree = captureResult.tree;
|
|
1814
|
+
if (componentTree === void 0 || componentTree === null) {
|
|
1815
|
+
throw new Error(`No component tree found for "${componentName}"`);
|
|
1816
|
+
}
|
|
1817
|
+
let instrumentRoot = convertToInstrumentNode(componentTree, 0);
|
|
1818
|
+
if (options.usesContext !== void 0) {
|
|
1819
|
+
const filtered = filterByContext(instrumentRoot, options.usesContext);
|
|
1820
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1821
|
+
}
|
|
1822
|
+
if (options.wastedRenders === true) {
|
|
1823
|
+
const filtered = filterWastedRenders(instrumentRoot);
|
|
1824
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1825
|
+
}
|
|
1826
|
+
if (options.sortBy !== void 0) {
|
|
1827
|
+
instrumentRoot = sortTree(instrumentRoot, options.sortBy);
|
|
1828
|
+
}
|
|
1829
|
+
if (options.providerDepth === true) {
|
|
1830
|
+
instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
|
|
1831
|
+
}
|
|
1832
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
1833
|
+
instrumentRoot = limitNodes(instrumentRoot, options.limit);
|
|
1834
|
+
}
|
|
1835
|
+
return instrumentRoot;
|
|
1836
|
+
} finally {
|
|
1837
|
+
pool.release(slot);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function createInstrumentTreeCommand() {
|
|
1841
|
+
return new commander.Command("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
1842
|
+
"--wasted-renders",
|
|
1843
|
+
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1844
|
+
false
|
|
1845
|
+
).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
|
|
1846
|
+
try {
|
|
1847
|
+
const manifest = loadManifest(opts.manifest);
|
|
1848
|
+
const descriptor = manifest.components[componentName];
|
|
1849
|
+
if (descriptor === void 0) {
|
|
1850
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`Component "${componentName}" not found in manifest.
|
|
1853
|
+
Available: ${available}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
if (opts.sortBy !== void 0) {
|
|
1857
|
+
const allowed = ["renderCount", "depth"];
|
|
1858
|
+
if (!allowed.includes(opts.sortBy)) {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const rootDir = process.cwd();
|
|
1865
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1866
|
+
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1867
|
+
`);
|
|
1868
|
+
const instrumentRoot = await runInstrumentTree({
|
|
1869
|
+
componentName,
|
|
1870
|
+
filePath,
|
|
1871
|
+
sortBy: opts.sortBy,
|
|
1872
|
+
limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
|
|
1873
|
+
usesContext: opts.usesContext,
|
|
1874
|
+
providerDepth: opts.providerDepth,
|
|
1875
|
+
wastedRenders: opts.wastedRenders
|
|
1876
|
+
});
|
|
1877
|
+
await shutdownPool();
|
|
1878
|
+
const fmt = resolveFormat2(opts.format);
|
|
1879
|
+
if (fmt === "json") {
|
|
1880
|
+
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1881
|
+
`);
|
|
1882
|
+
} else {
|
|
1883
|
+
const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
|
|
1884
|
+
process.stdout.write(`${tree}
|
|
1885
|
+
`);
|
|
1886
|
+
}
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
await shutdownPool();
|
|
1889
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1890
|
+
`);
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
function resolveFormat2(formatFlag) {
|
|
1896
|
+
if (formatFlag !== void 0) {
|
|
1897
|
+
const lower = formatFlag.toLowerCase();
|
|
1898
|
+
if (lower !== "json" && lower !== "tree") {
|
|
1899
|
+
throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
|
|
1900
|
+
}
|
|
1901
|
+
return lower;
|
|
1902
|
+
}
|
|
1903
|
+
return isTTY() ? "tree" : "json";
|
|
1904
|
+
}
|
|
1550
1905
|
|
|
1551
1906
|
// src/instrument/renders.ts
|
|
1552
|
-
var
|
|
1907
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1553
1908
|
function determineTrigger(event) {
|
|
1554
1909
|
if (event.forceUpdate) return "force_update";
|
|
1555
1910
|
if (event.stateChanged) return "state_change";
|
|
@@ -1848,26 +2203,26 @@ async function replayInteraction2(page, steps) {
|
|
|
1848
2203
|
}
|
|
1849
2204
|
}
|
|
1850
2205
|
}
|
|
1851
|
-
var
|
|
1852
|
-
async function
|
|
1853
|
-
if (
|
|
1854
|
-
|
|
2206
|
+
var _pool2 = null;
|
|
2207
|
+
async function getPool2() {
|
|
2208
|
+
if (_pool2 === null) {
|
|
2209
|
+
_pool2 = new render.BrowserPool({
|
|
1855
2210
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1856
2211
|
viewportWidth: 1280,
|
|
1857
2212
|
viewportHeight: 800
|
|
1858
2213
|
});
|
|
1859
|
-
await
|
|
2214
|
+
await _pool2.init();
|
|
1860
2215
|
}
|
|
1861
|
-
return
|
|
2216
|
+
return _pool2;
|
|
1862
2217
|
}
|
|
1863
|
-
async function
|
|
1864
|
-
if (
|
|
1865
|
-
await
|
|
1866
|
-
|
|
2218
|
+
async function shutdownPool2() {
|
|
2219
|
+
if (_pool2 !== null) {
|
|
2220
|
+
await _pool2.close();
|
|
2221
|
+
_pool2 = null;
|
|
1867
2222
|
}
|
|
1868
2223
|
}
|
|
1869
2224
|
async function analyzeRenders(options) {
|
|
1870
|
-
const manifestPath = options.manifestPath ??
|
|
2225
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
|
|
1871
2226
|
const manifest = loadManifest(manifestPath);
|
|
1872
2227
|
const descriptor = manifest.components[options.componentName];
|
|
1873
2228
|
if (descriptor === void 0) {
|
|
@@ -1880,7 +2235,7 @@ Available: ${available}`
|
|
|
1880
2235
|
const rootDir = process.cwd();
|
|
1881
2236
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
1882
2237
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1883
|
-
const pool = await
|
|
2238
|
+
const pool = await getPool2();
|
|
1884
2239
|
const slot = await pool.acquire();
|
|
1885
2240
|
const { page } = slot;
|
|
1886
2241
|
const startMs = performance.now();
|
|
@@ -1965,7 +2320,7 @@ function createInstrumentRendersCommand() {
|
|
|
1965
2320
|
"--interaction <json>",
|
|
1966
2321
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1967
2322
|
"[]"
|
|
1968
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2323
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1969
2324
|
async (componentName, opts) => {
|
|
1970
2325
|
let interaction = [];
|
|
1971
2326
|
try {
|
|
@@ -1988,7 +2343,7 @@ function createInstrumentRendersCommand() {
|
|
|
1988
2343
|
interaction,
|
|
1989
2344
|
manifestPath: opts.manifest
|
|
1990
2345
|
});
|
|
1991
|
-
await
|
|
2346
|
+
await shutdownPool2();
|
|
1992
2347
|
if (opts.json || !isTTY()) {
|
|
1993
2348
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1994
2349
|
`);
|
|
@@ -1997,7 +2352,7 @@ function createInstrumentRendersCommand() {
|
|
|
1997
2352
|
`);
|
|
1998
2353
|
}
|
|
1999
2354
|
} catch (err) {
|
|
2000
|
-
await
|
|
2355
|
+
await shutdownPool2();
|
|
2001
2356
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2002
2357
|
`);
|
|
2003
2358
|
process.exit(1);
|
|
@@ -2012,6 +2367,7 @@ function createInstrumentCommand() {
|
|
|
2012
2367
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2013
2368
|
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2014
2369
|
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
2370
|
+
instrumentCmd.addCommand(createInstrumentTreeCommand());
|
|
2015
2371
|
return instrumentCmd;
|
|
2016
2372
|
}
|
|
2017
2373
|
async function browserCapture(options) {
|
|
@@ -2171,24 +2527,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
2171
2527
|
}
|
|
2172
2528
|
|
|
2173
2529
|
// src/render-commands.ts
|
|
2174
|
-
var
|
|
2530
|
+
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2175
2531
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2176
|
-
var
|
|
2177
|
-
async function
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2532
|
+
var _pool3 = null;
|
|
2533
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2534
|
+
if (_pool3 === null) {
|
|
2535
|
+
_pool3 = new render.BrowserPool({
|
|
2180
2536
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2181
2537
|
viewportWidth,
|
|
2182
2538
|
viewportHeight
|
|
2183
2539
|
});
|
|
2184
|
-
await
|
|
2540
|
+
await _pool3.init();
|
|
2185
2541
|
}
|
|
2186
|
-
return
|
|
2542
|
+
return _pool3;
|
|
2187
2543
|
}
|
|
2188
|
-
async function
|
|
2189
|
-
if (
|
|
2190
|
-
await
|
|
2191
|
-
|
|
2544
|
+
async function shutdownPool3() {
|
|
2545
|
+
if (_pool3 !== null) {
|
|
2546
|
+
await _pool3.close();
|
|
2547
|
+
_pool3 = null;
|
|
2192
2548
|
}
|
|
2193
2549
|
}
|
|
2194
2550
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2199,7 +2555,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2199
2555
|
_satori: satori,
|
|
2200
2556
|
async renderCell(props, _complexityClass) {
|
|
2201
2557
|
const startMs = performance.now();
|
|
2202
|
-
const pool = await
|
|
2558
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2203
2559
|
const htmlHarness = await buildComponentHarness(
|
|
2204
2560
|
filePath,
|
|
2205
2561
|
componentName,
|
|
@@ -2296,7 +2652,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2296
2652
|
};
|
|
2297
2653
|
}
|
|
2298
2654
|
function registerRenderSingle(renderCmd) {
|
|
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",
|
|
2655
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
2300
2656
|
async (componentName, opts) => {
|
|
2301
2657
|
try {
|
|
2302
2658
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2335,7 +2691,7 @@ Available: ${available}`
|
|
|
2335
2691
|
}
|
|
2336
2692
|
}
|
|
2337
2693
|
);
|
|
2338
|
-
await
|
|
2694
|
+
await shutdownPool3();
|
|
2339
2695
|
if (outcome.crashed) {
|
|
2340
2696
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2341
2697
|
`);
|
|
@@ -2383,7 +2739,7 @@ Available: ${available}`
|
|
|
2383
2739
|
);
|
|
2384
2740
|
}
|
|
2385
2741
|
} catch (err) {
|
|
2386
|
-
await
|
|
2742
|
+
await shutdownPool3();
|
|
2387
2743
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2388
2744
|
`);
|
|
2389
2745
|
process.exit(1);
|
|
@@ -2395,7 +2751,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
2395
2751
|
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
|
|
2396
2752
|
"--contexts <ids>",
|
|
2397
2753
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
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",
|
|
2754
|
+
).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
2399
2755
|
async (componentName, opts) => {
|
|
2400
2756
|
try {
|
|
2401
2757
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2468,7 +2824,7 @@ Available: ${available}`
|
|
|
2468
2824
|
concurrency
|
|
2469
2825
|
});
|
|
2470
2826
|
const result = await matrix.render();
|
|
2471
|
-
await
|
|
2827
|
+
await shutdownPool3();
|
|
2472
2828
|
process.stderr.write(
|
|
2473
2829
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2474
2830
|
`
|
|
@@ -2513,7 +2869,7 @@ Available: ${available}`
|
|
|
2513
2869
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2514
2870
|
}
|
|
2515
2871
|
} catch (err) {
|
|
2516
|
-
await
|
|
2872
|
+
await shutdownPool3();
|
|
2517
2873
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2518
2874
|
`);
|
|
2519
2875
|
process.exit(1);
|
|
@@ -2522,7 +2878,7 @@ Available: ${available}`
|
|
|
2522
2878
|
);
|
|
2523
2879
|
}
|
|
2524
2880
|
function registerRenderAll(renderCmd) {
|
|
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",
|
|
2881
|
+
renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
|
|
2526
2882
|
async (opts) => {
|
|
2527
2883
|
try {
|
|
2528
2884
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2610,13 +2966,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2610
2966
|
workers.push(worker());
|
|
2611
2967
|
}
|
|
2612
2968
|
await Promise.all(workers);
|
|
2613
|
-
await
|
|
2969
|
+
await shutdownPool3();
|
|
2614
2970
|
process.stderr.write("\n");
|
|
2615
2971
|
const summary = formatSummaryText(results, outputDir);
|
|
2616
2972
|
process.stderr.write(`${summary}
|
|
2617
2973
|
`);
|
|
2618
2974
|
} catch (err) {
|
|
2619
|
-
await
|
|
2975
|
+
await shutdownPool3();
|
|
2620
2976
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2621
2977
|
`);
|
|
2622
2978
|
process.exit(1);
|
|
@@ -2658,26 +3014,26 @@ function createRenderCommand() {
|
|
|
2658
3014
|
return renderCmd;
|
|
2659
3015
|
}
|
|
2660
3016
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2661
|
-
var
|
|
2662
|
-
async function
|
|
2663
|
-
if (
|
|
2664
|
-
|
|
3017
|
+
var _pool4 = null;
|
|
3018
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
3019
|
+
if (_pool4 === null) {
|
|
3020
|
+
_pool4 = new render.BrowserPool({
|
|
2665
3021
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2666
3022
|
viewportWidth,
|
|
2667
3023
|
viewportHeight
|
|
2668
3024
|
});
|
|
2669
|
-
await
|
|
3025
|
+
await _pool4.init();
|
|
2670
3026
|
}
|
|
2671
|
-
return
|
|
3027
|
+
return _pool4;
|
|
2672
3028
|
}
|
|
2673
|
-
async function
|
|
2674
|
-
if (
|
|
2675
|
-
await
|
|
2676
|
-
|
|
3029
|
+
async function shutdownPool4() {
|
|
3030
|
+
if (_pool4 !== null) {
|
|
3031
|
+
await _pool4.close();
|
|
3032
|
+
_pool4 = null;
|
|
2677
3033
|
}
|
|
2678
3034
|
}
|
|
2679
3035
|
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2680
|
-
const pool = await
|
|
3036
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2681
3037
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2682
3038
|
const slot = await pool.acquire();
|
|
2683
3039
|
const { page } = slot;
|
|
@@ -2810,12 +3166,12 @@ async function runBaseline(options = {}) {
|
|
|
2810
3166
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
2811
3167
|
let manifest$1;
|
|
2812
3168
|
if (manifestPath !== void 0) {
|
|
2813
|
-
const { readFileSync:
|
|
3169
|
+
const { readFileSync: readFileSync9 } = await import('fs');
|
|
2814
3170
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
2815
3171
|
if (!fs.existsSync(absPath)) {
|
|
2816
3172
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2817
3173
|
}
|
|
2818
|
-
manifest$1 = JSON.parse(
|
|
3174
|
+
manifest$1 = JSON.parse(readFileSync9(absPath, "utf-8"));
|
|
2819
3175
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2820
3176
|
`);
|
|
2821
3177
|
} else {
|
|
@@ -2927,7 +3283,7 @@ async function runBaseline(options = {}) {
|
|
|
2927
3283
|
workers.push(worker());
|
|
2928
3284
|
}
|
|
2929
3285
|
await Promise.all(workers);
|
|
2930
|
-
await
|
|
3286
|
+
await shutdownPool4();
|
|
2931
3287
|
if (isTTY()) {
|
|
2932
3288
|
process.stderr.write("\n");
|
|
2933
3289
|
}
|
|
@@ -2976,12 +3332,485 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
2976
3332
|
}
|
|
2977
3333
|
);
|
|
2978
3334
|
}
|
|
3335
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3336
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3337
|
+
const compliancePath = path.resolve(baselineDir, "compliance.json");
|
|
3338
|
+
if (!fs.existsSync(compliancePath)) return null;
|
|
3339
|
+
const raw = JSON.parse(fs.readFileSync(compliancePath, "utf-8"));
|
|
3340
|
+
return raw;
|
|
3341
|
+
}
|
|
3342
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3343
|
+
const jsonPath = path.resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3344
|
+
if (!fs.existsSync(jsonPath)) return null;
|
|
3345
|
+
return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
3346
|
+
}
|
|
3347
|
+
var _pool5 = null;
|
|
3348
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3349
|
+
if (_pool5 === null) {
|
|
3350
|
+
_pool5 = new render.BrowserPool({
|
|
3351
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3352
|
+
viewportWidth,
|
|
3353
|
+
viewportHeight
|
|
3354
|
+
});
|
|
3355
|
+
await _pool5.init();
|
|
3356
|
+
}
|
|
3357
|
+
return _pool5;
|
|
3358
|
+
}
|
|
3359
|
+
async function shutdownPool5() {
|
|
3360
|
+
if (_pool5 !== null) {
|
|
3361
|
+
await _pool5.close();
|
|
3362
|
+
_pool5 = null;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3366
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3367
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3368
|
+
const slot = await pool.acquire();
|
|
3369
|
+
const { page } = slot;
|
|
3370
|
+
try {
|
|
3371
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3372
|
+
await page.waitForFunction(
|
|
3373
|
+
() => {
|
|
3374
|
+
const w = window;
|
|
3375
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3376
|
+
},
|
|
3377
|
+
{ timeout: 15e3 }
|
|
3378
|
+
);
|
|
3379
|
+
const renderError = await page.evaluate(() => {
|
|
3380
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3381
|
+
});
|
|
3382
|
+
if (renderError !== null) {
|
|
3383
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3384
|
+
}
|
|
3385
|
+
const rootDir = process.cwd();
|
|
3386
|
+
const classes = await page.evaluate(() => {
|
|
3387
|
+
const set = /* @__PURE__ */ new Set();
|
|
3388
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3389
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3390
|
+
if (c) set.add(c);
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
return [...set];
|
|
3394
|
+
});
|
|
3395
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3396
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3397
|
+
await page.addStyleTag({ content: projectCss });
|
|
3398
|
+
}
|
|
3399
|
+
const startMs = performance.now();
|
|
3400
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3401
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3402
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3403
|
+
throw new Error(
|
|
3404
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
const PAD = 24;
|
|
3408
|
+
const MIN_W = 320;
|
|
3409
|
+
const MIN_H = 200;
|
|
3410
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3411
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3412
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3413
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3414
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3415
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3416
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3417
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3418
|
+
const screenshot = await page.screenshot({
|
|
3419
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3420
|
+
type: "png"
|
|
3421
|
+
});
|
|
3422
|
+
const computedStylesRaw = {};
|
|
3423
|
+
const styles = await page.evaluate((sel) => {
|
|
3424
|
+
const el = document.querySelector(sel);
|
|
3425
|
+
if (el === null) return {};
|
|
3426
|
+
const computed = window.getComputedStyle(el);
|
|
3427
|
+
const out = {};
|
|
3428
|
+
for (const prop of [
|
|
3429
|
+
"display",
|
|
3430
|
+
"width",
|
|
3431
|
+
"height",
|
|
3432
|
+
"color",
|
|
3433
|
+
"backgroundColor",
|
|
3434
|
+
"fontSize",
|
|
3435
|
+
"fontFamily",
|
|
3436
|
+
"padding",
|
|
3437
|
+
"margin"
|
|
3438
|
+
]) {
|
|
3439
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3440
|
+
}
|
|
3441
|
+
return out;
|
|
3442
|
+
}, "[data-reactscope-root] > *");
|
|
3443
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3444
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3445
|
+
return {
|
|
3446
|
+
screenshot,
|
|
3447
|
+
width: Math.round(safeW),
|
|
3448
|
+
height: Math.round(safeH),
|
|
3449
|
+
renderTimeMs,
|
|
3450
|
+
computedStyles: computedStylesRaw
|
|
3451
|
+
};
|
|
3452
|
+
} finally {
|
|
3453
|
+
pool.release(slot);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3457
|
+
const flat = {};
|
|
3458
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3459
|
+
Object.assign(flat, styles);
|
|
3460
|
+
}
|
|
3461
|
+
const colors = {};
|
|
3462
|
+
const spacing = {};
|
|
3463
|
+
const typography = {};
|
|
3464
|
+
const borders = {};
|
|
3465
|
+
const shadows = {};
|
|
3466
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3467
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3468
|
+
colors[prop] = value;
|
|
3469
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3470
|
+
spacing[prop] = value;
|
|
3471
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3472
|
+
typography[prop] = value;
|
|
3473
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3474
|
+
borders[prop] = value;
|
|
3475
|
+
} else if (prop === "boxShadow") {
|
|
3476
|
+
shadows[prop] = value;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3480
|
+
}
|
|
3481
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3482
|
+
if (entry.renderFailed) return "unchanged";
|
|
3483
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3484
|
+
return "added";
|
|
3485
|
+
}
|
|
3486
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3487
|
+
return "removed";
|
|
3488
|
+
}
|
|
3489
|
+
const delta = entry.complianceDelta;
|
|
3490
|
+
if (delta !== null) {
|
|
3491
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3492
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3493
|
+
}
|
|
3494
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3495
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3496
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3497
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3498
|
+
}
|
|
3499
|
+
return "unchanged";
|
|
3500
|
+
}
|
|
3501
|
+
async function runDiff(options = {}) {
|
|
3502
|
+
const {
|
|
3503
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3504
|
+
componentsGlob,
|
|
3505
|
+
manifestPath,
|
|
3506
|
+
viewportWidth = 375,
|
|
3507
|
+
viewportHeight = 812,
|
|
3508
|
+
regressionThreshold = 0.01
|
|
3509
|
+
} = options;
|
|
3510
|
+
const startTime = performance.now();
|
|
3511
|
+
const rootDir = process.cwd();
|
|
3512
|
+
const baselineDir = path.resolve(rootDir, baselineDirRaw);
|
|
3513
|
+
if (!fs.existsSync(baselineDir)) {
|
|
3514
|
+
throw new Error(
|
|
3515
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
const baselineManifestPath = path.resolve(baselineDir, "manifest.json");
|
|
3519
|
+
if (!fs.existsSync(baselineManifestPath)) {
|
|
3520
|
+
throw new Error(
|
|
3521
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
const baselineManifest = JSON.parse(fs.readFileSync(baselineManifestPath, "utf-8"));
|
|
3525
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3526
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3527
|
+
process.stderr.write(
|
|
3528
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3529
|
+
`
|
|
3530
|
+
);
|
|
3531
|
+
let currentManifest;
|
|
3532
|
+
if (manifestPath !== void 0) {
|
|
3533
|
+
const absPath = path.resolve(rootDir, manifestPath);
|
|
3534
|
+
if (!fs.existsSync(absPath)) {
|
|
3535
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3536
|
+
}
|
|
3537
|
+
currentManifest = JSON.parse(fs.readFileSync(absPath, "utf-8"));
|
|
3538
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3539
|
+
`);
|
|
3540
|
+
} else {
|
|
3541
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3542
|
+
currentManifest = await manifest.generateManifest({ rootDir });
|
|
3543
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3544
|
+
process.stderr.write(`Found ${count} components.
|
|
3545
|
+
`);
|
|
3546
|
+
}
|
|
3547
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3548
|
+
if (componentsGlob !== void 0) {
|
|
3549
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3550
|
+
process.stderr.write(
|
|
3551
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3552
|
+
`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3556
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3557
|
+
);
|
|
3558
|
+
const total = componentNames.length;
|
|
3559
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3560
|
+
`);
|
|
3561
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3562
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3563
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3564
|
+
let completed = 0;
|
|
3565
|
+
const CONCURRENCY = 4;
|
|
3566
|
+
let nextIdx = 0;
|
|
3567
|
+
const renderOne = async (name) => {
|
|
3568
|
+
const descriptor = currentManifest.components[name];
|
|
3569
|
+
if (descriptor === void 0) return;
|
|
3570
|
+
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3571
|
+
const outcome = await render.safeRender(
|
|
3572
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3573
|
+
{
|
|
3574
|
+
props: {},
|
|
3575
|
+
sourceLocation: {
|
|
3576
|
+
file: descriptor.filePath,
|
|
3577
|
+
line: descriptor.loc.start,
|
|
3578
|
+
column: 0
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
);
|
|
3582
|
+
completed++;
|
|
3583
|
+
const pct = Math.round(completed / total * 100);
|
|
3584
|
+
if (isTTY()) {
|
|
3585
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3586
|
+
}
|
|
3587
|
+
if (outcome.crashed) {
|
|
3588
|
+
renderFailures.add(name);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
const result = outcome.result;
|
|
3592
|
+
currentRenderMeta.set(name, {
|
|
3593
|
+
width: result.width,
|
|
3594
|
+
height: result.height,
|
|
3595
|
+
renderTimeMs: result.renderTimeMs
|
|
3596
|
+
});
|
|
3597
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3598
|
+
};
|
|
3599
|
+
if (total > 0) {
|
|
3600
|
+
const worker = async () => {
|
|
3601
|
+
while (nextIdx < componentNames.length) {
|
|
3602
|
+
const i = nextIdx++;
|
|
3603
|
+
const name = componentNames[i];
|
|
3604
|
+
if (name !== void 0) {
|
|
3605
|
+
await renderOne(name);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
const workers = [];
|
|
3610
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3611
|
+
workers.push(worker());
|
|
3612
|
+
}
|
|
3613
|
+
await Promise.all(workers);
|
|
3614
|
+
}
|
|
3615
|
+
await shutdownPool5();
|
|
3616
|
+
if (isTTY() && total > 0) {
|
|
3617
|
+
process.stderr.write("\n");
|
|
3618
|
+
}
|
|
3619
|
+
const resolver = new tokens.TokenResolver([]);
|
|
3620
|
+
const engine = new tokens.ComplianceEngine(resolver);
|
|
3621
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3622
|
+
const entries = [];
|
|
3623
|
+
for (const name of componentNames) {
|
|
3624
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3625
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3626
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3627
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3628
|
+
const failed = renderFailures.has(name);
|
|
3629
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3630
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3631
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3632
|
+
const partial = {
|
|
3633
|
+
name,
|
|
3634
|
+
baselineCompliance: baselineComplianceScore,
|
|
3635
|
+
currentCompliance: currentComplianceScore,
|
|
3636
|
+
complianceDelta: delta,
|
|
3637
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3638
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3639
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3640
|
+
renderFailed: failed
|
|
3641
|
+
};
|
|
3642
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3643
|
+
}
|
|
3644
|
+
for (const name of removedNames) {
|
|
3645
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3646
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3647
|
+
entries.push({
|
|
3648
|
+
name,
|
|
3649
|
+
status: "removed",
|
|
3650
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3651
|
+
currentCompliance: null,
|
|
3652
|
+
complianceDelta: null,
|
|
3653
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3654
|
+
currentDimensions: null,
|
|
3655
|
+
renderTimeMs: null,
|
|
3656
|
+
renderFailed: false
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
const summary = {
|
|
3660
|
+
total: entries.length,
|
|
3661
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3662
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3663
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3664
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3665
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3666
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3667
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3668
|
+
};
|
|
3669
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3670
|
+
const wallClockMs = performance.now() - startTime;
|
|
3671
|
+
return {
|
|
3672
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3673
|
+
baselineDir,
|
|
3674
|
+
summary,
|
|
3675
|
+
components: entries,
|
|
3676
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3677
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3678
|
+
hasRegressions,
|
|
3679
|
+
wallClockMs
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
var STATUS_ICON = {
|
|
3683
|
+
added: "+",
|
|
3684
|
+
removed: "-",
|
|
3685
|
+
unchanged: " ",
|
|
3686
|
+
compliance_regressed: "\u2193",
|
|
3687
|
+
compliance_improved: "\u2191",
|
|
3688
|
+
size_changed: "~"
|
|
3689
|
+
};
|
|
3690
|
+
var STATUS_LABEL = {
|
|
3691
|
+
added: "added",
|
|
3692
|
+
removed: "removed",
|
|
3693
|
+
unchanged: "ok",
|
|
3694
|
+
compliance_regressed: "regressed",
|
|
3695
|
+
compliance_improved: "improved",
|
|
3696
|
+
size_changed: "size changed"
|
|
3697
|
+
};
|
|
3698
|
+
function formatDiffReport(result) {
|
|
3699
|
+
const lines = [];
|
|
3700
|
+
const title = "Scope Report Diff";
|
|
3701
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3702
|
+
lines.push(title, rule2);
|
|
3703
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3704
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3705
|
+
lines.push(
|
|
3706
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3707
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3708
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3709
|
+
""
|
|
3710
|
+
);
|
|
3711
|
+
const s = result.summary;
|
|
3712
|
+
lines.push(
|
|
3713
|
+
`Components: ${s.total} total ` + [
|
|
3714
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3715
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3716
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3717
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3718
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3719
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3720
|
+
].filter(Boolean).join(" "),
|
|
3721
|
+
""
|
|
3722
|
+
);
|
|
3723
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3724
|
+
if (notable.length === 0) {
|
|
3725
|
+
lines.push(" No changes detected.");
|
|
3726
|
+
} else {
|
|
3727
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3728
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3729
|
+
const divider = "-".repeat(header.length);
|
|
3730
|
+
lines.push(header, divider);
|
|
3731
|
+
for (const entry of notable) {
|
|
3732
|
+
const icon = STATUS_ICON[entry.status];
|
|
3733
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3734
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3735
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3736
|
+
if (entry.complianceDelta !== null) {
|
|
3737
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3738
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3739
|
+
}
|
|
3740
|
+
let dimStr = "\u2014";
|
|
3741
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3742
|
+
const b = entry.baselineDimensions;
|
|
3743
|
+
const c = entry.currentDimensions;
|
|
3744
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3745
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3746
|
+
} else {
|
|
3747
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3748
|
+
}
|
|
3749
|
+
} else if (entry.currentDimensions !== null) {
|
|
3750
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3751
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3752
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3753
|
+
}
|
|
3754
|
+
if (entry.renderFailed) {
|
|
3755
|
+
dimStr = "render failed";
|
|
3756
|
+
}
|
|
3757
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
lines.push(
|
|
3761
|
+
"",
|
|
3762
|
+
rule2,
|
|
3763
|
+
result.hasRegressions ? `Diff complete: ${result.summary.complianceRegressed + result.summary.renderFailed} regression(s) detected in ${(result.wallClockMs / 1e3).toFixed(1)}s` : `Diff complete: no regressions in ${(result.wallClockMs / 1e3).toFixed(1)}s`
|
|
3764
|
+
);
|
|
3765
|
+
return lines.join("\n");
|
|
3766
|
+
}
|
|
3767
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3768
|
+
reportCmd.command("diff").description("Compare the current component library against a saved baseline snapshot").option("-b, --baseline <dir>", "Baseline directory to compare against", DEFAULT_BASELINE_DIR2).option("--components <glob>", "Glob pattern to diff 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").option("--json", "Output diff as JSON instead of human-readable text", false).option("-o, --output <path>", "Write the diff JSON to a file").option(
|
|
3769
|
+
"--regression-threshold <n>",
|
|
3770
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3771
|
+
"0.01"
|
|
3772
|
+
).action(
|
|
3773
|
+
async (opts) => {
|
|
3774
|
+
try {
|
|
3775
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3776
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3777
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3778
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3779
|
+
const result = await runDiff({
|
|
3780
|
+
baselineDir: opts.baseline,
|
|
3781
|
+
componentsGlob: opts.components,
|
|
3782
|
+
manifestPath: opts.manifest,
|
|
3783
|
+
viewportWidth,
|
|
3784
|
+
viewportHeight,
|
|
3785
|
+
regressionThreshold
|
|
3786
|
+
});
|
|
3787
|
+
if (opts.output !== void 0) {
|
|
3788
|
+
fs.writeFileSync(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3789
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3790
|
+
`);
|
|
3791
|
+
}
|
|
3792
|
+
if (opts.json) {
|
|
3793
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3794
|
+
`);
|
|
3795
|
+
} else {
|
|
3796
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3797
|
+
`);
|
|
3798
|
+
}
|
|
3799
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3800
|
+
} catch (err) {
|
|
3801
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3802
|
+
`);
|
|
3803
|
+
process.exit(2);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
2979
3808
|
|
|
2980
3809
|
// src/tree-formatter.ts
|
|
2981
|
-
var
|
|
2982
|
-
var
|
|
2983
|
-
var
|
|
2984
|
-
var
|
|
3810
|
+
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3811
|
+
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
3812
|
+
var VERTICAL2 = "\u2502 ";
|
|
3813
|
+
var EMPTY2 = " ";
|
|
2985
3814
|
function buildLabel(node, options) {
|
|
2986
3815
|
const parts = [node.name];
|
|
2987
3816
|
if (node.type === "memo") {
|
|
@@ -3028,19 +3857,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
|
|
|
3028
3857
|
}
|
|
3029
3858
|
return;
|
|
3030
3859
|
}
|
|
3031
|
-
const connector = isLast ?
|
|
3860
|
+
const connector = isLast ? LAST_BRANCH2 : BRANCH2;
|
|
3032
3861
|
const label = buildLabel(node, options);
|
|
3033
3862
|
lines.push(`${prefix}${connector}${label}`);
|
|
3034
3863
|
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
3035
3864
|
const childCount = countVisibleDescendants(node, options);
|
|
3036
3865
|
if (childCount > 0) {
|
|
3037
|
-
const nextPrefix2 = prefix + (isLast ?
|
|
3038
|
-
lines.push(`${nextPrefix2}${
|
|
3866
|
+
const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3867
|
+
lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3039
3868
|
}
|
|
3040
3869
|
return;
|
|
3041
3870
|
}
|
|
3042
3871
|
const visibleChildren = getVisibleChildren(node, options);
|
|
3043
|
-
const nextPrefix = prefix + (isLast ?
|
|
3872
|
+
const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3044
3873
|
for (let i = 0; i < visibleChildren.length; i++) {
|
|
3045
3874
|
const child = visibleChildren[i];
|
|
3046
3875
|
if (child !== void 0) {
|
|
@@ -3082,7 +3911,7 @@ function formatTree(root, options = {}) {
|
|
|
3082
3911
|
if (options.maxDepth === 0) {
|
|
3083
3912
|
const childCount = countVisibleDescendants(root, options);
|
|
3084
3913
|
if (childCount > 0) {
|
|
3085
|
-
lines.push(`${
|
|
3914
|
+
lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3086
3915
|
}
|
|
3087
3916
|
} else {
|
|
3088
3917
|
const visibleChildren = getVisibleChildren(root, options);
|
|
@@ -3793,6 +4622,7 @@ function createProgram(options = {}) {
|
|
|
3793
4622
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
3794
4623
|
if (existingReportCmd !== void 0) {
|
|
3795
4624
|
registerBaselineSubCommand(existingReportCmd);
|
|
4625
|
+
registerDiffSubCommand(existingReportCmd);
|
|
3796
4626
|
}
|
|
3797
4627
|
return program;
|
|
3798
4628
|
}
|