@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/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command8 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -255,9 +255,9 @@ function createRL() {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
async function ask(rl, question) {
|
|
258
|
-
return new Promise((
|
|
258
|
+
return new Promise((resolve12) => {
|
|
259
259
|
rl.question(question, (answer) => {
|
|
260
|
-
|
|
260
|
+
resolve12(answer.trim());
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -406,9 +406,9 @@ function createInitCommand() {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
// src/instrument/renders.ts
|
|
409
|
-
import { resolve as
|
|
410
|
-
import { BrowserPool } from "@agent-scope/render";
|
|
411
|
-
import { Command as
|
|
409
|
+
import { resolve as resolve5 } from "path";
|
|
410
|
+
import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
|
|
411
|
+
import { Command as Command4 } from "commander";
|
|
412
412
|
|
|
413
413
|
// src/component-bundler.ts
|
|
414
414
|
import { dirname } from "path";
|
|
@@ -1602,8 +1602,371 @@ Available: ${available}`
|
|
|
1602
1602
|
return cmd;
|
|
1603
1603
|
}
|
|
1604
1604
|
|
|
1605
|
-
// src/instrument/
|
|
1605
|
+
// src/instrument/tree.ts
|
|
1606
|
+
import { resolve as resolve4 } from "path";
|
|
1607
|
+
import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
|
|
1608
|
+
import { BrowserPool } from "@agent-scope/render";
|
|
1609
|
+
import { Command as Command3 } from "commander";
|
|
1606
1610
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
1611
|
+
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
1612
|
+
var DEFAULT_VIEWPORT_HEIGHT = 812;
|
|
1613
|
+
var _pool = null;
|
|
1614
|
+
async function getPool() {
|
|
1615
|
+
if (_pool === null) {
|
|
1616
|
+
_pool = new BrowserPool({
|
|
1617
|
+
size: { browsers: 1, pagesPerBrowser: 1 },
|
|
1618
|
+
viewportWidth: DEFAULT_VIEWPORT_WIDTH,
|
|
1619
|
+
viewportHeight: DEFAULT_VIEWPORT_HEIGHT
|
|
1620
|
+
});
|
|
1621
|
+
await _pool.init();
|
|
1622
|
+
}
|
|
1623
|
+
return _pool;
|
|
1624
|
+
}
|
|
1625
|
+
async function shutdownPool() {
|
|
1626
|
+
if (_pool !== null) {
|
|
1627
|
+
await _pool.close();
|
|
1628
|
+
_pool = null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
function mapNodeType(node) {
|
|
1632
|
+
if (node.type === "forward_ref") return "forwardRef";
|
|
1633
|
+
if (node.type === "host") return "host";
|
|
1634
|
+
const name = node.name;
|
|
1635
|
+
if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
|
|
1636
|
+
if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
|
|
1637
|
+
return node.type;
|
|
1638
|
+
}
|
|
1639
|
+
function flattenSerializedValue(sv) {
|
|
1640
|
+
if (sv === null || sv === void 0) return null;
|
|
1641
|
+
const v = sv;
|
|
1642
|
+
switch (v.type) {
|
|
1643
|
+
case "null":
|
|
1644
|
+
case "undefined":
|
|
1645
|
+
return null;
|
|
1646
|
+
case "string":
|
|
1647
|
+
case "number":
|
|
1648
|
+
case "boolean":
|
|
1649
|
+
return v.value;
|
|
1650
|
+
case "object": {
|
|
1651
|
+
if (!Array.isArray(v.entries)) return {};
|
|
1652
|
+
const result = {};
|
|
1653
|
+
for (const entry of v.entries) {
|
|
1654
|
+
result[entry.key] = flattenSerializedValue(entry.value);
|
|
1655
|
+
}
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
case "array": {
|
|
1659
|
+
if (!Array.isArray(v.items)) return [];
|
|
1660
|
+
return v.items.map(flattenSerializedValue);
|
|
1661
|
+
}
|
|
1662
|
+
case "function":
|
|
1663
|
+
return "[Function]";
|
|
1664
|
+
case "symbol":
|
|
1665
|
+
return `[Symbol: ${v.description ?? ""}]`;
|
|
1666
|
+
case "circular":
|
|
1667
|
+
return "[Circular]";
|
|
1668
|
+
case "truncated":
|
|
1669
|
+
return `[Truncated: ${v.preview ?? ""}]`;
|
|
1670
|
+
default:
|
|
1671
|
+
return v.preview ?? null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function flattenHookState(hooks) {
|
|
1675
|
+
const result = {};
|
|
1676
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1677
|
+
const hook = hooks[i];
|
|
1678
|
+
if (hook === void 0) continue;
|
|
1679
|
+
const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
|
|
1680
|
+
result[key] = flattenSerializedValue(hook.value);
|
|
1681
|
+
}
|
|
1682
|
+
return result;
|
|
1683
|
+
}
|
|
1684
|
+
function extractContextNames(contexts) {
|
|
1685
|
+
const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
|
|
1686
|
+
return names;
|
|
1687
|
+
}
|
|
1688
|
+
function anyContextChanged(contexts) {
|
|
1689
|
+
return contexts.some((c) => c.didTriggerRender);
|
|
1690
|
+
}
|
|
1691
|
+
function convertToInstrumentNode(node, depth = 0) {
|
|
1692
|
+
const contexts = extractContextNames(node.context);
|
|
1693
|
+
const contextChanged = anyContextChanged(node.context);
|
|
1694
|
+
const state = flattenHookState(node.state);
|
|
1695
|
+
const propsFlat = flattenSerializedValue(node.props);
|
|
1696
|
+
const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
|
|
1697
|
+
return {
|
|
1698
|
+
component: node.name,
|
|
1699
|
+
type: mapNodeType(node),
|
|
1700
|
+
renderCount: node.renderCount,
|
|
1701
|
+
lastRenderDuration: node.renderDuration,
|
|
1702
|
+
memoized: node.type === "memo",
|
|
1703
|
+
// memoSkipped requires tracking bail-outs across commits — not available from
|
|
1704
|
+
// a single-shot capture. Defaulted to 0.
|
|
1705
|
+
memoSkipped: 0,
|
|
1706
|
+
props,
|
|
1707
|
+
// propsChanged is not tracked in a single-shot capture — would need a diff
|
|
1708
|
+
// between two renders. Defaulted to false.
|
|
1709
|
+
propsChanged: false,
|
|
1710
|
+
state,
|
|
1711
|
+
stateChanged: false,
|
|
1712
|
+
contextChanged,
|
|
1713
|
+
contexts,
|
|
1714
|
+
depth,
|
|
1715
|
+
children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function filterByContext(node, contextName) {
|
|
1719
|
+
const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
|
|
1720
|
+
const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
|
|
1721
|
+
if (!selfMatches && filteredChildren.length === 0) return null;
|
|
1722
|
+
return { ...node, children: filteredChildren };
|
|
1723
|
+
}
|
|
1724
|
+
function filterWastedRenders(node) {
|
|
1725
|
+
const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
|
|
1726
|
+
const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
|
|
1727
|
+
if (!isWasted && filteredChildren.length === 0) return null;
|
|
1728
|
+
return { ...node, children: filteredChildren };
|
|
1729
|
+
}
|
|
1730
|
+
function sortTree(node, sortBy) {
|
|
1731
|
+
const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
|
|
1732
|
+
if (sortBy === "renderCount") return b.renderCount - a.renderCount;
|
|
1733
|
+
return a.depth - b.depth;
|
|
1734
|
+
});
|
|
1735
|
+
return { ...node, children: sortedChildren };
|
|
1736
|
+
}
|
|
1737
|
+
function annotateProviderDepth(node, providerDepth = 0) {
|
|
1738
|
+
const isProvider = node.type === "context.provider";
|
|
1739
|
+
const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
|
|
1740
|
+
return {
|
|
1741
|
+
...node,
|
|
1742
|
+
_providerDepth: providerDepth,
|
|
1743
|
+
children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function limitNodes(root, limit) {
|
|
1747
|
+
let remaining = limit;
|
|
1748
|
+
const clip = (node) => {
|
|
1749
|
+
if (remaining <= 0) return null;
|
|
1750
|
+
remaining--;
|
|
1751
|
+
const clippedChildren = [];
|
|
1752
|
+
for (const child of node.children) {
|
|
1753
|
+
const clipped = clip(child);
|
|
1754
|
+
if (clipped !== null) clippedChildren.push(clipped);
|
|
1755
|
+
}
|
|
1756
|
+
return { ...node, children: clippedChildren };
|
|
1757
|
+
};
|
|
1758
|
+
return clip(root) ?? root;
|
|
1759
|
+
}
|
|
1760
|
+
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1761
|
+
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
1762
|
+
var VERTICAL = "\u2502 ";
|
|
1763
|
+
var EMPTY = " ";
|
|
1764
|
+
function buildTTYLabel(node, showProviderDepth) {
|
|
1765
|
+
const parts = [node.component];
|
|
1766
|
+
switch (node.type) {
|
|
1767
|
+
case "memo":
|
|
1768
|
+
parts.push("[memo]");
|
|
1769
|
+
break;
|
|
1770
|
+
case "forwardRef":
|
|
1771
|
+
parts.push("[forwardRef]");
|
|
1772
|
+
break;
|
|
1773
|
+
case "class":
|
|
1774
|
+
parts.push("[class]");
|
|
1775
|
+
break;
|
|
1776
|
+
case "context.provider":
|
|
1777
|
+
parts.push("[provider]");
|
|
1778
|
+
break;
|
|
1779
|
+
case "context.consumer":
|
|
1780
|
+
parts.push("[consumer]");
|
|
1781
|
+
break;
|
|
1782
|
+
default:
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (node.renderCount > 0) {
|
|
1786
|
+
const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
|
|
1787
|
+
parts.push(`(renders:${node.renderCount}${durationStr})`);
|
|
1788
|
+
}
|
|
1789
|
+
if (node.contexts.length > 0) {
|
|
1790
|
+
parts.push(`[ctx:${node.contexts.join(",")}]`);
|
|
1791
|
+
}
|
|
1792
|
+
if (showProviderDepth) {
|
|
1793
|
+
const pd = node._providerDepth;
|
|
1794
|
+
if (pd !== void 0 && pd > 0) {
|
|
1795
|
+
parts.push(`[pd:${pd}]`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return parts.join(" ");
|
|
1799
|
+
}
|
|
1800
|
+
function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
|
|
1801
|
+
if (node.type === "host") {
|
|
1802
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1803
|
+
const child = node.children[i];
|
|
1804
|
+
if (child !== void 0) {
|
|
1805
|
+
renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const connector = isLast ? LAST_BRANCH : BRANCH;
|
|
1811
|
+
lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
|
|
1812
|
+
const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
|
|
1813
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1814
|
+
const child = node.children[i];
|
|
1815
|
+
if (child !== void 0) {
|
|
1816
|
+
renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function formatInstrumentTree(root, showProviderDepth = false) {
|
|
1821
|
+
const lines = [];
|
|
1822
|
+
if (root.type !== "host") {
|
|
1823
|
+
lines.push(buildTTYLabel(root, showProviderDepth));
|
|
1824
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1825
|
+
const child = root.children[i];
|
|
1826
|
+
if (child !== void 0) {
|
|
1827
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
} else {
|
|
1831
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
1832
|
+
const child = root.children[i];
|
|
1833
|
+
if (child !== void 0) {
|
|
1834
|
+
renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return lines.join("\n");
|
|
1839
|
+
}
|
|
1840
|
+
async function runInstrumentTree(options) {
|
|
1841
|
+
const { componentName, filePath } = options;
|
|
1842
|
+
const pool = await getPool();
|
|
1843
|
+
const slot = await pool.acquire();
|
|
1844
|
+
const { page } = slot;
|
|
1845
|
+
try {
|
|
1846
|
+
await page.addInitScript({ content: getBrowserEntryScript2() });
|
|
1847
|
+
const htmlHarness = await buildComponentHarness(
|
|
1848
|
+
filePath,
|
|
1849
|
+
componentName,
|
|
1850
|
+
{},
|
|
1851
|
+
DEFAULT_VIEWPORT_WIDTH
|
|
1852
|
+
);
|
|
1853
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
1854
|
+
await page.waitForFunction(
|
|
1855
|
+
() => {
|
|
1856
|
+
const w = window;
|
|
1857
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
1858
|
+
},
|
|
1859
|
+
{ timeout: 15e3 }
|
|
1860
|
+
);
|
|
1861
|
+
const renderError = await page.evaluate(
|
|
1862
|
+
() => window.__SCOPE_RENDER_ERROR__ ?? null
|
|
1863
|
+
);
|
|
1864
|
+
if (renderError !== null) {
|
|
1865
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
1866
|
+
}
|
|
1867
|
+
const captureJson = await page.evaluate(async () => {
|
|
1868
|
+
const w = window;
|
|
1869
|
+
if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
|
|
1870
|
+
throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
|
|
1871
|
+
}
|
|
1872
|
+
return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
|
|
1873
|
+
});
|
|
1874
|
+
const captureResult = JSON.parse(captureJson);
|
|
1875
|
+
const componentTree = captureResult.tree;
|
|
1876
|
+
if (componentTree === void 0 || componentTree === null) {
|
|
1877
|
+
throw new Error(`No component tree found for "${componentName}"`);
|
|
1878
|
+
}
|
|
1879
|
+
let instrumentRoot = convertToInstrumentNode(componentTree, 0);
|
|
1880
|
+
if (options.usesContext !== void 0) {
|
|
1881
|
+
const filtered = filterByContext(instrumentRoot, options.usesContext);
|
|
1882
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1883
|
+
}
|
|
1884
|
+
if (options.wastedRenders === true) {
|
|
1885
|
+
const filtered = filterWastedRenders(instrumentRoot);
|
|
1886
|
+
instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
|
|
1887
|
+
}
|
|
1888
|
+
if (options.sortBy !== void 0) {
|
|
1889
|
+
instrumentRoot = sortTree(instrumentRoot, options.sortBy);
|
|
1890
|
+
}
|
|
1891
|
+
if (options.providerDepth === true) {
|
|
1892
|
+
instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
|
|
1893
|
+
}
|
|
1894
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
1895
|
+
instrumentRoot = limitNodes(instrumentRoot, options.limit);
|
|
1896
|
+
}
|
|
1897
|
+
return instrumentRoot;
|
|
1898
|
+
} finally {
|
|
1899
|
+
pool.release(slot);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function createInstrumentTreeCommand() {
|
|
1903
|
+
return new Command3("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(
|
|
1904
|
+
"--wasted-renders",
|
|
1905
|
+
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
1906
|
+
false
|
|
1907
|
+
).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
|
|
1908
|
+
try {
|
|
1909
|
+
const manifest = loadManifest(opts.manifest);
|
|
1910
|
+
const descriptor = manifest.components[componentName];
|
|
1911
|
+
if (descriptor === void 0) {
|
|
1912
|
+
const available = Object.keys(manifest.components).slice(0, 5).join(", ");
|
|
1913
|
+
throw new Error(
|
|
1914
|
+
`Component "${componentName}" not found in manifest.
|
|
1915
|
+
Available: ${available}`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if (opts.sortBy !== void 0) {
|
|
1919
|
+
const allowed = ["renderCount", "depth"];
|
|
1920
|
+
if (!allowed.includes(opts.sortBy)) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
const rootDir = process.cwd();
|
|
1927
|
+
const filePath = resolve4(rootDir, descriptor.filePath);
|
|
1928
|
+
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
1929
|
+
`);
|
|
1930
|
+
const instrumentRoot = await runInstrumentTree({
|
|
1931
|
+
componentName,
|
|
1932
|
+
filePath,
|
|
1933
|
+
sortBy: opts.sortBy,
|
|
1934
|
+
limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
|
|
1935
|
+
usesContext: opts.usesContext,
|
|
1936
|
+
providerDepth: opts.providerDepth,
|
|
1937
|
+
wastedRenders: opts.wastedRenders
|
|
1938
|
+
});
|
|
1939
|
+
await shutdownPool();
|
|
1940
|
+
const fmt = resolveFormat2(opts.format);
|
|
1941
|
+
if (fmt === "json") {
|
|
1942
|
+
process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
|
|
1943
|
+
`);
|
|
1944
|
+
} else {
|
|
1945
|
+
const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
|
|
1946
|
+
process.stdout.write(`${tree}
|
|
1947
|
+
`);
|
|
1948
|
+
}
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
await shutdownPool();
|
|
1951
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1952
|
+
`);
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
function resolveFormat2(formatFlag) {
|
|
1958
|
+
if (formatFlag !== void 0) {
|
|
1959
|
+
const lower = formatFlag.toLowerCase();
|
|
1960
|
+
if (lower !== "json" && lower !== "tree") {
|
|
1961
|
+
throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
|
|
1962
|
+
}
|
|
1963
|
+
return lower;
|
|
1964
|
+
}
|
|
1965
|
+
return isTTY() ? "tree" : "json";
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// src/instrument/renders.ts
|
|
1969
|
+
var MANIFEST_PATH5 = ".reactscope/manifest.json";
|
|
1607
1970
|
function determineTrigger(event) {
|
|
1608
1971
|
if (event.forceUpdate) return "force_update";
|
|
1609
1972
|
if (event.stateChanged) return "state_change";
|
|
@@ -1904,26 +2267,26 @@ async function replayInteraction2(page, steps) {
|
|
|
1904
2267
|
}
|
|
1905
2268
|
}
|
|
1906
2269
|
}
|
|
1907
|
-
var
|
|
1908
|
-
async function
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
2270
|
+
var _pool2 = null;
|
|
2271
|
+
async function getPool2() {
|
|
2272
|
+
if (_pool2 === null) {
|
|
2273
|
+
_pool2 = new BrowserPool2({
|
|
1911
2274
|
size: { browsers: 1, pagesPerBrowser: 2 },
|
|
1912
2275
|
viewportWidth: 1280,
|
|
1913
2276
|
viewportHeight: 800
|
|
1914
2277
|
});
|
|
1915
|
-
await
|
|
2278
|
+
await _pool2.init();
|
|
1916
2279
|
}
|
|
1917
|
-
return
|
|
2280
|
+
return _pool2;
|
|
1918
2281
|
}
|
|
1919
|
-
async function
|
|
1920
|
-
if (
|
|
1921
|
-
await
|
|
1922
|
-
|
|
2282
|
+
async function shutdownPool2() {
|
|
2283
|
+
if (_pool2 !== null) {
|
|
2284
|
+
await _pool2.close();
|
|
2285
|
+
_pool2 = null;
|
|
1923
2286
|
}
|
|
1924
2287
|
}
|
|
1925
2288
|
async function analyzeRenders(options) {
|
|
1926
|
-
const manifestPath = options.manifestPath ??
|
|
2289
|
+
const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
|
|
1927
2290
|
const manifest = loadManifest(manifestPath);
|
|
1928
2291
|
const descriptor = manifest.components[options.componentName];
|
|
1929
2292
|
if (descriptor === void 0) {
|
|
@@ -1934,9 +2297,9 @@ Available: ${available}`
|
|
|
1934
2297
|
);
|
|
1935
2298
|
}
|
|
1936
2299
|
const rootDir = process.cwd();
|
|
1937
|
-
const filePath =
|
|
2300
|
+
const filePath = resolve5(rootDir, descriptor.filePath);
|
|
1938
2301
|
const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
|
|
1939
|
-
const pool = await
|
|
2302
|
+
const pool = await getPool2();
|
|
1940
2303
|
const slot = await pool.acquire();
|
|
1941
2304
|
const { page } = slot;
|
|
1942
2305
|
const startMs = performance.now();
|
|
@@ -2017,11 +2380,11 @@ function formatRendersTable(result) {
|
|
|
2017
2380
|
return lines.join("\n");
|
|
2018
2381
|
}
|
|
2019
2382
|
function createInstrumentRendersCommand() {
|
|
2020
|
-
return new
|
|
2383
|
+
return new Command4("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
2021
2384
|
"--interaction <json>",
|
|
2022
2385
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
2023
2386
|
"[]"
|
|
2024
|
-
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json",
|
|
2387
|
+
).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
|
|
2025
2388
|
async (componentName, opts) => {
|
|
2026
2389
|
let interaction = [];
|
|
2027
2390
|
try {
|
|
@@ -2044,7 +2407,7 @@ function createInstrumentRendersCommand() {
|
|
|
2044
2407
|
interaction,
|
|
2045
2408
|
manifestPath: opts.manifest
|
|
2046
2409
|
});
|
|
2047
|
-
await
|
|
2410
|
+
await shutdownPool2();
|
|
2048
2411
|
if (opts.json || !isTTY()) {
|
|
2049
2412
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2050
2413
|
`);
|
|
@@ -2053,7 +2416,7 @@ function createInstrumentRendersCommand() {
|
|
|
2053
2416
|
`);
|
|
2054
2417
|
}
|
|
2055
2418
|
} catch (err) {
|
|
2056
|
-
await
|
|
2419
|
+
await shutdownPool2();
|
|
2057
2420
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2058
2421
|
`);
|
|
2059
2422
|
process.exit(1);
|
|
@@ -2062,34 +2425,35 @@ function createInstrumentRendersCommand() {
|
|
|
2062
2425
|
);
|
|
2063
2426
|
}
|
|
2064
2427
|
function createInstrumentCommand() {
|
|
2065
|
-
const instrumentCmd = new
|
|
2428
|
+
const instrumentCmd = new Command4("instrument").description(
|
|
2066
2429
|
"Structured instrumentation commands for React component analysis"
|
|
2067
2430
|
);
|
|
2068
2431
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
2069
2432
|
instrumentCmd.addCommand(createInstrumentHooksCommand());
|
|
2070
2433
|
instrumentCmd.addCommand(createInstrumentProfileCommand());
|
|
2434
|
+
instrumentCmd.addCommand(createInstrumentTreeCommand());
|
|
2071
2435
|
return instrumentCmd;
|
|
2072
2436
|
}
|
|
2073
2437
|
|
|
2074
2438
|
// src/render-commands.ts
|
|
2075
2439
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2076
|
-
import { resolve as
|
|
2440
|
+
import { resolve as resolve7 } from "path";
|
|
2077
2441
|
import {
|
|
2078
2442
|
ALL_CONTEXT_IDS,
|
|
2079
2443
|
ALL_STRESS_IDS,
|
|
2080
|
-
BrowserPool as
|
|
2444
|
+
BrowserPool as BrowserPool3,
|
|
2081
2445
|
contextAxis,
|
|
2082
2446
|
RenderMatrix,
|
|
2083
2447
|
SatoriRenderer,
|
|
2084
2448
|
safeRender,
|
|
2085
2449
|
stressAxis
|
|
2086
2450
|
} from "@agent-scope/render";
|
|
2087
|
-
import { Command as
|
|
2451
|
+
import { Command as Command5 } from "commander";
|
|
2088
2452
|
|
|
2089
2453
|
// src/tailwind-css.ts
|
|
2090
2454
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
2091
2455
|
import { createRequire } from "module";
|
|
2092
|
-
import { resolve as
|
|
2456
|
+
import { resolve as resolve6 } from "path";
|
|
2093
2457
|
var CONFIG_FILENAMES = [
|
|
2094
2458
|
".reactscope/config.json",
|
|
2095
2459
|
".reactscope/config.js",
|
|
@@ -2106,14 +2470,14 @@ var STYLE_ENTRY_CANDIDATES = [
|
|
|
2106
2470
|
var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
|
|
2107
2471
|
var compilerCache = null;
|
|
2108
2472
|
function getCachedBuild(cwd) {
|
|
2109
|
-
if (compilerCache !== null &&
|
|
2473
|
+
if (compilerCache !== null && resolve6(compilerCache.cwd) === resolve6(cwd)) {
|
|
2110
2474
|
return compilerCache.build;
|
|
2111
2475
|
}
|
|
2112
2476
|
return null;
|
|
2113
2477
|
}
|
|
2114
2478
|
function findStylesEntry(cwd) {
|
|
2115
2479
|
for (const name of CONFIG_FILENAMES) {
|
|
2116
|
-
const p =
|
|
2480
|
+
const p = resolve6(cwd, name);
|
|
2117
2481
|
if (!existsSync4(p)) continue;
|
|
2118
2482
|
try {
|
|
2119
2483
|
if (name.endsWith(".json")) {
|
|
@@ -2122,28 +2486,28 @@ function findStylesEntry(cwd) {
|
|
|
2122
2486
|
const scope = data.scope;
|
|
2123
2487
|
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
2124
2488
|
if (typeof entry === "string") {
|
|
2125
|
-
const full =
|
|
2489
|
+
const full = resolve6(cwd, entry);
|
|
2126
2490
|
if (existsSync4(full)) return full;
|
|
2127
2491
|
}
|
|
2128
2492
|
}
|
|
2129
2493
|
} catch {
|
|
2130
2494
|
}
|
|
2131
2495
|
}
|
|
2132
|
-
const pkgPath =
|
|
2496
|
+
const pkgPath = resolve6(cwd, "package.json");
|
|
2133
2497
|
if (existsSync4(pkgPath)) {
|
|
2134
2498
|
try {
|
|
2135
2499
|
const raw = readFileSync4(pkgPath, "utf-8");
|
|
2136
2500
|
const pkg = JSON.parse(raw);
|
|
2137
2501
|
const entry = pkg.scope?.stylesEntry;
|
|
2138
2502
|
if (typeof entry === "string") {
|
|
2139
|
-
const full =
|
|
2503
|
+
const full = resolve6(cwd, entry);
|
|
2140
2504
|
if (existsSync4(full)) return full;
|
|
2141
2505
|
}
|
|
2142
2506
|
} catch {
|
|
2143
2507
|
}
|
|
2144
2508
|
}
|
|
2145
2509
|
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
2146
|
-
const full =
|
|
2510
|
+
const full = resolve6(cwd, candidate);
|
|
2147
2511
|
if (existsSync4(full)) {
|
|
2148
2512
|
try {
|
|
2149
2513
|
const content = readFileSync4(full, "utf-8");
|
|
@@ -2161,7 +2525,7 @@ async function getTailwindCompiler(cwd) {
|
|
|
2161
2525
|
if (entryPath === null) return null;
|
|
2162
2526
|
let compile;
|
|
2163
2527
|
try {
|
|
2164
|
-
const require2 = createRequire(
|
|
2528
|
+
const require2 = createRequire(resolve6(cwd, "package.json"));
|
|
2165
2529
|
const tailwind = require2("tailwindcss");
|
|
2166
2530
|
const fn = tailwind.compile;
|
|
2167
2531
|
if (typeof fn !== "function") return null;
|
|
@@ -2172,8 +2536,8 @@ async function getTailwindCompiler(cwd) {
|
|
|
2172
2536
|
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
2173
2537
|
const loadStylesheet = async (id, base) => {
|
|
2174
2538
|
if (id === "tailwindcss") {
|
|
2175
|
-
const nodeModules =
|
|
2176
|
-
const tailwindCssPath =
|
|
2539
|
+
const nodeModules = resolve6(cwd, "node_modules");
|
|
2540
|
+
const tailwindCssPath = resolve6(nodeModules, "tailwindcss", "index.css");
|
|
2177
2541
|
if (!existsSync4(tailwindCssPath)) {
|
|
2178
2542
|
throw new Error(
|
|
2179
2543
|
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
@@ -2182,10 +2546,10 @@ async function getTailwindCompiler(cwd) {
|
|
|
2182
2546
|
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
2183
2547
|
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
2184
2548
|
}
|
|
2185
|
-
const full =
|
|
2549
|
+
const full = resolve6(base, id);
|
|
2186
2550
|
if (existsSync4(full)) {
|
|
2187
2551
|
const content = readFileSync4(full, "utf-8");
|
|
2188
|
-
return { path: full, base:
|
|
2552
|
+
return { path: full, base: resolve6(full, ".."), content };
|
|
2189
2553
|
}
|
|
2190
2554
|
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
2191
2555
|
};
|
|
@@ -2207,24 +2571,24 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
2207
2571
|
}
|
|
2208
2572
|
|
|
2209
2573
|
// src/render-commands.ts
|
|
2210
|
-
var
|
|
2574
|
+
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2211
2575
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2212
|
-
var
|
|
2213
|
-
async function
|
|
2214
|
-
if (
|
|
2215
|
-
|
|
2576
|
+
var _pool3 = null;
|
|
2577
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2578
|
+
if (_pool3 === null) {
|
|
2579
|
+
_pool3 = new BrowserPool3({
|
|
2216
2580
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2217
2581
|
viewportWidth,
|
|
2218
2582
|
viewportHeight
|
|
2219
2583
|
});
|
|
2220
|
-
await
|
|
2584
|
+
await _pool3.init();
|
|
2221
2585
|
}
|
|
2222
|
-
return
|
|
2586
|
+
return _pool3;
|
|
2223
2587
|
}
|
|
2224
|
-
async function
|
|
2225
|
-
if (
|
|
2226
|
-
await
|
|
2227
|
-
|
|
2588
|
+
async function shutdownPool3() {
|
|
2589
|
+
if (_pool3 !== null) {
|
|
2590
|
+
await _pool3.close();
|
|
2591
|
+
_pool3 = null;
|
|
2228
2592
|
}
|
|
2229
2593
|
}
|
|
2230
2594
|
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
@@ -2235,7 +2599,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2235
2599
|
_satori: satori,
|
|
2236
2600
|
async renderCell(props, _complexityClass) {
|
|
2237
2601
|
const startMs = performance.now();
|
|
2238
|
-
const pool = await
|
|
2602
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2239
2603
|
const htmlHarness = await buildComponentHarness(
|
|
2240
2604
|
filePath,
|
|
2241
2605
|
componentName,
|
|
@@ -2332,7 +2696,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2332
2696
|
};
|
|
2333
2697
|
}
|
|
2334
2698
|
function registerRenderSingle(renderCmd) {
|
|
2335
|
-
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",
|
|
2699
|
+
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(
|
|
2336
2700
|
async (componentName, opts) => {
|
|
2337
2701
|
try {
|
|
2338
2702
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2354,7 +2718,7 @@ Available: ${available}`
|
|
|
2354
2718
|
}
|
|
2355
2719
|
const { width, height } = parseViewport(opts.viewport);
|
|
2356
2720
|
const rootDir = process.cwd();
|
|
2357
|
-
const filePath =
|
|
2721
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2358
2722
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
2359
2723
|
process.stderr.write(
|
|
2360
2724
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
@@ -2371,7 +2735,7 @@ Available: ${available}`
|
|
|
2371
2735
|
}
|
|
2372
2736
|
}
|
|
2373
2737
|
);
|
|
2374
|
-
await
|
|
2738
|
+
await shutdownPool3();
|
|
2375
2739
|
if (outcome.crashed) {
|
|
2376
2740
|
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
2377
2741
|
`);
|
|
@@ -2384,7 +2748,7 @@ Available: ${available}`
|
|
|
2384
2748
|
}
|
|
2385
2749
|
const result = outcome.result;
|
|
2386
2750
|
if (opts.output !== void 0) {
|
|
2387
|
-
const outPath =
|
|
2751
|
+
const outPath = resolve7(process.cwd(), opts.output);
|
|
2388
2752
|
writeFileSync4(outPath, result.screenshot);
|
|
2389
2753
|
process.stdout.write(
|
|
2390
2754
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -2398,9 +2762,9 @@ Available: ${available}`
|
|
|
2398
2762
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
2399
2763
|
`);
|
|
2400
2764
|
} else if (fmt === "file") {
|
|
2401
|
-
const dir =
|
|
2765
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2402
2766
|
mkdirSync3(dir, { recursive: true });
|
|
2403
|
-
const outPath =
|
|
2767
|
+
const outPath = resolve7(dir, `${componentName}.png`);
|
|
2404
2768
|
writeFileSync4(outPath, result.screenshot);
|
|
2405
2769
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
2406
2770
|
process.stdout.write(
|
|
@@ -2408,9 +2772,9 @@ Available: ${available}`
|
|
|
2408
2772
|
`
|
|
2409
2773
|
);
|
|
2410
2774
|
} else {
|
|
2411
|
-
const dir =
|
|
2775
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2412
2776
|
mkdirSync3(dir, { recursive: true });
|
|
2413
|
-
const outPath =
|
|
2777
|
+
const outPath = resolve7(dir, `${componentName}.png`);
|
|
2414
2778
|
writeFileSync4(outPath, result.screenshot);
|
|
2415
2779
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
2416
2780
|
process.stdout.write(
|
|
@@ -2419,7 +2783,7 @@ Available: ${available}`
|
|
|
2419
2783
|
);
|
|
2420
2784
|
}
|
|
2421
2785
|
} catch (err) {
|
|
2422
|
-
await
|
|
2786
|
+
await shutdownPool3();
|
|
2423
2787
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2424
2788
|
`);
|
|
2425
2789
|
process.exit(1);
|
|
@@ -2431,7 +2795,7 @@ function registerRenderMatrix(renderCmd) {
|
|
|
2431
2795
|
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(
|
|
2432
2796
|
"--contexts <ids>",
|
|
2433
2797
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
2434
|
-
).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",
|
|
2798
|
+
).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(
|
|
2435
2799
|
async (componentName, opts) => {
|
|
2436
2800
|
try {
|
|
2437
2801
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2446,7 +2810,7 @@ Available: ${available}`
|
|
|
2446
2810
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
2447
2811
|
const { width, height } = { width: 375, height: 812 };
|
|
2448
2812
|
const rootDir = process.cwd();
|
|
2449
|
-
const filePath =
|
|
2813
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2450
2814
|
const renderer = buildRenderer(filePath, componentName, width, height);
|
|
2451
2815
|
const axes = [];
|
|
2452
2816
|
if (opts.axes !== void 0) {
|
|
@@ -2504,7 +2868,7 @@ Available: ${available}`
|
|
|
2504
2868
|
concurrency
|
|
2505
2869
|
});
|
|
2506
2870
|
const result = await matrix.render();
|
|
2507
|
-
await
|
|
2871
|
+
await shutdownPool3();
|
|
2508
2872
|
process.stderr.write(
|
|
2509
2873
|
`Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
|
|
2510
2874
|
`
|
|
@@ -2513,7 +2877,7 @@ Available: ${available}`
|
|
|
2513
2877
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2514
2878
|
const gen = new SpriteSheetGenerator();
|
|
2515
2879
|
const sheet = await gen.generate(result);
|
|
2516
|
-
const spritePath =
|
|
2880
|
+
const spritePath = resolve7(process.cwd(), opts.sprite);
|
|
2517
2881
|
writeFileSync4(spritePath, sheet.png);
|
|
2518
2882
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
2519
2883
|
`);
|
|
@@ -2523,9 +2887,9 @@ Available: ${available}`
|
|
|
2523
2887
|
const { SpriteSheetGenerator } = await import("@agent-scope/render");
|
|
2524
2888
|
const gen = new SpriteSheetGenerator();
|
|
2525
2889
|
const sheet = await gen.generate(result);
|
|
2526
|
-
const dir =
|
|
2890
|
+
const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2527
2891
|
mkdirSync3(dir, { recursive: true });
|
|
2528
|
-
const outPath =
|
|
2892
|
+
const outPath = resolve7(dir, `${componentName}-matrix.png`);
|
|
2529
2893
|
writeFileSync4(outPath, sheet.png);
|
|
2530
2894
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
2531
2895
|
process.stdout.write(
|
|
@@ -2549,7 +2913,7 @@ Available: ${available}`
|
|
|
2549
2913
|
process.stdout.write(formatMatrixCsv(componentName, result));
|
|
2550
2914
|
}
|
|
2551
2915
|
} catch (err) {
|
|
2552
|
-
await
|
|
2916
|
+
await shutdownPool3();
|
|
2553
2917
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2554
2918
|
`);
|
|
2555
2919
|
process.exit(1);
|
|
@@ -2558,7 +2922,7 @@ Available: ${available}`
|
|
|
2558
2922
|
);
|
|
2559
2923
|
}
|
|
2560
2924
|
function registerRenderAll(renderCmd) {
|
|
2561
|
-
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",
|
|
2925
|
+
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(
|
|
2562
2926
|
async (opts) => {
|
|
2563
2927
|
try {
|
|
2564
2928
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -2569,7 +2933,7 @@ function registerRenderAll(renderCmd) {
|
|
|
2569
2933
|
return;
|
|
2570
2934
|
}
|
|
2571
2935
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
2572
|
-
const outputDir =
|
|
2936
|
+
const outputDir = resolve7(process.cwd(), opts.outputDir);
|
|
2573
2937
|
mkdirSync3(outputDir, { recursive: true });
|
|
2574
2938
|
const rootDir = process.cwd();
|
|
2575
2939
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
@@ -2579,7 +2943,7 @@ function registerRenderAll(renderCmd) {
|
|
|
2579
2943
|
const renderOne = async (name) => {
|
|
2580
2944
|
const descriptor = manifest.components[name];
|
|
2581
2945
|
if (descriptor === void 0) return;
|
|
2582
|
-
const filePath =
|
|
2946
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2583
2947
|
const renderer = buildRenderer(filePath, name, 375, 812);
|
|
2584
2948
|
const outcome = await safeRender(
|
|
2585
2949
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
@@ -2602,7 +2966,7 @@ function registerRenderAll(renderCmd) {
|
|
|
2602
2966
|
success: false,
|
|
2603
2967
|
errorMessage: outcome.error.message
|
|
2604
2968
|
});
|
|
2605
|
-
const errPath =
|
|
2969
|
+
const errPath = resolve7(outputDir, `${name}.error.json`);
|
|
2606
2970
|
writeFileSync4(
|
|
2607
2971
|
errPath,
|
|
2608
2972
|
JSON.stringify(
|
|
@@ -2620,9 +2984,9 @@ function registerRenderAll(renderCmd) {
|
|
|
2620
2984
|
}
|
|
2621
2985
|
const result = outcome.result;
|
|
2622
2986
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
2623
|
-
const pngPath =
|
|
2987
|
+
const pngPath = resolve7(outputDir, `${name}.png`);
|
|
2624
2988
|
writeFileSync4(pngPath, result.screenshot);
|
|
2625
|
-
const jsonPath =
|
|
2989
|
+
const jsonPath = resolve7(outputDir, `${name}.json`);
|
|
2626
2990
|
writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
2627
2991
|
if (isTTY()) {
|
|
2628
2992
|
process.stdout.write(
|
|
@@ -2646,13 +3010,13 @@ function registerRenderAll(renderCmd) {
|
|
|
2646
3010
|
workers.push(worker());
|
|
2647
3011
|
}
|
|
2648
3012
|
await Promise.all(workers);
|
|
2649
|
-
await
|
|
3013
|
+
await shutdownPool3();
|
|
2650
3014
|
process.stderr.write("\n");
|
|
2651
3015
|
const summary = formatSummaryText(results, outputDir);
|
|
2652
3016
|
process.stderr.write(`${summary}
|
|
2653
3017
|
`);
|
|
2654
3018
|
} catch (err) {
|
|
2655
|
-
await
|
|
3019
|
+
await shutdownPool3();
|
|
2656
3020
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2657
3021
|
`);
|
|
2658
3022
|
process.exit(1);
|
|
@@ -2685,7 +3049,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
2685
3049
|
return "json";
|
|
2686
3050
|
}
|
|
2687
3051
|
function createRenderCommand() {
|
|
2688
|
-
const renderCmd = new
|
|
3052
|
+
const renderCmd = new Command5("render").description(
|
|
2689
3053
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
2690
3054
|
);
|
|
2691
3055
|
registerRenderSingle(renderCmd);
|
|
@@ -2696,31 +3060,31 @@ function createRenderCommand() {
|
|
|
2696
3060
|
|
|
2697
3061
|
// src/report/baseline.ts
|
|
2698
3062
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2699
|
-
import { resolve as
|
|
3063
|
+
import { resolve as resolve8 } from "path";
|
|
2700
3064
|
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
2701
|
-
import { BrowserPool as
|
|
3065
|
+
import { BrowserPool as BrowserPool4, safeRender as safeRender2 } from "@agent-scope/render";
|
|
2702
3066
|
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
2703
3067
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2704
|
-
var
|
|
2705
|
-
async function
|
|
2706
|
-
if (
|
|
2707
|
-
|
|
3068
|
+
var _pool4 = null;
|
|
3069
|
+
async function getPool4(viewportWidth, viewportHeight) {
|
|
3070
|
+
if (_pool4 === null) {
|
|
3071
|
+
_pool4 = new BrowserPool4({
|
|
2708
3072
|
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2709
3073
|
viewportWidth,
|
|
2710
3074
|
viewportHeight
|
|
2711
3075
|
});
|
|
2712
|
-
await
|
|
3076
|
+
await _pool4.init();
|
|
2713
3077
|
}
|
|
2714
|
-
return
|
|
3078
|
+
return _pool4;
|
|
2715
3079
|
}
|
|
2716
|
-
async function
|
|
2717
|
-
if (
|
|
2718
|
-
await
|
|
2719
|
-
|
|
3080
|
+
async function shutdownPool4() {
|
|
3081
|
+
if (_pool4 !== null) {
|
|
3082
|
+
await _pool4.close();
|
|
3083
|
+
_pool4 = null;
|
|
2720
3084
|
}
|
|
2721
3085
|
}
|
|
2722
3086
|
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2723
|
-
const pool = await
|
|
3087
|
+
const pool = await getPool4(viewportWidth, viewportHeight);
|
|
2724
3088
|
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2725
3089
|
const slot = await pool.acquire();
|
|
2726
3090
|
const { page } = slot;
|
|
@@ -2845,20 +3209,20 @@ async function runBaseline(options = {}) {
|
|
|
2845
3209
|
} = options;
|
|
2846
3210
|
const startTime = performance.now();
|
|
2847
3211
|
const rootDir = process.cwd();
|
|
2848
|
-
const baselineDir =
|
|
2849
|
-
const rendersDir =
|
|
3212
|
+
const baselineDir = resolve8(rootDir, outputDir);
|
|
3213
|
+
const rendersDir = resolve8(baselineDir, "renders");
|
|
2850
3214
|
if (existsSync5(baselineDir)) {
|
|
2851
3215
|
rmSync(baselineDir, { recursive: true, force: true });
|
|
2852
3216
|
}
|
|
2853
3217
|
mkdirSync4(rendersDir, { recursive: true });
|
|
2854
3218
|
let manifest;
|
|
2855
3219
|
if (manifestPath !== void 0) {
|
|
2856
|
-
const { readFileSync:
|
|
2857
|
-
const absPath =
|
|
3220
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
3221
|
+
const absPath = resolve8(rootDir, manifestPath);
|
|
2858
3222
|
if (!existsSync5(absPath)) {
|
|
2859
3223
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2860
3224
|
}
|
|
2861
|
-
manifest = JSON.parse(
|
|
3225
|
+
manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
|
|
2862
3226
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2863
3227
|
`);
|
|
2864
3228
|
} else {
|
|
@@ -2868,7 +3232,7 @@ async function runBaseline(options = {}) {
|
|
|
2868
3232
|
process.stderr.write(`Found ${count} components.
|
|
2869
3233
|
`);
|
|
2870
3234
|
}
|
|
2871
|
-
writeFileSync5(
|
|
3235
|
+
writeFileSync5(resolve8(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2872
3236
|
let componentNames = Object.keys(manifest.components);
|
|
2873
3237
|
if (componentsGlob !== void 0) {
|
|
2874
3238
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -2889,7 +3253,7 @@ async function runBaseline(options = {}) {
|
|
|
2889
3253
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2890
3254
|
};
|
|
2891
3255
|
writeFileSync5(
|
|
2892
|
-
|
|
3256
|
+
resolve8(baselineDir, "compliance.json"),
|
|
2893
3257
|
JSON.stringify(emptyReport, null, 2),
|
|
2894
3258
|
"utf-8"
|
|
2895
3259
|
);
|
|
@@ -2910,7 +3274,7 @@ async function runBaseline(options = {}) {
|
|
|
2910
3274
|
const renderOne = async (name) => {
|
|
2911
3275
|
const descriptor = manifest.components[name];
|
|
2912
3276
|
if (descriptor === void 0) return;
|
|
2913
|
-
const filePath =
|
|
3277
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2914
3278
|
const outcome = await safeRender2(
|
|
2915
3279
|
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2916
3280
|
{
|
|
@@ -2929,7 +3293,7 @@ async function runBaseline(options = {}) {
|
|
|
2929
3293
|
}
|
|
2930
3294
|
if (outcome.crashed) {
|
|
2931
3295
|
failureCount++;
|
|
2932
|
-
const errPath =
|
|
3296
|
+
const errPath = resolve8(rendersDir, `${name}.error.json`);
|
|
2933
3297
|
writeFileSync5(
|
|
2934
3298
|
errPath,
|
|
2935
3299
|
JSON.stringify(
|
|
@@ -2947,10 +3311,10 @@ async function runBaseline(options = {}) {
|
|
|
2947
3311
|
return;
|
|
2948
3312
|
}
|
|
2949
3313
|
const result = outcome.result;
|
|
2950
|
-
writeFileSync5(
|
|
3314
|
+
writeFileSync5(resolve8(rendersDir, `${name}.png`), result.screenshot);
|
|
2951
3315
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2952
3316
|
writeFileSync5(
|
|
2953
|
-
|
|
3317
|
+
resolve8(rendersDir, `${name}.json`),
|
|
2954
3318
|
JSON.stringify(jsonOutput, null, 2),
|
|
2955
3319
|
"utf-8"
|
|
2956
3320
|
);
|
|
@@ -2970,7 +3334,7 @@ async function runBaseline(options = {}) {
|
|
|
2970
3334
|
workers.push(worker());
|
|
2971
3335
|
}
|
|
2972
3336
|
await Promise.all(workers);
|
|
2973
|
-
await
|
|
3337
|
+
await shutdownPool4();
|
|
2974
3338
|
if (isTTY()) {
|
|
2975
3339
|
process.stderr.write("\n");
|
|
2976
3340
|
}
|
|
@@ -2978,7 +3342,7 @@ async function runBaseline(options = {}) {
|
|
|
2978
3342
|
const engine = new ComplianceEngine(resolver);
|
|
2979
3343
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2980
3344
|
writeFileSync5(
|
|
2981
|
-
|
|
3345
|
+
resolve8(baselineDir, "compliance.json"),
|
|
2982
3346
|
JSON.stringify(batchReport, null, 2),
|
|
2983
3347
|
"utf-8"
|
|
2984
3348
|
);
|
|
@@ -3020,11 +3384,491 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3020
3384
|
);
|
|
3021
3385
|
}
|
|
3022
3386
|
|
|
3387
|
+
// src/report/diff.ts
|
|
3388
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3389
|
+
import { resolve as resolve9 } from "path";
|
|
3390
|
+
import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
|
|
3391
|
+
import { BrowserPool as BrowserPool5, safeRender as safeRender3 } from "@agent-scope/render";
|
|
3392
|
+
import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
|
|
3393
|
+
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3394
|
+
function loadBaselineCompliance(baselineDir) {
|
|
3395
|
+
const compliancePath = resolve9(baselineDir, "compliance.json");
|
|
3396
|
+
if (!existsSync6(compliancePath)) return null;
|
|
3397
|
+
const raw = JSON.parse(readFileSync5(compliancePath, "utf-8"));
|
|
3398
|
+
return raw;
|
|
3399
|
+
}
|
|
3400
|
+
function loadBaselineRenderJson(baselineDir, componentName) {
|
|
3401
|
+
const jsonPath = resolve9(baselineDir, "renders", `${componentName}.json`);
|
|
3402
|
+
if (!existsSync6(jsonPath)) return null;
|
|
3403
|
+
return JSON.parse(readFileSync5(jsonPath, "utf-8"));
|
|
3404
|
+
}
|
|
3405
|
+
var _pool5 = null;
|
|
3406
|
+
async function getPool5(viewportWidth, viewportHeight) {
|
|
3407
|
+
if (_pool5 === null) {
|
|
3408
|
+
_pool5 = new BrowserPool5({
|
|
3409
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
3410
|
+
viewportWidth,
|
|
3411
|
+
viewportHeight
|
|
3412
|
+
});
|
|
3413
|
+
await _pool5.init();
|
|
3414
|
+
}
|
|
3415
|
+
return _pool5;
|
|
3416
|
+
}
|
|
3417
|
+
async function shutdownPool5() {
|
|
3418
|
+
if (_pool5 !== null) {
|
|
3419
|
+
await _pool5.close();
|
|
3420
|
+
_pool5 = null;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
3424
|
+
const pool = await getPool5(viewportWidth, viewportHeight);
|
|
3425
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
3426
|
+
const slot = await pool.acquire();
|
|
3427
|
+
const { page } = slot;
|
|
3428
|
+
try {
|
|
3429
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
3430
|
+
await page.waitForFunction(
|
|
3431
|
+
() => {
|
|
3432
|
+
const w = window;
|
|
3433
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
3434
|
+
},
|
|
3435
|
+
{ timeout: 15e3 }
|
|
3436
|
+
);
|
|
3437
|
+
const renderError = await page.evaluate(() => {
|
|
3438
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
3439
|
+
});
|
|
3440
|
+
if (renderError !== null) {
|
|
3441
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
3442
|
+
}
|
|
3443
|
+
const rootDir = process.cwd();
|
|
3444
|
+
const classes = await page.evaluate(() => {
|
|
3445
|
+
const set = /* @__PURE__ */ new Set();
|
|
3446
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
3447
|
+
for (const c of el.className.split(/\s+/)) {
|
|
3448
|
+
if (c) set.add(c);
|
|
3449
|
+
}
|
|
3450
|
+
});
|
|
3451
|
+
return [...set];
|
|
3452
|
+
});
|
|
3453
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
3454
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
3455
|
+
await page.addStyleTag({ content: projectCss });
|
|
3456
|
+
}
|
|
3457
|
+
const startMs = performance.now();
|
|
3458
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
3459
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
3460
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
3461
|
+
throw new Error(
|
|
3462
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
const PAD = 24;
|
|
3466
|
+
const MIN_W = 320;
|
|
3467
|
+
const MIN_H = 200;
|
|
3468
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3469
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3470
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
3471
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
3472
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
3473
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
3474
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3475
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3476
|
+
const screenshot = await page.screenshot({
|
|
3477
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3478
|
+
type: "png"
|
|
3479
|
+
});
|
|
3480
|
+
const computedStylesRaw = {};
|
|
3481
|
+
const styles = await page.evaluate((sel) => {
|
|
3482
|
+
const el = document.querySelector(sel);
|
|
3483
|
+
if (el === null) return {};
|
|
3484
|
+
const computed = window.getComputedStyle(el);
|
|
3485
|
+
const out = {};
|
|
3486
|
+
for (const prop of [
|
|
3487
|
+
"display",
|
|
3488
|
+
"width",
|
|
3489
|
+
"height",
|
|
3490
|
+
"color",
|
|
3491
|
+
"backgroundColor",
|
|
3492
|
+
"fontSize",
|
|
3493
|
+
"fontFamily",
|
|
3494
|
+
"padding",
|
|
3495
|
+
"margin"
|
|
3496
|
+
]) {
|
|
3497
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
3498
|
+
}
|
|
3499
|
+
return out;
|
|
3500
|
+
}, "[data-reactscope-root] > *");
|
|
3501
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
3502
|
+
const renderTimeMs = performance.now() - startMs;
|
|
3503
|
+
return {
|
|
3504
|
+
screenshot,
|
|
3505
|
+
width: Math.round(safeW),
|
|
3506
|
+
height: Math.round(safeH),
|
|
3507
|
+
renderTimeMs,
|
|
3508
|
+
computedStyles: computedStylesRaw
|
|
3509
|
+
};
|
|
3510
|
+
} finally {
|
|
3511
|
+
pool.release(slot);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function extractComputedStyles2(computedStylesRaw) {
|
|
3515
|
+
const flat = {};
|
|
3516
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
3517
|
+
Object.assign(flat, styles);
|
|
3518
|
+
}
|
|
3519
|
+
const colors = {};
|
|
3520
|
+
const spacing = {};
|
|
3521
|
+
const typography = {};
|
|
3522
|
+
const borders = {};
|
|
3523
|
+
const shadows = {};
|
|
3524
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
3525
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
3526
|
+
colors[prop] = value;
|
|
3527
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
3528
|
+
spacing[prop] = value;
|
|
3529
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
3530
|
+
typography[prop] = value;
|
|
3531
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
3532
|
+
borders[prop] = value;
|
|
3533
|
+
} else if (prop === "boxShadow") {
|
|
3534
|
+
shadows[prop] = value;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return { colors, spacing, typography, borders, shadows };
|
|
3538
|
+
}
|
|
3539
|
+
function classifyComponent(entry, regressionThreshold) {
|
|
3540
|
+
if (entry.renderFailed) return "unchanged";
|
|
3541
|
+
if (entry.baselineCompliance === null && entry.currentCompliance !== null) {
|
|
3542
|
+
return "added";
|
|
3543
|
+
}
|
|
3544
|
+
if (entry.baselineCompliance !== null && entry.currentCompliance === null) {
|
|
3545
|
+
return "removed";
|
|
3546
|
+
}
|
|
3547
|
+
const delta = entry.complianceDelta;
|
|
3548
|
+
if (delta !== null) {
|
|
3549
|
+
if (delta <= -regressionThreshold) return "compliance_regressed";
|
|
3550
|
+
if (delta >= regressionThreshold) return "compliance_improved";
|
|
3551
|
+
}
|
|
3552
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3553
|
+
const dw = Math.abs(entry.currentDimensions.width - entry.baselineDimensions.width);
|
|
3554
|
+
const dh = Math.abs(entry.currentDimensions.height - entry.baselineDimensions.height);
|
|
3555
|
+
if (dw > 10 || dh > 10) return "size_changed";
|
|
3556
|
+
}
|
|
3557
|
+
return "unchanged";
|
|
3558
|
+
}
|
|
3559
|
+
async function runDiff(options = {}) {
|
|
3560
|
+
const {
|
|
3561
|
+
baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
|
|
3562
|
+
componentsGlob,
|
|
3563
|
+
manifestPath,
|
|
3564
|
+
viewportWidth = 375,
|
|
3565
|
+
viewportHeight = 812,
|
|
3566
|
+
regressionThreshold = 0.01
|
|
3567
|
+
} = options;
|
|
3568
|
+
const startTime = performance.now();
|
|
3569
|
+
const rootDir = process.cwd();
|
|
3570
|
+
const baselineDir = resolve9(rootDir, baselineDirRaw);
|
|
3571
|
+
if (!existsSync6(baselineDir)) {
|
|
3572
|
+
throw new Error(
|
|
3573
|
+
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
const baselineManifestPath = resolve9(baselineDir, "manifest.json");
|
|
3577
|
+
if (!existsSync6(baselineManifestPath)) {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
const baselineManifest = JSON.parse(readFileSync5(baselineManifestPath, "utf-8"));
|
|
3583
|
+
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
3584
|
+
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
3585
|
+
process.stderr.write(
|
|
3586
|
+
`Comparing against baseline at ${baselineDir} (${baselineComponentNames.size} components)
|
|
3587
|
+
`
|
|
3588
|
+
);
|
|
3589
|
+
let currentManifest;
|
|
3590
|
+
if (manifestPath !== void 0) {
|
|
3591
|
+
const absPath = resolve9(rootDir, manifestPath);
|
|
3592
|
+
if (!existsSync6(absPath)) {
|
|
3593
|
+
throw new Error(`Manifest not found at "${absPath}".`);
|
|
3594
|
+
}
|
|
3595
|
+
currentManifest = JSON.parse(readFileSync5(absPath, "utf-8"));
|
|
3596
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3597
|
+
`);
|
|
3598
|
+
} else {
|
|
3599
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
3600
|
+
currentManifest = await generateManifest3({ rootDir });
|
|
3601
|
+
const count = Object.keys(currentManifest.components).length;
|
|
3602
|
+
process.stderr.write(`Found ${count} components.
|
|
3603
|
+
`);
|
|
3604
|
+
}
|
|
3605
|
+
let componentNames = Object.keys(currentManifest.components);
|
|
3606
|
+
if (componentsGlob !== void 0) {
|
|
3607
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
3608
|
+
process.stderr.write(
|
|
3609
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
3610
|
+
`
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
const removedNames = [...baselineComponentNames].filter(
|
|
3614
|
+
(name) => !currentManifest.components[name] && (componentsGlob === void 0 || matchGlob(componentsGlob, name))
|
|
3615
|
+
);
|
|
3616
|
+
const total = componentNames.length;
|
|
3617
|
+
process.stderr.write(`Rendering ${total} components for diff\u2026
|
|
3618
|
+
`);
|
|
3619
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
3620
|
+
const currentRenderMeta = /* @__PURE__ */ new Map();
|
|
3621
|
+
const renderFailures = /* @__PURE__ */ new Set();
|
|
3622
|
+
let completed = 0;
|
|
3623
|
+
const CONCURRENCY = 4;
|
|
3624
|
+
let nextIdx = 0;
|
|
3625
|
+
const renderOne = async (name) => {
|
|
3626
|
+
const descriptor = currentManifest.components[name];
|
|
3627
|
+
if (descriptor === void 0) return;
|
|
3628
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3629
|
+
const outcome = await safeRender3(
|
|
3630
|
+
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3631
|
+
{
|
|
3632
|
+
props: {},
|
|
3633
|
+
sourceLocation: {
|
|
3634
|
+
file: descriptor.filePath,
|
|
3635
|
+
line: descriptor.loc.start,
|
|
3636
|
+
column: 0
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
completed++;
|
|
3641
|
+
const pct = Math.round(completed / total * 100);
|
|
3642
|
+
if (isTTY()) {
|
|
3643
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
3644
|
+
}
|
|
3645
|
+
if (outcome.crashed) {
|
|
3646
|
+
renderFailures.add(name);
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
const result = outcome.result;
|
|
3650
|
+
currentRenderMeta.set(name, {
|
|
3651
|
+
width: result.width,
|
|
3652
|
+
height: result.height,
|
|
3653
|
+
renderTimeMs: result.renderTimeMs
|
|
3654
|
+
});
|
|
3655
|
+
computedStylesMap.set(name, extractComputedStyles2(result.computedStyles));
|
|
3656
|
+
};
|
|
3657
|
+
if (total > 0) {
|
|
3658
|
+
const worker = async () => {
|
|
3659
|
+
while (nextIdx < componentNames.length) {
|
|
3660
|
+
const i = nextIdx++;
|
|
3661
|
+
const name = componentNames[i];
|
|
3662
|
+
if (name !== void 0) {
|
|
3663
|
+
await renderOne(name);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
const workers = [];
|
|
3668
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
3669
|
+
workers.push(worker());
|
|
3670
|
+
}
|
|
3671
|
+
await Promise.all(workers);
|
|
3672
|
+
}
|
|
3673
|
+
await shutdownPool5();
|
|
3674
|
+
if (isTTY() && total > 0) {
|
|
3675
|
+
process.stderr.write("\n");
|
|
3676
|
+
}
|
|
3677
|
+
const resolver = new TokenResolver2([]);
|
|
3678
|
+
const engine = new ComplianceEngine2(resolver);
|
|
3679
|
+
const currentBatchReport = engine.auditBatch(computedStylesMap);
|
|
3680
|
+
const entries = [];
|
|
3681
|
+
for (const name of componentNames) {
|
|
3682
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3683
|
+
const currentComp = currentBatchReport.components[name] ?? null;
|
|
3684
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3685
|
+
const currentMeta = currentRenderMeta.get(name) ?? null;
|
|
3686
|
+
const failed = renderFailures.has(name);
|
|
3687
|
+
const baselineComplianceScore = baselineComp?.aggregateCompliance ?? null;
|
|
3688
|
+
const currentComplianceScore = currentComp?.compliance ?? null;
|
|
3689
|
+
const delta = baselineComplianceScore !== null && currentComplianceScore !== null ? currentComplianceScore - baselineComplianceScore : null;
|
|
3690
|
+
const partial = {
|
|
3691
|
+
name,
|
|
3692
|
+
baselineCompliance: baselineComplianceScore,
|
|
3693
|
+
currentCompliance: currentComplianceScore,
|
|
3694
|
+
complianceDelta: delta,
|
|
3695
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3696
|
+
currentDimensions: currentMeta !== null ? { width: currentMeta.width, height: currentMeta.height } : null,
|
|
3697
|
+
renderTimeMs: currentMeta?.renderTimeMs ?? null,
|
|
3698
|
+
renderFailed: failed
|
|
3699
|
+
};
|
|
3700
|
+
entries.push({ ...partial, status: classifyComponent(partial, regressionThreshold) });
|
|
3701
|
+
}
|
|
3702
|
+
for (const name of removedNames) {
|
|
3703
|
+
const baselineComp = baselineCompliance?.components[name] ?? null;
|
|
3704
|
+
const baselineMeta = loadBaselineRenderJson(baselineDir, name);
|
|
3705
|
+
entries.push({
|
|
3706
|
+
name,
|
|
3707
|
+
status: "removed",
|
|
3708
|
+
baselineCompliance: baselineComp?.aggregateCompliance ?? null,
|
|
3709
|
+
currentCompliance: null,
|
|
3710
|
+
complianceDelta: null,
|
|
3711
|
+
baselineDimensions: baselineMeta !== null ? { width: baselineMeta.width, height: baselineMeta.height } : null,
|
|
3712
|
+
currentDimensions: null,
|
|
3713
|
+
renderTimeMs: null,
|
|
3714
|
+
renderFailed: false
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
const summary = {
|
|
3718
|
+
total: entries.length,
|
|
3719
|
+
added: entries.filter((e) => e.status === "added").length,
|
|
3720
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
3721
|
+
unchanged: entries.filter((e) => e.status === "unchanged").length,
|
|
3722
|
+
complianceRegressed: entries.filter((e) => e.status === "compliance_regressed").length,
|
|
3723
|
+
complianceImproved: entries.filter((e) => e.status === "compliance_improved").length,
|
|
3724
|
+
sizeChanged: entries.filter((e) => e.status === "size_changed").length,
|
|
3725
|
+
renderFailed: entries.filter((e) => e.renderFailed).length
|
|
3726
|
+
};
|
|
3727
|
+
const hasRegressions = summary.complianceRegressed > 0 || summary.removed > 0 || summary.renderFailed > 0;
|
|
3728
|
+
const wallClockMs = performance.now() - startTime;
|
|
3729
|
+
return {
|
|
3730
|
+
diffedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3731
|
+
baselineDir,
|
|
3732
|
+
summary,
|
|
3733
|
+
components: entries,
|
|
3734
|
+
baselineAggregateCompliance: baselineCompliance?.aggregateCompliance ?? 0,
|
|
3735
|
+
currentAggregateCompliance: currentBatchReport.aggregateCompliance,
|
|
3736
|
+
hasRegressions,
|
|
3737
|
+
wallClockMs
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
var STATUS_ICON = {
|
|
3741
|
+
added: "+",
|
|
3742
|
+
removed: "-",
|
|
3743
|
+
unchanged: " ",
|
|
3744
|
+
compliance_regressed: "\u2193",
|
|
3745
|
+
compliance_improved: "\u2191",
|
|
3746
|
+
size_changed: "~"
|
|
3747
|
+
};
|
|
3748
|
+
var STATUS_LABEL = {
|
|
3749
|
+
added: "added",
|
|
3750
|
+
removed: "removed",
|
|
3751
|
+
unchanged: "ok",
|
|
3752
|
+
compliance_regressed: "regressed",
|
|
3753
|
+
compliance_improved: "improved",
|
|
3754
|
+
size_changed: "size changed"
|
|
3755
|
+
};
|
|
3756
|
+
function formatDiffReport(result) {
|
|
3757
|
+
const lines = [];
|
|
3758
|
+
const title = "Scope Report Diff";
|
|
3759
|
+
const rule2 = "\u2501".repeat(Math.max(title.length, 40));
|
|
3760
|
+
lines.push(title, rule2);
|
|
3761
|
+
const complianceDelta = result.currentAggregateCompliance - result.baselineAggregateCompliance;
|
|
3762
|
+
const complianceSign = complianceDelta >= 0 ? "+" : "";
|
|
3763
|
+
lines.push(
|
|
3764
|
+
`Baseline compliance: ${(result.baselineAggregateCompliance * 100).toFixed(1)}%`,
|
|
3765
|
+
`Current compliance: ${(result.currentAggregateCompliance * 100).toFixed(1)}%`,
|
|
3766
|
+
`Delta: ${complianceSign}${(complianceDelta * 100).toFixed(1)}%`,
|
|
3767
|
+
""
|
|
3768
|
+
);
|
|
3769
|
+
const s = result.summary;
|
|
3770
|
+
lines.push(
|
|
3771
|
+
`Components: ${s.total} total ` + [
|
|
3772
|
+
s.added > 0 ? `${s.added} added` : "",
|
|
3773
|
+
s.removed > 0 ? `${s.removed} removed` : "",
|
|
3774
|
+
s.complianceRegressed > 0 ? `${s.complianceRegressed} regressed` : "",
|
|
3775
|
+
s.complianceImproved > 0 ? `${s.complianceImproved} improved` : "",
|
|
3776
|
+
s.sizeChanged > 0 ? `${s.sizeChanged} size changed` : "",
|
|
3777
|
+
s.renderFailed > 0 ? `${s.renderFailed} failed` : ""
|
|
3778
|
+
].filter(Boolean).join(" "),
|
|
3779
|
+
""
|
|
3780
|
+
);
|
|
3781
|
+
const notable = result.components.filter((e) => e.status !== "unchanged");
|
|
3782
|
+
if (notable.length === 0) {
|
|
3783
|
+
lines.push(" No changes detected.");
|
|
3784
|
+
} else {
|
|
3785
|
+
const nameWidth = Math.max(9, ...notable.map((e) => e.name.length));
|
|
3786
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${"STATUS".padEnd(13)} ${"COMPLIANCE \u0394".padEnd(13)} DIMENSIONS`;
|
|
3787
|
+
const divider = "-".repeat(header.length);
|
|
3788
|
+
lines.push(header, divider);
|
|
3789
|
+
for (const entry of notable) {
|
|
3790
|
+
const icon = STATUS_ICON[entry.status];
|
|
3791
|
+
const label = STATUS_LABEL[entry.status].padEnd(13);
|
|
3792
|
+
const name = entry.name.padEnd(nameWidth);
|
|
3793
|
+
let complianceStr = "\u2014".padEnd(13);
|
|
3794
|
+
if (entry.complianceDelta !== null) {
|
|
3795
|
+
const sign = entry.complianceDelta >= 0 ? "+" : "";
|
|
3796
|
+
complianceStr = `${sign}${(entry.complianceDelta * 100).toFixed(1)}%`.padEnd(13);
|
|
3797
|
+
}
|
|
3798
|
+
let dimStr = "\u2014";
|
|
3799
|
+
if (entry.baselineDimensions !== null && entry.currentDimensions !== null) {
|
|
3800
|
+
const b = entry.baselineDimensions;
|
|
3801
|
+
const c = entry.currentDimensions;
|
|
3802
|
+
if (b.width !== c.width || b.height !== c.height) {
|
|
3803
|
+
dimStr = `${b.width}\xD7${b.height} \u2192 ${c.width}\xD7${c.height}`;
|
|
3804
|
+
} else {
|
|
3805
|
+
dimStr = `${c.width}\xD7${c.height}`;
|
|
3806
|
+
}
|
|
3807
|
+
} else if (entry.currentDimensions !== null) {
|
|
3808
|
+
dimStr = `${entry.currentDimensions.width}\xD7${entry.currentDimensions.height}`;
|
|
3809
|
+
} else if (entry.baselineDimensions !== null) {
|
|
3810
|
+
dimStr = `${entry.baselineDimensions.width}\xD7${entry.baselineDimensions.height} (removed)`;
|
|
3811
|
+
}
|
|
3812
|
+
if (entry.renderFailed) {
|
|
3813
|
+
dimStr = "render failed";
|
|
3814
|
+
}
|
|
3815
|
+
lines.push(`${icon} ${name} ${label} ${complianceStr} ${dimStr}`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
lines.push(
|
|
3819
|
+
"",
|
|
3820
|
+
rule2,
|
|
3821
|
+
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`
|
|
3822
|
+
);
|
|
3823
|
+
return lines.join("\n");
|
|
3824
|
+
}
|
|
3825
|
+
function registerDiffSubCommand(reportCmd) {
|
|
3826
|
+
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(
|
|
3827
|
+
"--regression-threshold <n>",
|
|
3828
|
+
"Minimum compliance drop (0\u20131) to classify as a regression",
|
|
3829
|
+
"0.01"
|
|
3830
|
+
).action(
|
|
3831
|
+
async (opts) => {
|
|
3832
|
+
try {
|
|
3833
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
3834
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
3835
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
3836
|
+
const regressionThreshold = Number.parseFloat(opts.regressionThreshold);
|
|
3837
|
+
const result = await runDiff({
|
|
3838
|
+
baselineDir: opts.baseline,
|
|
3839
|
+
componentsGlob: opts.components,
|
|
3840
|
+
manifestPath: opts.manifest,
|
|
3841
|
+
viewportWidth,
|
|
3842
|
+
viewportHeight,
|
|
3843
|
+
regressionThreshold
|
|
3844
|
+
});
|
|
3845
|
+
if (opts.output !== void 0) {
|
|
3846
|
+
writeFileSync6(opts.output, JSON.stringify(result, null, 2), "utf-8");
|
|
3847
|
+
process.stderr.write(`Diff written to ${opts.output}
|
|
3848
|
+
`);
|
|
3849
|
+
}
|
|
3850
|
+
if (opts.json) {
|
|
3851
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3852
|
+
`);
|
|
3853
|
+
} else {
|
|
3854
|
+
process.stdout.write(`${formatDiffReport(result)}
|
|
3855
|
+
`);
|
|
3856
|
+
}
|
|
3857
|
+
process.exit(result.hasRegressions ? 1 : 0);
|
|
3858
|
+
} catch (err) {
|
|
3859
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3860
|
+
`);
|
|
3861
|
+
process.exit(2);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3023
3867
|
// src/tree-formatter.ts
|
|
3024
|
-
var
|
|
3025
|
-
var
|
|
3026
|
-
var
|
|
3027
|
-
var
|
|
3868
|
+
var BRANCH2 = "\u251C\u2500\u2500 ";
|
|
3869
|
+
var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
|
|
3870
|
+
var VERTICAL2 = "\u2502 ";
|
|
3871
|
+
var EMPTY2 = " ";
|
|
3028
3872
|
function buildLabel(node, options) {
|
|
3029
3873
|
const parts = [node.name];
|
|
3030
3874
|
if (node.type === "memo") {
|
|
@@ -3072,19 +3916,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
|
|
|
3072
3916
|
}
|
|
3073
3917
|
return;
|
|
3074
3918
|
}
|
|
3075
|
-
const connector = isLast ?
|
|
3919
|
+
const connector = isLast ? LAST_BRANCH2 : BRANCH2;
|
|
3076
3920
|
const label = buildLabel(node, options);
|
|
3077
3921
|
lines.push(`${prefix}${connector}${label}`);
|
|
3078
3922
|
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
3079
3923
|
const childCount = countVisibleDescendants(node, options);
|
|
3080
3924
|
if (childCount > 0) {
|
|
3081
|
-
const nextPrefix2 = prefix + (isLast ?
|
|
3082
|
-
lines.push(`${nextPrefix2}${
|
|
3925
|
+
const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3926
|
+
lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3083
3927
|
}
|
|
3084
3928
|
return;
|
|
3085
3929
|
}
|
|
3086
3930
|
const visibleChildren = getVisibleChildren(node, options);
|
|
3087
|
-
const nextPrefix = prefix + (isLast ?
|
|
3931
|
+
const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
|
|
3088
3932
|
for (let i = 0; i < visibleChildren.length; i++) {
|
|
3089
3933
|
const child = visibleChildren[i];
|
|
3090
3934
|
if (child !== void 0) {
|
|
@@ -3126,7 +3970,7 @@ function formatTree(root, options = {}) {
|
|
|
3126
3970
|
if (options.maxDepth === 0) {
|
|
3127
3971
|
const childCount = countVisibleDescendants(root, options);
|
|
3128
3972
|
if (childCount > 0) {
|
|
3129
|
-
lines.push(`${
|
|
3973
|
+
lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
|
|
3130
3974
|
}
|
|
3131
3975
|
} else {
|
|
3132
3976
|
const visibleChildren = getVisibleChildren(root, options);
|
|
@@ -3300,50 +4144,50 @@ function buildStructuredReport(report) {
|
|
|
3300
4144
|
}
|
|
3301
4145
|
|
|
3302
4146
|
// src/tokens/commands.ts
|
|
3303
|
-
import { existsSync as
|
|
3304
|
-
import { resolve as
|
|
4147
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4148
|
+
import { resolve as resolve11 } from "path";
|
|
3305
4149
|
import {
|
|
3306
4150
|
parseTokenFileSync as parseTokenFileSync2,
|
|
3307
4151
|
TokenParseError,
|
|
3308
|
-
TokenResolver as
|
|
4152
|
+
TokenResolver as TokenResolver4,
|
|
3309
4153
|
TokenValidationError,
|
|
3310
4154
|
validateTokenFile
|
|
3311
4155
|
} from "@agent-scope/tokens";
|
|
3312
|
-
import { Command as
|
|
4156
|
+
import { Command as Command7 } from "commander";
|
|
3313
4157
|
|
|
3314
4158
|
// src/tokens/export.ts
|
|
3315
|
-
import { existsSync as
|
|
3316
|
-
import { resolve as
|
|
4159
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4160
|
+
import { resolve as resolve10 } from "path";
|
|
3317
4161
|
import {
|
|
3318
4162
|
exportTokens,
|
|
3319
4163
|
parseTokenFileSync,
|
|
3320
4164
|
ThemeResolver,
|
|
3321
|
-
TokenResolver as
|
|
4165
|
+
TokenResolver as TokenResolver3
|
|
3322
4166
|
} from "@agent-scope/tokens";
|
|
3323
|
-
import { Command as
|
|
4167
|
+
import { Command as Command6 } from "commander";
|
|
3324
4168
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
3325
4169
|
var CONFIG_FILE = "reactscope.config.json";
|
|
3326
4170
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
3327
4171
|
function resolveTokenFilePath(fileFlag) {
|
|
3328
4172
|
if (fileFlag !== void 0) {
|
|
3329
|
-
return
|
|
4173
|
+
return resolve10(process.cwd(), fileFlag);
|
|
3330
4174
|
}
|
|
3331
|
-
const configPath =
|
|
3332
|
-
if (
|
|
4175
|
+
const configPath = resolve10(process.cwd(), CONFIG_FILE);
|
|
4176
|
+
if (existsSync7(configPath)) {
|
|
3333
4177
|
try {
|
|
3334
|
-
const raw =
|
|
4178
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
3335
4179
|
const config = JSON.parse(raw);
|
|
3336
4180
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3337
4181
|
const file = config.tokens.file;
|
|
3338
|
-
return
|
|
4182
|
+
return resolve10(process.cwd(), file);
|
|
3339
4183
|
}
|
|
3340
4184
|
} catch {
|
|
3341
4185
|
}
|
|
3342
4186
|
}
|
|
3343
|
-
return
|
|
4187
|
+
return resolve10(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
3344
4188
|
}
|
|
3345
4189
|
function createTokensExportCommand() {
|
|
3346
|
-
return new
|
|
4190
|
+
return new Command6("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
3347
4191
|
"--theme <name>",
|
|
3348
4192
|
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
3349
4193
|
).action(
|
|
@@ -3359,13 +4203,13 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
|
3359
4203
|
const format = opts.format;
|
|
3360
4204
|
try {
|
|
3361
4205
|
const filePath = resolveTokenFilePath(opts.file);
|
|
3362
|
-
if (!
|
|
4206
|
+
if (!existsSync7(filePath)) {
|
|
3363
4207
|
throw new Error(
|
|
3364
4208
|
`Token file not found at ${filePath}.
|
|
3365
4209
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3366
4210
|
);
|
|
3367
4211
|
}
|
|
3368
|
-
const raw =
|
|
4212
|
+
const raw = readFileSync6(filePath, "utf-8");
|
|
3369
4213
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
3370
4214
|
let themesMap;
|
|
3371
4215
|
if (opts.theme !== void 0) {
|
|
@@ -3376,7 +4220,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
|
3376
4220
|
Available themes: ${available}`
|
|
3377
4221
|
);
|
|
3378
4222
|
}
|
|
3379
|
-
const baseResolver = new
|
|
4223
|
+
const baseResolver = new TokenResolver3(tokens);
|
|
3380
4224
|
const themeResolver = ThemeResolver.fromTokenFile(
|
|
3381
4225
|
baseResolver,
|
|
3382
4226
|
rawFile
|
|
@@ -3404,8 +4248,8 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
3404
4248
|
themes: themesMap
|
|
3405
4249
|
});
|
|
3406
4250
|
if (opts.out !== void 0) {
|
|
3407
|
-
const outPath =
|
|
3408
|
-
|
|
4251
|
+
const outPath = resolve10(process.cwd(), opts.out);
|
|
4252
|
+
writeFileSync7(outPath, output, "utf-8");
|
|
3409
4253
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
3410
4254
|
`);
|
|
3411
4255
|
} else {
|
|
@@ -3445,30 +4289,30 @@ function buildTable2(headers, rows) {
|
|
|
3445
4289
|
}
|
|
3446
4290
|
function resolveTokenFilePath2(fileFlag) {
|
|
3447
4291
|
if (fileFlag !== void 0) {
|
|
3448
|
-
return
|
|
4292
|
+
return resolve11(process.cwd(), fileFlag);
|
|
3449
4293
|
}
|
|
3450
|
-
const configPath =
|
|
3451
|
-
if (
|
|
4294
|
+
const configPath = resolve11(process.cwd(), CONFIG_FILE2);
|
|
4295
|
+
if (existsSync8(configPath)) {
|
|
3452
4296
|
try {
|
|
3453
|
-
const raw =
|
|
4297
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
3454
4298
|
const config = JSON.parse(raw);
|
|
3455
4299
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
3456
4300
|
const file = config.tokens.file;
|
|
3457
|
-
return
|
|
4301
|
+
return resolve11(process.cwd(), file);
|
|
3458
4302
|
}
|
|
3459
4303
|
} catch {
|
|
3460
4304
|
}
|
|
3461
4305
|
}
|
|
3462
|
-
return
|
|
4306
|
+
return resolve11(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
3463
4307
|
}
|
|
3464
4308
|
function loadTokens(absPath) {
|
|
3465
|
-
if (!
|
|
4309
|
+
if (!existsSync8(absPath)) {
|
|
3466
4310
|
throw new Error(
|
|
3467
4311
|
`Token file not found at ${absPath}.
|
|
3468
4312
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3469
4313
|
);
|
|
3470
4314
|
}
|
|
3471
|
-
const raw =
|
|
4315
|
+
const raw = readFileSync7(absPath, "utf-8");
|
|
3472
4316
|
return parseTokenFileSync2(raw);
|
|
3473
4317
|
}
|
|
3474
4318
|
function getRawValue(node, segments) {
|
|
@@ -3508,7 +4352,7 @@ function registerGet2(tokensCmd) {
|
|
|
3508
4352
|
try {
|
|
3509
4353
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3510
4354
|
const { tokens } = loadTokens(filePath);
|
|
3511
|
-
const resolver = new
|
|
4355
|
+
const resolver = new TokenResolver4(tokens);
|
|
3512
4356
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
3513
4357
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
3514
4358
|
if (useJson) {
|
|
@@ -3534,7 +4378,7 @@ function registerList2(tokensCmd) {
|
|
|
3534
4378
|
try {
|
|
3535
4379
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3536
4380
|
const { tokens } = loadTokens(filePath);
|
|
3537
|
-
const resolver = new
|
|
4381
|
+
const resolver = new TokenResolver4(tokens);
|
|
3538
4382
|
const filtered = resolver.list(opts.type, category);
|
|
3539
4383
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3540
4384
|
if (useJson) {
|
|
@@ -3564,7 +4408,7 @@ function registerSearch(tokensCmd) {
|
|
|
3564
4408
|
try {
|
|
3565
4409
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3566
4410
|
const { tokens } = loadTokens(filePath);
|
|
3567
|
-
const resolver = new
|
|
4411
|
+
const resolver = new TokenResolver4(tokens);
|
|
3568
4412
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
3569
4413
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
3570
4414
|
"color",
|
|
@@ -3647,7 +4491,7 @@ function registerResolve(tokensCmd) {
|
|
|
3647
4491
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3648
4492
|
const absFilePath = filePath;
|
|
3649
4493
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
3650
|
-
const resolver = new
|
|
4494
|
+
const resolver = new TokenResolver4(tokens);
|
|
3651
4495
|
resolver.resolve(tokenPath);
|
|
3652
4496
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
3653
4497
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -3682,13 +4526,13 @@ function registerValidate(tokensCmd) {
|
|
|
3682
4526
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
3683
4527
|
try {
|
|
3684
4528
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
3685
|
-
if (!
|
|
4529
|
+
if (!existsSync8(filePath)) {
|
|
3686
4530
|
throw new Error(
|
|
3687
4531
|
`Token file not found at ${filePath}.
|
|
3688
4532
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
3689
4533
|
);
|
|
3690
4534
|
}
|
|
3691
|
-
const raw =
|
|
4535
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3692
4536
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
3693
4537
|
const errors = [];
|
|
3694
4538
|
let parsed;
|
|
@@ -3756,7 +4600,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
3756
4600
|
}
|
|
3757
4601
|
}
|
|
3758
4602
|
function createTokensCommand() {
|
|
3759
|
-
const tokensCmd = new
|
|
4603
|
+
const tokensCmd = new Command7("tokens").description(
|
|
3760
4604
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
3761
4605
|
);
|
|
3762
4606
|
registerGet2(tokensCmd);
|
|
@@ -3770,7 +4614,7 @@ function createTokensCommand() {
|
|
|
3770
4614
|
|
|
3771
4615
|
// src/program.ts
|
|
3772
4616
|
function createProgram(options = {}) {
|
|
3773
|
-
const program2 = new
|
|
4617
|
+
const program2 = new Command8("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
3774
4618
|
program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
3775
4619
|
async (url, opts) => {
|
|
3776
4620
|
try {
|
|
@@ -3843,7 +4687,7 @@ function createProgram(options = {}) {
|
|
|
3843
4687
|
}
|
|
3844
4688
|
);
|
|
3845
4689
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
3846
|
-
const raw =
|
|
4690
|
+
const raw = readFileSync8(tracePath, "utf-8");
|
|
3847
4691
|
const trace = loadTrace(raw);
|
|
3848
4692
|
const source = generateTest(trace, {
|
|
3849
4693
|
description: opts.description,
|
|
@@ -3860,6 +4704,7 @@ function createProgram(options = {}) {
|
|
|
3860
4704
|
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
3861
4705
|
if (existingReportCmd !== void 0) {
|
|
3862
4706
|
registerBaselineSubCommand(existingReportCmd);
|
|
4707
|
+
registerDiffSubCommand(existingReportCmd);
|
|
3863
4708
|
}
|
|
3864
4709
|
return program2;
|
|
3865
4710
|
}
|