@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.js
CHANGED
|
@@ -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((resolve12) => {
|
|
209
209
|
rl.question(question, (answer) => {
|
|
210
|
-
|
|
210
|
+
resolve12(answer.trim());
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
}
|
|
@@ -1524,9 +1524,364 @@ Available: ${available}`
|
|
|
1524
1524
|
);
|
|
1525
1525
|
return cmd;
|
|
1526
1526
|
}
|
|
1527
|
+
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1528
|
+
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1529
|
+
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1530
|
+
var _pool = null;
|
|
1531
|
+
async function getPool() {
|
|
1532
|
+
if (_pool === null) {
|
|
1533
|
+
_pool = new BrowserPool({
|
|
1534
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1535
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1536
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1537
|
+
});
|
|
1538
|
+
await _pool.init();
|
|
1539
|
+
}
|
|
1540
|
+
return _pool;
|
|
1541
|
+
}
|
|
1542
|
+
async function shutdownPool() {
|
|
1543
|
+
if (_pool !== null) {
|
|
1544
|
+
await _pool.close();
|
|
1545
|
+
_pool = null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function mapNodeType(node) {
|
|
1549
|
+
if (node.type === "forward_ref") return "forwardRef";
|
|
1550
|
+
if (node.type === "host") return "host";
|
|
1551
|
+
const name = node.name;
|
|
1552
|
+
if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
|
|
1553
|
+
if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
|
|
1554
|
+
return node.type;
|
|
1555
|
+
}
|
|
1556
|
+
function flattenSerializedValue(sv) {
|
|
1557
|
+
if (sv === null || sv === void 0) return null;
|
|
1558
|
+
const v = sv;
|
|
1559
|
+
switch (v.type) {
|
|
1560
|
+
case "null":
|
|
1561
|
+
case "undefined":
|
|
1562
|
+
return null;
|
|
1563
|
+
case "string":
|
|
1564
|
+
case "number":
|
|
1565
|
+
case "boolean":
|
|
1566
|
+
return v.value;
|
|
1567
|
+
case "object": {
|
|
1568
|
+
if (!Array.isArray(v.entries)) return {};
|
|
1569
|
+
const result = {};
|
|
1570
|
+
for (const entry of v.entries) {
|
|
1571
|
+
result[entry.key] = flattenSerializedValue(entry.value);
|
|
1572
|
+
}
|
|
1573
|
+
return result;
|
|
1574
|
+
}
|
|
1575
|
+
case "array": {
|
|
1576
|
+
if (!Array.isArray(v.items)) return [];
|
|
1577
|
+
return v.items.map(flattenSerializedValue);
|
|
1578
|
+
}
|
|
1579
|
+
case "function":
|
|
1580
|
+
return "[Function]";
|
|
1581
|
+
case "symbol":
|
|
1582
|
+
return `[Symbol: ${v.description ?? ""}]`;
|
|
1583
|
+
case "circular":
|
|
1584
|
+
return "[Circular]";
|
|
1585
|
+
case "truncated":
|
|
1586
|
+
return `[Truncated: ${v.preview ?? ""}]`;
|
|
1587
|
+
default:
|
|
1588
|
+
return v.preview ?? null;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function flattenHookState(hooks) {
|
|
1592
|
+
const result = {};
|
|
1593
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1594
|
+
const hook = hooks[i];
|
|
1595
|
+
if (hook === void 0) continue;
|
|
1596
|
+
const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
|
|
1597
|
+
result[key] = flattenSerializedValue(hook.value);
|
|
1598
|
+
}
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
function extractContextNames(contexts) {
|
|
1602
|
+
const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1603
|
+
return names;
|
|
1604
|
+
}
|
|
1605
|
+
function anyContextChanged(contexts) {
|
|
1606
|
+
return contexts.some((c) => c.didTriggerRender);
|
|
1607
|
+
}
|
|
1608
|
+
function convertToInstrumentNode(node, depth = 0) {
|
|
1609
|
+
const contexts = extractContextNames(node.context);
|
|
1610
|
+
const contextChanged = anyContextChanged(node.context);
|
|
1611
|
+
const state = flattenHookState(node.state);
|
|
1612
|
+
const propsFlat = flattenSerializedValue(node.props);
|
|
1613
|
+
const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
|
|
1614
|
+
return {
|
|
1615
|
+
component: node.name,
|
|
1616
|
+
type: mapNodeType(node),
|
|
1617
|
+
renderCount: node.renderCount,
|
|
1618
|
+
lastRenderDuration: node.renderDuration,
|
|
1619
|
+
memoized: node.type === "memo",
|
|
1620
|
+
// memoSkipped requires tracking bail-outs across commits — not available from
|
|
1621
|
+
// a single-shot capture. Defaulted to 0.
|
|
1622
|
+
memoSkipped: 0,
|
|
1623
|
+
props,
|
|
1624
|
+
// propsChanged is not tracked in a single-shot capture — would need a diff
|
|
1625
|
+
// between two renders. Defaulted to false.
|
|
1626
|
+
propsChanged: false,
|
|
1627
|
+
state,
|
|
1628
|
+
stateChanged: false,
|
|
1629
|
+
contextChanged,
|
|
1630
|
+
contexts,
|
|
1631
|
+
depth,
|
|
1632
|
+
children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function filterByContext(node, contextName) {
|
|
1636
|
+
const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
|
|
1637
|
+
const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
|
|
1638
|
+
if (!selfMatches && filteredChildren.length === 0) return null;
|
|
1639
|
+
return { ...node, children: filteredChildren };
|
|
1640
|
+
}
|
|
1641
|
+
function filterWastedRenders(node) {
|
|
1642
|
+
const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
|
|
1643
|
+
const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
|
|
1644
|
+
if (!isWasted && filteredChildren.length === 0) return null;
|
|
1645
|
+
return { ...node, children: filteredChildren };
|
|
1646
|
+
}
|
|
1647
|
+
function sortTree(node, sortBy) {
|
|
1648
|
+
const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
|
|
1649
|
+
if (sortBy === "renderCount") return b.renderCount - a.renderCount;
|
|
1650
|
+
return a.depth - b.depth;
|
|
1651
|
+
});
|
|
1652
|
+
return { ...node, children: sortedChildren };
|
|
1653
|
+
}
|
|
1654
|
+
function annotateProviderDepth(node, providerDepth = 0) {
|
|
1655
|
+
const isProvider = node.type === "context.provider";
|
|
1656
|
+
const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
|
|
1657
|
+
return {
|
|
1658
|
+
...node,
|
|
1659
|
+
_providerDepth: providerDepth,
|
|
1660
|
+
children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function limitNodes(root, limit) {
|
|
1664
|
+
let remaining = limit;
|
|
1665
|
+
const clip = (node) => {
|
|
1666
|
+
if (remaining <= 0) return null;
|
|
1667
|
+
remaining--;
|
|
1668
|
+
const clippedChildren = [];
|
|
1669
|
+
for (const child of node.children) {
|
|
1670
|
+
const clipped = clip(child);
|
|
1671
|
+
if (clipped !== null) clippedChildren.push(clipped);
|
|
1672
|
+
}
|
|
1673
|
+
return { ...node, children: clippedChildren };
|
|
1674
|
+
};
|
|
1675
|
+
return clip(root) ?? root;
|
|
1676
|
+
}
|
|
1677
|
+
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1678
|
+
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
1679
|
+
var VERTICAL = "\u2502 ";
|
|
1680
|
+
var EMPTY = " ";
|
|
1681
|
+
function buildTTYLabel(node, showProviderDepth) {
|
|
1682
|
+
const parts = [node.component];
|
|
1683
|
+
switch (node.type) {
|
|
1684
|
+
case "memo":
|
|
1685
|
+
parts.push("[memo]");
|
|
1686
|
+
break;
|
|
1687
|
+
case "forwardRef":
|
|
1688
|
+
parts.push("[forwardRef]");
|
|
1689
|
+
break;
|
|
1690
|
+
case "class":
|
|
1691
|
+
parts.push("[class]");
|
|
1692
|
+
break;
|
|
1693
|
+
case "context.provider":
|
|
1694
|
+
parts.push("[provider]");
|
|
1695
|
+
break;
|
|
1696
|
+
case "context.consumer":
|
|
1697
|
+
parts.push("[consumer]");
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
if (node.renderCount > 0) {
|
|
1701
|
+
const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
|
|
1702
|
+
parts.push(`(renders:${node.renderCount}${durationStr})`);
|
|
1703
|
+
}
|
|
1704
|
+
if (node.contexts.length > 0) {
|
|
1705
|
+
parts.push(`[ctx:${node.contexts.join(",")}]`);
|
|
1706
|
+
}
|
|
1707
|
+
if (showProviderDepth) {
|
|
1708
|
+
const pd = node._providerDepth;
|
|
1709
|
+
if (pd !== void 0 && pd > 0) {
|
|
1710
|
+
parts.push(`[pd:${pd}]`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return parts.join(" ");
|
|
1714
|
+
}
|
|
1715
|
+
function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
|
|
1716
|
+
if (node.type === "host") {
|
|
1717
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1718
|
+
const child = node.children[i];
|
|
1719
|
+
if (child !== void 0) {
|
|
1720
|
+
renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const connector = isLast ? LAST_BRANCH : BRANCH;
|
|
1726
|
+
lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
|
|
1727
|
+
const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
|
|
1728
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1729
|
+
const child = node.children[i];
|
|
1730
|
+
if (child !== void 0) {
|
|
1731
|
+
renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function formatInstrumentTree(root, showProviderDepth = false) {
|
|
1736
|
+
const lines = [];
|
|
1737
|
+
if (root.type !== "host") {
|
|
1738
|
+
lines.push(buildTTYLabel(root, showProviderDepth));
|
|
1739
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1740
|
+
const child = root.children[i];
|
|
1741
|
+
if (child !== void 0) {
|
|
1742
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1747
|
+
const child = root.children[i];
|
|
1748
|
+
if (child !== void 0) {
|
|
1749
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return lines.join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
async function runInstrumentTree(options) {
|
|
1756
|
+
const { componentName, filePath } = options;
|
|
1757
|
+
const pool = await getPool();
|
|
1758
|
+
const slot = await pool.acquire();
|
|
1759
|
+
const { page } = slot;
|
|
1760
|
+
try {
|
|
1761
|
+
await page.addInitScript({ content: getBrowserEntryScript() });
|
|
1762
|
+
const htmlHarness = await buildComponentHarness(
|
|
1763
|
+
filePath,
|
|
1764
|
+
componentName,
|
|
1765
|
+
{},
|
|
1766
|
+
DEFAULT_VIEWPORT_WIDTH
|
|
1767
|
+
);
|
|
1768
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1769
|
+
await page.waitForFunction(
|
|
1770
|
+
() => {
|
|
1771
|
+
const w = window;
|
|
1772
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1773
|
+
},
|
|
1774
|
+
{ timeout: 15e3 }
|
|
1775
|
+
);
|
|
1776
|
+
const renderError = await page.evaluate(
|
|
1777
|
+
() => window.__SCOPE_RENDER_ERROR__ ?? null
|
|
1778
|
+
);
|
|
1779
|
+
if (renderError !== null) {
|
|
1780
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1781
|
+
}
|
|
1782
|
+
const captureJson = await page.evaluate(async () => {
|
|
1783
|
+
const w = window;
|
|
1784
|
+
if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
|
|
1785
|
+
throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
|
|
1786
|
+
}
|
|
1787
|
+
return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
|
|
1788
|
+
});
|
|
1789
|
+
const captureResult = JSON.parse(captureJson);
|
|
1790
|
+
const componentTree = captureResult.tree;
|
|
1791
|
+
if (componentTree === void 0 || componentTree === null) {
|
|
1792
|
+
throw new Error(`No component tree found for "${componentName}"`);
|
|
1793
|
+
}
|
|
1794
|
+
let instrumentRoot = convertToInstrumentNode(componentTree, 0);
|
|
1795
|
+
if (options.usesContext !== void 0) {
|
|
1796
|
+
const filtered = filterByContext(instrumentRoot, options.usesContext);
|
|
1797
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1798
|
+
}
|
|
1799
|
+
if (options.wastedRenders === true) {
|
|
1800
|
+
const filtered = filterWastedRenders(instrumentRoot);
|
|
1801
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1802
|
+
}
|
|
1803
|
+
if (options.sortBy !== void 0) {
|
|
1804
|
+
instrumentRoot = sortTree(instrumentRoot, options.sortBy);
|
|
1805
|
+
}
|
|
1806
|
+
if (options.providerDepth === true) {
|
|
1807
|
+
instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
|
|
1808
|
+
}
|
|
1809
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
1810
|
+
instrumentRoot = limitNodes(instrumentRoot, options.limit);
|
|
1811
|
+
}
|
|
1812
|
+
return instrumentRoot;
|
|
1813
|
+
} finally {
|
|
1814
|
+
pool.release(slot);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function createInstrumentTreeCommand() {
|
|
1818
|
+
return new 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(
|
|
1819
|
+
"--wasted-renders",
|
|
1820
|
+
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1821
|
+
false
|
|
1822
|
+
).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
|
|
1823
|
+
try {
|
|
1824
|
+
const manifest = loadManifest(opts.manifest);
|
|
1825
|
+
const descriptor = manifest.components[componentName];
|
|
1826
|
+
if (descriptor === void 0) {
|
|
1827
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1828
|
+
throw new Error(
|
|
1829
|
+
`Component "${componentName}" not found in manifest.
|
|
1830
|
+
Available: ${available}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
if (opts.sortBy !== void 0) {
|
|
1834
|
+
const allowed = ["renderCount", "depth"];
|
|
1835
|
+
if (!allowed.includes(opts.sortBy)) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const rootDir = process.cwd();
|
|
1842
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
1843
|
+
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1844
|
+
`);
|
|
1845
|
+
const instrumentRoot = await runInstrumentTree({
|
|
1846
|
+
componentName,
|
|
1847
|
+
filePath,
|
|
1848
|
+
sortBy: opts.sortBy,
|
|
1849
|
+
limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
|
|
1850
|
+
usesContext: opts.usesContext,
|
|
1851
|
+
providerDepth: opts.providerDepth,
|
|
1852
|
+
wastedRenders: opts.wastedRenders
|
|
1853
|
+
});
|
|
1854
|
+
await shutdownPool();
|
|
1855
|
+
const fmt = resolveFormat2(opts.format);
|
|
1856
|
+
if (fmt === "json") {
|
|
1857
|
+
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1858
|
+
`);
|
|
1859
|
+
} else {
|
|
1860
|
+
const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
|
|
1861
|
+
process.stdout.write(`${tree}
|
|
1862
|
+
`);
|
|
1863
|
+
}
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
await shutdownPool();
|
|
1866
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1867
|
+
`);
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
function resolveFormat2(formatFlag) {
|
|
1873
|
+
if (formatFlag !== void 0) {
|
|
1874
|
+
const lower = formatFlag.toLowerCase();
|
|
1875
|
+
if (lower !== "json" && lower !== "tree") {
|
|
1876
|
+
throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
|
|
1877
|
+
}
|
|
1878
|
+
return lower;
|
|
1879
|
+
}
|
|
1880
|
+
return isTTY() ? "tree" : "json";
|
|
1881
|
+
}
|
|
1527
1882
|
|
|
1528
1883
|
// src/instrument/renders.ts
|
|
1529
|
-
var
|
|
1884
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1530
1885
|
function determineTrigger(event) {
|
|
1531
1886
|
if (event.forceUpdate) return "force_update";
|
|
1532
1887
|
if (event.stateChanged) return "state_change";
|
|
@@ -1825,26 +2180,26 @@ async function replayInteraction2(page, steps) {
|
|
|
1825
2180
|
}
|
|
1826
2181
|
}
|
|
1827
2182
|
}
|
|
1828
|
-
var
|
|
1829
|
-
async function
|
|
1830
|
-
if (
|
|
1831
|
-
|
|
2183
|
+
var _pool2 = null;
|
|
2184
|
+
async function getPool2() {
|
|
2185
|
+
if (_pool2 === null) {
|
|
2186
|
+
_pool2 = new BrowserPool({
|
|
1832
2187
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1833
2188
|
viewportWidth: 1280,
|
|
1834
2189
|
viewportHeight: 800
|
|
1835
2190
|
});
|
|
1836
|
-
await
|
|
2191
|
+
await _pool2.init();
|
|
1837
2192
|
}
|
|
1838
|
-
return
|
|
2193
|
+
return _pool2;
|
|
1839
2194
|
}
|
|
1840
|
-
async function
|
|
1841
|
-
if (
|
|
1842
|
-
await
|
|
1843
|
-
|
|
2195
|
+
async function shutdownPool2() {
|
|
2196
|
+
if (_pool2 !== null) {
|
|
2197
|
+
await _pool2.close();
|
|
2198
|
+
_pool2 = null;
|
|
1844
2199
|
}
|
|
1845
2200
|
}
|
|
1846
2201
|
async function analyzeRenders(options) {
|
|
1847
|
-
const manifestPath = options.manifestPath ??
|
|
2202
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
|
|
1848
2203
|
const manifest = loadManifest(manifestPath);
|
|
1849
2204
|
const descriptor = manifest.components[options.componentName];
|
|
1850
2205
|
if (descriptor === void 0) {
|
|
@@ -1857,7 +2212,7 @@ Available: ${available}`
|
|
|
1857
2212
|
const rootDir = process.cwd();
|
|
1858
2213
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
1859
2214
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1860
|
-
const pool = await
|
|
2215
|
+
const pool = await getPool2();
|
|
1861
2216
|
const slot = await pool.acquire();
|
|
1862
2217
|
const { page } = slot;
|
|
1863
2218
|
const startMs = performance.now();
|
|
@@ -1942,7 +2297,7 @@ function createInstrumentRendersCommand() {
|
|
|
1942
2297
|
"--interaction <json>",
|
|
1943
2298
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1944
2299
|
"[]"
|
|
1945
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2300
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
1946
2301
|
async (componentName, opts) => {
|
|
1947
2302
|
let interaction = [];
|
|
1948
2303
|
try {
|
|
@@ -1965,7 +2320,7 @@ function createInstrumentRendersCommand() {
|
|
|
1965
2320
|
interaction,
|
|
1966
2321
|
manifestPath: opts.manifest
|
|
1967
2322
|
});
|
|
1968
|
-
await
|
|
2323
|
+
await shutdownPool2();
|
|
1969
2324
|
if (opts.json || !isTTY()) {
|
|
1970
2325
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1971
2326
|
`);
|
|
@@ -1974,7 +2329,7 @@ function createInstrumentRendersCommand() {
|
|
|
1974
2329
|
`);
|
|
1975
2330
|
}
|
|
1976
2331
|
} catch (err) {
|
|
1977
|
-
await
|
|
2332
|
+
await shutdownPool2();
|
|
1978
2333
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1979
2334
|
`);
|
|
1980
2335
|
process.exit(1);
|
|
@@ -1989,6 +2344,7 @@ function createInstrumentCommand() {
|
|
|
1989
2344
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
1990
2345
|
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
1991
2346
|
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
2347
|
+
instrumentCmd.addCommand(createInstrumentTreeCommand());
|
|
1992
2348
|
return instrumentCmd;
|
|
1993
2349
|
}
|
|
1994
2350
|
async function browserCapture(options) {
|
|
@@ -2148,24 +2504,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
2148
2504
|
}
|
|
2149
2505
|
|
|
2150
2506
|
// src/render-commands.ts
|
|
2151
|
-
var
|
|
2507
|
+
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2152
2508
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2153
|
-
var
|
|
2154
|
-
async function
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2509
|
+
var _pool3 = null;
|
|
2510
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2511
|
+
if (_pool3 === null) {
|
|
2512
|
+
_pool3 = new BrowserPool({
|
|
2157
2513
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2158
2514
|
viewportWidth,
|
|
2159
2515
|
viewportHeight
|
|
2160
2516
|
});
|
|
2161
|
-
await
|
|
2517
|
+
await _pool3.init();
|
|
2162
2518
|
}
|
|
2163
|
-
return
|
|
2519
|
+
return _pool3;
|
|
2164
2520
|
}
|
|
2165
|
-
async function
|
|
2166
|
-
if (
|
|
2167
|
-
await
|
|
2168
|
-
|
|
2521
|
+
async function shutdownPool3() {
|
|
2522
|
+
if (_pool3 !== null) {
|
|
2523
|
+
await _pool3.close();
|
|
2524
|
+
_pool3 = null;
|
|
2169
2525
|
}
|
|
2170
2526
|
}
|
|
2171
2527
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2176,7 +2532,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2176
2532
|
_satori: satori,
|
|
2177
2533
|
async renderCell(props, _complexityClass) {
|
|
2178
2534
|
const startMs = performance.now();
|
|
2179
|
-
const pool = await
|
|
2535
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2180
2536
|
const htmlHarness = await buildComponentHarness(
|
|
2181
2537
|
filePath,
|
|
2182
2538
|
componentName,
|
|
@@ -2273,7 +2629,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2273
2629
|
};
|
|
2274
2630
|
}
|
|
2275
2631
|
function registerRenderSingle(renderCmd) {
|
|
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",
|
|
2632
|
+
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(
|
|
2277
2633
|
async (componentName, opts) => {
|
|
2278
2634
|
try {
|
|
2279
2635
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2312,7 +2668,7 @@ Available: ${available}`
|
|
|
2312
2668
|
}
|
|
2313
2669
|
}
|
|
2314
2670
|
);
|
|
2315
|
-
await
|
|
2671
|
+
await shutdownPool3();
|
|
2316
2672
|
if (outcome.crashed) {
|
|
2317
2673
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2318
2674
|
`);
|
|
@@ -2360,7 +2716,7 @@ Available: ${available}`
|
|
|
2360
2716
|
);
|
|
2361
2717
|
}
|
|
2362
2718
|
} catch (err) {
|
|
2363
|
-
await
|
|
2719
|
+
await shutdownPool3();
|
|
2364
2720
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2365
2721
|
`);
|
|
2366
2722
|
process.exit(1);
|
|
@@ -2372,7 +2728,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
2372
2728
|
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(
|
|
2373
2729
|
"--contexts <ids>",
|
|
2374
2730
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
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",
|
|
2731
|
+
).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(
|
|
2376
2732
|
async (componentName, opts) => {
|
|
2377
2733
|
try {
|
|
2378
2734
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2445,7 +2801,7 @@ Available: ${available}`
|
|
|
2445
2801
|
concurrency
|
|
2446
2802
|
});
|
|
2447
2803
|
const result = await matrix.render();
|
|
2448
|
-
await
|
|
2804
|
+
await shutdownPool3();
|
|
2449
2805
|
process.stderr.write(
|
|
2450
2806
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2451
2807
|
`
|
|
@@ -2490,7 +2846,7 @@ Available: ${available}`
|
|
|
2490
2846
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2491
2847
|
}
|
|
2492
2848
|
} catch (err) {
|
|
2493
|
-
await
|
|
2849
|
+
await shutdownPool3();
|
|
2494
2850
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2495
2851
|
`);
|
|
2496
2852
|
process.exit(1);
|
|
@@ -2499,7 +2855,7 @@ Available: ${available}`
|
|
|
2499
2855
|
);
|
|
2500
2856
|
}
|
|
2501
2857
|
function registerRenderAll(renderCmd) {
|
|
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",
|
|
2858
|
+
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(
|
|
2503
2859
|
async (opts) => {
|
|
2504
2860
|
try {
|
|
2505
2861
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2587,13 +2943,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2587
2943
|
workers.push(worker());
|
|
2588
2944
|
}
|
|
2589
2945
|
await Promise.all(workers);
|
|
2590
|
-
await
|
|
2946
|
+
await shutdownPool3();
|
|
2591
2947
|
process.stderr.write("\n");
|
|
2592
2948
|
const summary = formatSummaryText(results, outputDir);
|
|
2593
2949
|
process.stderr.write(`${summary}
|
|
2594
2950
|
`);
|
|
2595
2951
|
} catch (err) {
|
|
2596
|
-
await
|
|
2952
|
+
await shutdownPool3();
|
|
2597
2953
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2598
2954
|
`);
|
|
2599
2955
|
process.exit(1);
|
|
@@ -2635,26 +2991,26 @@ function createRenderCommand() {
|
|
|
2635
2991
|
return renderCmd;
|
|
2636
2992
|
}
|
|
2637
2993
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2638
|
-
var
|
|
2639
|
-
async function
|
|
2640
|
-
if (
|
|
2641
|
-
|
|
2994
|
+
var _pool4 = null;
|
|
2995
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
2996
|
+
if (_pool4 === null) {
|
|
2997
|
+
_pool4 = new BrowserPool({
|
|
2642
2998
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2643
2999
|
viewportWidth,
|
|
2644
3000
|
viewportHeight
|
|
2645
3001
|
});
|
|
2646
|
-
await
|
|
3002
|
+
await _pool4.init();
|
|
2647
3003
|
}
|
|
2648
|
-
return
|
|
3004
|
+
return _pool4;
|
|
2649
3005
|
}
|
|
2650
|
-
async function
|
|
2651
|
-
if (
|
|
2652
|
-
await
|
|
2653
|
-
|
|
3006
|
+
async function shutdownPool4() {
|
|
3007
|
+
if (_pool4 !== null) {
|
|
3008
|
+
await _pool4.close();
|
|
3009
|
+
_pool4 = null;
|
|
2654
3010
|
}
|
|
2655
3011
|
}
|
|
2656
3012
|
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2657
|
-
const pool = await
|
|
3013
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2658
3014
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2659
3015
|
const slot = await pool.acquire();
|
|
2660
3016
|
const { page } = slot;
|
|
@@ -2787,12 +3143,12 @@ async function runBaseline(options = {}) {
|
|
|
2787
3143
|
mkdirSync(rendersDir, { recursive: true });
|
|
2788
3144
|
let manifest;
|
|
2789
3145
|
if (manifestPath !== void 0) {
|
|
2790
|
-
const { readFileSync:
|
|
3146
|
+
const { readFileSync: readFileSync9 } = await import('fs');
|
|
2791
3147
|
const absPath = resolve(rootDir, manifestPath);
|
|
2792
3148
|
if (!existsSync(absPath)) {
|
|
2793
3149
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2794
3150
|
}
|
|
2795
|
-
manifest = JSON.parse(
|
|
3151
|
+
manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
|
|
2796
3152
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2797
3153
|
`);
|
|
2798
3154
|
} else {
|
|
@@ -2904,7 +3260,7 @@ async function runBaseline(options = {}) {
|
|
|
2904
3260
|
workers.push(worker());
|
|
2905
3261
|
}
|
|
2906
3262
|
await Promise.all(workers);
|
|
2907
|
-
await
|
|
3263
|
+
await shutdownPool4();
|
|
2908
3264
|
if (isTTY()) {
|
|
2909
3265
|
process.stderr.write("\n");
|
|
2910
3266
|
}
|
|
@@ -2953,12 +3309,485 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
2953
3309
|
}
|
|
2954
3310
|
);
|
|
2955
3311
|
}
|
|
3312
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3313
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3314
|
+
const compliancePath = resolve(baselineDir, "compliance.json");
|
|
3315
|
+
if (!existsSync(compliancePath)) return null;
|
|
3316
|
+
const raw = JSON.parse(readFileSync(compliancePath, "utf-8"));
|
|
3317
|
+
return raw;
|
|
3318
|
+
}
|
|
3319
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3320
|
+
const jsonPath = resolve(baselineDir, "renders", `${componentName}.json`);
|
|
3321
|
+
if (!existsSync(jsonPath)) return null;
|
|
3322
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
3323
|
+
}
|
|
3324
|
+
var _pool5 = null;
|
|
3325
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3326
|
+
if (_pool5 === null) {
|
|
3327
|
+
_pool5 = new BrowserPool({
|
|
3328
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3329
|
+
viewportWidth,
|
|
3330
|
+
viewportHeight
|
|
3331
|
+
});
|
|
3332
|
+
await _pool5.init();
|
|
3333
|
+
}
|
|
3334
|
+
return _pool5;
|
|
3335
|
+
}
|
|
3336
|
+
async function shutdownPool5() {
|
|
3337
|
+
if (_pool5 !== null) {
|
|
3338
|
+
await _pool5.close();
|
|
3339
|
+
_pool5 = null;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3343
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3344
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3345
|
+
const slot = await pool.acquire();
|
|
3346
|
+
const { page } = slot;
|
|
3347
|
+
try {
|
|
3348
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3349
|
+
await page.waitForFunction(
|
|
3350
|
+
() => {
|
|
3351
|
+
const w = window;
|
|
3352
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3353
|
+
},
|
|
3354
|
+
{ timeout: 15e3 }
|
|
3355
|
+
);
|
|
3356
|
+
const renderError = await page.evaluate(() => {
|
|
3357
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3358
|
+
});
|
|
3359
|
+
if (renderError !== null) {
|
|
3360
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3361
|
+
}
|
|
3362
|
+
const rootDir = process.cwd();
|
|
3363
|
+
const classes = await page.evaluate(() => {
|
|
3364
|
+
const set = /* @__PURE__ */ new Set();
|
|
3365
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3366
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3367
|
+
if (c) set.add(c);
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
return [...set];
|
|
3371
|
+
});
|
|
3372
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3373
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3374
|
+
await page.addStyleTag({ content: projectCss });
|
|
3375
|
+
}
|
|
3376
|
+
const startMs = performance.now();
|
|
3377
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3378
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3379
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3380
|
+
throw new Error(
|
|
3381
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const PAD = 24;
|
|
3385
|
+
const MIN_W = 320;
|
|
3386
|
+
const MIN_H = 200;
|
|
3387
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3388
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3389
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3390
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3391
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3392
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3393
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3394
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3395
|
+
const screenshot = await page.screenshot({
|
|
3396
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3397
|
+
type: "png"
|
|
3398
|
+
});
|
|
3399
|
+
const computedStylesRaw = {};
|
|
3400
|
+
const styles = await page.evaluate((sel) => {
|
|
3401
|
+
const el = document.querySelector(sel);
|
|
3402
|
+
if (el === null) return {};
|
|
3403
|
+
const computed = window.getComputedStyle(el);
|
|
3404
|
+
const out = {};
|
|
3405
|
+
for (const prop of [
|
|
3406
|
+
"display",
|
|
3407
|
+
"width",
|
|
3408
|
+
"height",
|
|
3409
|
+
"color",
|
|
3410
|
+
"backgroundColor",
|
|
3411
|
+
"fontSize",
|
|
3412
|
+
"fontFamily",
|
|
3413
|
+
"padding",
|
|
3414
|
+
"margin"
|
|
3415
|
+
]) {
|
|
3416
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3417
|
+
}
|
|
3418
|
+
return out;
|
|
3419
|
+
}, "[data-reactscope-root] > *");
|
|
3420
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3421
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3422
|
+
return {
|
|
3423
|
+
screenshot,
|
|
3424
|
+
width: Math.round(safeW),
|
|
3425
|
+
height: Math.round(safeH),
|
|
3426
|
+
renderTimeMs,
|
|
3427
|
+
computedStyles: computedStylesRaw
|
|
3428
|
+
};
|
|
3429
|
+
} finally {
|
|
3430
|
+
pool.release(slot);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3434
|
+
const flat = {};
|
|
3435
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3436
|
+
Object.assign(flat, styles);
|
|
3437
|
+
}
|
|
3438
|
+
const colors = {};
|
|
3439
|
+
const spacing = {};
|
|
3440
|
+
const typography = {};
|
|
3441
|
+
const borders = {};
|
|
3442
|
+
const shadows = {};
|
|
3443
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3444
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3445
|
+
colors[prop] = value;
|
|
3446
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3447
|
+
spacing[prop] = value;
|
|
3448
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3449
|
+
typography[prop] = value;
|
|
3450
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3451
|
+
borders[prop] = value;
|
|
3452
|
+
} else if (prop === "boxShadow") {
|
|
3453
|
+
shadows[prop] = value;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3457
|
+
}
|
|
3458
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3459
|
+
if (entry.renderFailed) return "unchanged";
|
|
3460
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3461
|
+
return "added";
|
|
3462
|
+
}
|
|
3463
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3464
|
+
return "removed";
|
|
3465
|
+
}
|
|
3466
|
+
const delta = entry.complianceDelta;
|
|
3467
|
+
if (delta !== null) {
|
|
3468
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3469
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3470
|
+
}
|
|
3471
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3472
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3473
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3474
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3475
|
+
}
|
|
3476
|
+
return "unchanged";
|
|
3477
|
+
}
|
|
3478
|
+
async function runDiff(options = {}) {
|
|
3479
|
+
const {
|
|
3480
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3481
|
+
componentsGlob,
|
|
3482
|
+
manifestPath,
|
|
3483
|
+
viewportWidth = 375,
|
|
3484
|
+
viewportHeight = 812,
|
|
3485
|
+
regressionThreshold = 0.01
|
|
3486
|
+
} = options;
|
|
3487
|
+
const startTime = performance.now();
|
|
3488
|
+
const rootDir = process.cwd();
|
|
3489
|
+
const baselineDir = resolve(rootDir, baselineDirRaw);
|
|
3490
|
+
if (!existsSync(baselineDir)) {
|
|
3491
|
+
throw new Error(
|
|
3492
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3493
|
+
);
|
|
3494
|
+
}
|
|
3495
|
+
const baselineManifestPath = resolve(baselineDir, "manifest.json");
|
|
3496
|
+
if (!existsSync(baselineManifestPath)) {
|
|
3497
|
+
throw new Error(
|
|
3498
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
const baselineManifest = JSON.parse(readFileSync(baselineManifestPath, "utf-8"));
|
|
3502
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3503
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3504
|
+
process.stderr.write(
|
|
3505
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3506
|
+
`
|
|
3507
|
+
);
|
|
3508
|
+
let currentManifest;
|
|
3509
|
+
if (manifestPath !== void 0) {
|
|
3510
|
+
const absPath = resolve(rootDir, manifestPath);
|
|
3511
|
+
if (!existsSync(absPath)) {
|
|
3512
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3513
|
+
}
|
|
3514
|
+
currentManifest = JSON.parse(readFileSync(absPath, "utf-8"));
|
|
3515
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3516
|
+
`);
|
|
3517
|
+
} else {
|
|
3518
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3519
|
+
currentManifest = await generateManifest({ rootDir });
|
|
3520
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3521
|
+
process.stderr.write(`Found ${count} components.
|
|
3522
|
+
`);
|
|
3523
|
+
}
|
|
3524
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3525
|
+
if (componentsGlob !== void 0) {
|
|
3526
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3527
|
+
process.stderr.write(
|
|
3528
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3529
|
+
`
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3533
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3534
|
+
);
|
|
3535
|
+
const total = componentNames.length;
|
|
3536
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3537
|
+
`);
|
|
3538
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3539
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3540
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3541
|
+
let completed = 0;
|
|
3542
|
+
const CONCURRENCY = 4;
|
|
3543
|
+
let nextIdx = 0;
|
|
3544
|
+
const renderOne = async (name) => {
|
|
3545
|
+
const descriptor = currentManifest.components[name];
|
|
3546
|
+
if (descriptor === void 0) return;
|
|
3547
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3548
|
+
const outcome = await safeRender(
|
|
3549
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3550
|
+
{
|
|
3551
|
+
props: {},
|
|
3552
|
+
sourceLocation: {
|
|
3553
|
+
file: descriptor.filePath,
|
|
3554
|
+
line: descriptor.loc.start,
|
|
3555
|
+
column: 0
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
);
|
|
3559
|
+
completed++;
|
|
3560
|
+
const pct = Math.round(completed / total * 100);
|
|
3561
|
+
if (isTTY()) {
|
|
3562
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3563
|
+
}
|
|
3564
|
+
if (outcome.crashed) {
|
|
3565
|
+
renderFailures.add(name);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
const result = outcome.result;
|
|
3569
|
+
currentRenderMeta.set(name, {
|
|
3570
|
+
width: result.width,
|
|
3571
|
+
height: result.height,
|
|
3572
|
+
renderTimeMs: result.renderTimeMs
|
|
3573
|
+
});
|
|
3574
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3575
|
+
};
|
|
3576
|
+
if (total > 0) {
|
|
3577
|
+
const worker = async () => {
|
|
3578
|
+
while (nextIdx < componentNames.length) {
|
|
3579
|
+
const i = nextIdx++;
|
|
3580
|
+
const name = componentNames[i];
|
|
3581
|
+
if (name !== void 0) {
|
|
3582
|
+
await renderOne(name);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
const workers = [];
|
|
3587
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3588
|
+
workers.push(worker());
|
|
3589
|
+
}
|
|
3590
|
+
await Promise.all(workers);
|
|
3591
|
+
}
|
|
3592
|
+
await shutdownPool5();
|
|
3593
|
+
if (isTTY() && total > 0) {
|
|
3594
|
+
process.stderr.write("\n");
|
|
3595
|
+
}
|
|
3596
|
+
const resolver = new TokenResolver([]);
|
|
3597
|
+
const engine = new ComplianceEngine(resolver);
|
|
3598
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3599
|
+
const entries = [];
|
|
3600
|
+
for (const name of componentNames) {
|
|
3601
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3602
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3603
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3604
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3605
|
+
const failed = renderFailures.has(name);
|
|
3606
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3607
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3608
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3609
|
+
const partial = {
|
|
3610
|
+
name,
|
|
3611
|
+
baselineCompliance: baselineComplianceScore,
|
|
3612
|
+
currentCompliance: currentComplianceScore,
|
|
3613
|
+
complianceDelta: delta,
|
|
3614
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3615
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3616
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3617
|
+
renderFailed: failed
|
|
3618
|
+
};
|
|
3619
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3620
|
+
}
|
|
3621
|
+
for (const name of removedNames) {
|
|
3622
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3623
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3624
|
+
entries.push({
|
|
3625
|
+
name,
|
|
3626
|
+
status: "removed",
|
|
3627
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3628
|
+
currentCompliance: null,
|
|
3629
|
+
complianceDelta: null,
|
|
3630
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3631
|
+
currentDimensions: null,
|
|
3632
|
+
renderTimeMs: null,
|
|
3633
|
+
renderFailed: false
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
const summary = {
|
|
3637
|
+
total: entries.length,
|
|
3638
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3639
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3640
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3641
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3642
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3643
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3644
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3645
|
+
};
|
|
3646
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3647
|
+
const wallClockMs = performance.now() - startTime;
|
|
3648
|
+
return {
|
|
3649
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3650
|
+
baselineDir,
|
|
3651
|
+
summary,
|
|
3652
|
+
components: entries,
|
|
3653
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3654
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3655
|
+
hasRegressions,
|
|
3656
|
+
wallClockMs
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
var STATUS_ICON = {
|
|
3660
|
+
added: "+",
|
|
3661
|
+
removed: "-",
|
|
3662
|
+
unchanged: " ",
|
|
3663
|
+
compliance_regressed: "\u2193",
|
|
3664
|
+
compliance_improved: "\u2191",
|
|
3665
|
+
size_changed: "~"
|
|
3666
|
+
};
|
|
3667
|
+
var STATUS_LABEL = {
|
|
3668
|
+
added: "added",
|
|
3669
|
+
removed: "removed",
|
|
3670
|
+
unchanged: "ok",
|
|
3671
|
+
compliance_regressed: "regressed",
|
|
3672
|
+
compliance_improved: "improved",
|
|
3673
|
+
size_changed: "size changed"
|
|
3674
|
+
};
|
|
3675
|
+
function formatDiffReport(result) {
|
|
3676
|
+
const lines = [];
|
|
3677
|
+
const title = "Scope Report Diff";
|
|
3678
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3679
|
+
lines.push(title, rule2);
|
|
3680
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3681
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3682
|
+
lines.push(
|
|
3683
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3684
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3685
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3686
|
+
""
|
|
3687
|
+
);
|
|
3688
|
+
const s = result.summary;
|
|
3689
|
+
lines.push(
|
|
3690
|
+
`Components: ${s.total} total ` + [
|
|
3691
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3692
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3693
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3694
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3695
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3696
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3697
|
+
].filter(Boolean).join(" "),
|
|
3698
|
+
""
|
|
3699
|
+
);
|
|
3700
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3701
|
+
if (notable.length === 0) {
|
|
3702
|
+
lines.push(" No changes detected.");
|
|
3703
|
+
} else {
|
|
3704
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3705
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3706
|
+
const divider = "-".repeat(header.length);
|
|
3707
|
+
lines.push(header, divider);
|
|
3708
|
+
for (const entry of notable) {
|
|
3709
|
+
const icon = STATUS_ICON[entry.status];
|
|
3710
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3711
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3712
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3713
|
+
if (entry.complianceDelta !== null) {
|
|
3714
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3715
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3716
|
+
}
|
|
3717
|
+
let dimStr = "\u2014";
|
|
3718
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3719
|
+
const b = entry.baselineDimensions;
|
|
3720
|
+
const c = entry.currentDimensions;
|
|
3721
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3722
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3723
|
+
} else {
|
|
3724
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3725
|
+
}
|
|
3726
|
+
} else if (entry.currentDimensions !== null) {
|
|
3727
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3728
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3729
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3730
|
+
}
|
|
3731
|
+
if (entry.renderFailed) {
|
|
3732
|
+
dimStr = "render failed";
|
|
3733
|
+
}
|
|
3734
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
lines.push(
|
|
3738
|
+
"",
|
|
3739
|
+
rule2,
|
|
3740
|
+
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`
|
|
3741
|
+
);
|
|
3742
|
+
return lines.join("\n");
|
|
3743
|
+
}
|
|
3744
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3745
|
+
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(
|
|
3746
|
+
"--regression-threshold <n>",
|
|
3747
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3748
|
+
"0.01"
|
|
3749
|
+
).action(
|
|
3750
|
+
async (opts) => {
|
|
3751
|
+
try {
|
|
3752
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3753
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3754
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3755
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3756
|
+
const result = await runDiff({
|
|
3757
|
+
baselineDir: opts.baseline,
|
|
3758
|
+
componentsGlob: opts.components,
|
|
3759
|
+
manifestPath: opts.manifest,
|
|
3760
|
+
viewportWidth,
|
|
3761
|
+
viewportHeight,
|
|
3762
|
+
regressionThreshold
|
|
3763
|
+
});
|
|
3764
|
+
if (opts.output !== void 0) {
|
|
3765
|
+
writeFileSync(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3766
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3767
|
+
`);
|
|
3768
|
+
}
|
|
3769
|
+
if (opts.json) {
|
|
3770
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3771
|
+
`);
|
|
3772
|
+
} else {
|
|
3773
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3774
|
+
`);
|
|
3775
|
+
}
|
|
3776
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3777
|
+
} catch (err) {
|
|
3778
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3779
|
+
`);
|
|
3780
|
+
process.exit(2);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
2956
3785
|
|
|
2957
3786
|
// src/tree-formatter.ts
|
|
2958
|
-
var
|
|
2959
|
-
var
|
|
2960
|
-
var
|
|
2961
|
-
var
|
|
3787
|
+
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3788
|
+
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
3789
|
+
var VERTICAL2 = "\u2502 ";
|
|
3790
|
+
var EMPTY2 = " ";
|
|
2962
3791
|
function buildLabel(node, options) {
|
|
2963
3792
|
const parts = [node.name];
|
|
2964
3793
|
if (node.type === "memo") {
|
|
@@ -3005,19 +3834,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
|
|
|
3005
3834
|
}
|
|
3006
3835
|
return;
|
|
3007
3836
|
}
|
|
3008
|
-
const connector = isLast ?
|
|
3837
|
+
const connector = isLast ? LAST_BRANCH2 : BRANCH2;
|
|
3009
3838
|
const label = buildLabel(node, options);
|
|
3010
3839
|
lines.push(`${prefix}${connector}${label}`);
|
|
3011
3840
|
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
3012
3841
|
const childCount = countVisibleDescendants(node, options);
|
|
3013
3842
|
if (childCount > 0) {
|
|
3014
|
-
const nextPrefix2 = prefix + (isLast ?
|
|
3015
|
-
lines.push(`${nextPrefix2}${
|
|
3843
|
+
const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3844
|
+
lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3016
3845
|
}
|
|
3017
3846
|
return;
|
|
3018
3847
|
}
|
|
3019
3848
|
const visibleChildren = getVisibleChildren(node, options);
|
|
3020
|
-
const nextPrefix = prefix + (isLast ?
|
|
3849
|
+
const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3021
3850
|
for (let i = 0; i < visibleChildren.length; i++) {
|
|
3022
3851
|
const child = visibleChildren[i];
|
|
3023
3852
|
if (child !== void 0) {
|
|
@@ -3059,7 +3888,7 @@ function formatTree(root, options = {}) {
|
|
|
3059
3888
|
if (options.maxDepth === 0) {
|
|
3060
3889
|
const childCount = countVisibleDescendants(root, options);
|
|
3061
3890
|
if (childCount > 0) {
|
|
3062
|
-
lines.push(`${
|
|
3891
|
+
lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3063
3892
|
}
|
|
3064
3893
|
} else {
|
|
3065
3894
|
const visibleChildren = getVisibleChildren(root, options);
|
|
@@ -3770,6 +4599,7 @@ function createProgram(options = {}) {
|
|
|
3770
4599
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
3771
4600
|
if (existingReportCmd !== void 0) {
|
|
3772
4601
|
registerBaselineSubCommand(existingReportCmd);
|
|
4602
|
+
registerDiffSubCommand(existingReportCmd);
|
|
3773
4603
|
}
|
|
3774
4604
|
return program;
|
|
3775
4605
|
}
|