@agent-scope/cli 1.17.3 → 1.18.1
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 +646 -197
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +542 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +540 -97
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
6
|
import { Command as Command10 } from "commander";
|
|
7
7
|
|
|
@@ -61,9 +61,9 @@ import { Command } from "commander";
|
|
|
61
61
|
// src/component-bundler.ts
|
|
62
62
|
import { dirname } from "path";
|
|
63
63
|
import * as esbuild from "esbuild";
|
|
64
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss,
|
|
64
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
|
|
65
65
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
66
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
66
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
|
|
67
67
|
}
|
|
68
68
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
69
69
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -99,7 +99,12 @@ import { createElement } from "react";
|
|
|
99
99
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
// If a scope file wrapper was injected, use it to wrap the component
|
|
103
|
+
var Wrapper = (window).__SCOPE_WRAPPER__;
|
|
104
|
+
var element = Wrapper
|
|
105
|
+
? createElement(Wrapper, null, createElement(Component, props))
|
|
106
|
+
: createElement(Component, props);
|
|
107
|
+
createRoot(rootEl).render(element);
|
|
103
108
|
// Use requestAnimationFrame to let React flush the render
|
|
104
109
|
requestAnimationFrame(function() {
|
|
105
110
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
@@ -136,6 +141,21 @@ import { createElement } from "react";
|
|
|
136
141
|
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
137
142
|
banner: {
|
|
138
143
|
js: "/* @agent-scope/cli component harness */"
|
|
144
|
+
},
|
|
145
|
+
// CSS imports (e.g. `import './styles.css'`) are handled at the page level via
|
|
146
|
+
// globalCSS injection. Tell esbuild to treat CSS files as empty modules so
|
|
147
|
+
// components that import CSS directly (e.g. App.tsx) don't error during bundling.
|
|
148
|
+
loader: {
|
|
149
|
+
".css": "empty",
|
|
150
|
+
".svg": "dataurl",
|
|
151
|
+
".png": "dataurl",
|
|
152
|
+
".jpg": "dataurl",
|
|
153
|
+
".jpeg": "dataurl",
|
|
154
|
+
".gif": "dataurl",
|
|
155
|
+
".webp": "dataurl",
|
|
156
|
+
".ttf": "dataurl",
|
|
157
|
+
".woff": "dataurl",
|
|
158
|
+
".woff2": "dataurl"
|
|
139
159
|
}
|
|
140
160
|
});
|
|
141
161
|
if (result.errors.length > 0) {
|
|
@@ -149,12 +169,11 @@ ${msg}`);
|
|
|
149
169
|
}
|
|
150
170
|
return outputFile.text;
|
|
151
171
|
}
|
|
152
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
172
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
|
|
153
173
|
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
154
174
|
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
155
175
|
</style>` : "";
|
|
156
|
-
const
|
|
157
|
-
` : "";
|
|
176
|
+
const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
|
|
158
177
|
return `<!DOCTYPE html>
|
|
159
178
|
<html lang="en">
|
|
160
179
|
<head>
|
|
@@ -169,7 +188,8 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
|
169
188
|
</head>
|
|
170
189
|
<body>
|
|
171
190
|
<div id="scope-root" data-reactscope-root></div>
|
|
172
|
-
${
|
|
191
|
+
${wrapperScriptBlock}
|
|
192
|
+
<script>${bundledScript}</script>
|
|
173
193
|
</body>
|
|
174
194
|
</html>`;
|
|
175
195
|
}
|
|
@@ -537,16 +557,67 @@ async function getTailwindCompiler(cwd) {
|
|
|
537
557
|
from: entryPath,
|
|
538
558
|
loadStylesheet
|
|
539
559
|
});
|
|
540
|
-
const
|
|
541
|
-
compilerCache = { cwd, build:
|
|
542
|
-
return
|
|
560
|
+
const build3 = result.build.bind(result);
|
|
561
|
+
compilerCache = { cwd, build: build3 };
|
|
562
|
+
return build3;
|
|
543
563
|
}
|
|
544
564
|
async function getCompiledCssForClasses(cwd, classes) {
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
565
|
+
const build3 = await getTailwindCompiler(cwd);
|
|
566
|
+
if (build3 === null) return null;
|
|
547
567
|
const deduped = [...new Set(classes)].filter(Boolean);
|
|
548
568
|
if (deduped.length === 0) return null;
|
|
549
|
-
return
|
|
569
|
+
return build3(deduped);
|
|
570
|
+
}
|
|
571
|
+
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
572
|
+
const { existsSync: existsSync15, readFileSync: readFileSync13 } = await import("fs");
|
|
573
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
574
|
+
if (!existsSync15(cssFilePath)) return null;
|
|
575
|
+
const raw = readFileSync13(cssFilePath, "utf-8");
|
|
576
|
+
const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
|
|
577
|
+
if (!needsCompile) {
|
|
578
|
+
return raw;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const require2 = createRequire3(resolve(cwd, "package.json"));
|
|
582
|
+
let postcss;
|
|
583
|
+
let twPlugin;
|
|
584
|
+
try {
|
|
585
|
+
postcss = require2("postcss");
|
|
586
|
+
twPlugin = require2("tailwindcss");
|
|
587
|
+
} catch {
|
|
588
|
+
return raw;
|
|
589
|
+
}
|
|
590
|
+
let autoprefixerPlugin;
|
|
591
|
+
try {
|
|
592
|
+
autoprefixerPlugin = require2("autoprefixer");
|
|
593
|
+
} catch {
|
|
594
|
+
autoprefixerPlugin = null;
|
|
595
|
+
}
|
|
596
|
+
const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
|
|
597
|
+
const result = await postcss(plugins).process(raw, {
|
|
598
|
+
from: cssFilePath,
|
|
599
|
+
to: cssFilePath
|
|
600
|
+
});
|
|
601
|
+
return result.css;
|
|
602
|
+
} catch (err) {
|
|
603
|
+
process.stderr.write(
|
|
604
|
+
`[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
|
|
605
|
+
`
|
|
606
|
+
);
|
|
607
|
+
return raw;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function loadGlobalCss(globalCssFiles, cwd) {
|
|
611
|
+
if (globalCssFiles.length === 0) return null;
|
|
612
|
+
const parts = [];
|
|
613
|
+
for (const relPath of globalCssFiles) {
|
|
614
|
+
const absPath = resolve(cwd, relPath);
|
|
615
|
+
const css = await compileGlobalCssFile(absPath, cwd);
|
|
616
|
+
if (css !== null && css.trim().length > 0) {
|
|
617
|
+
parts.push(css);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
550
621
|
}
|
|
551
622
|
|
|
552
623
|
// src/ci/commands.ts
|
|
@@ -1060,6 +1131,20 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1060
1131
|
}
|
|
1061
1132
|
return unique;
|
|
1062
1133
|
}
|
|
1134
|
+
var GLOBAL_CSS_CANDIDATES = [
|
|
1135
|
+
"src/styles.css",
|
|
1136
|
+
"src/index.css",
|
|
1137
|
+
"src/global.css",
|
|
1138
|
+
"src/globals.css",
|
|
1139
|
+
"src/app.css",
|
|
1140
|
+
"src/main.css",
|
|
1141
|
+
"styles/globals.css",
|
|
1142
|
+
"styles/global.css",
|
|
1143
|
+
"styles/index.css"
|
|
1144
|
+
];
|
|
1145
|
+
function detectGlobalCSSFiles(rootDir) {
|
|
1146
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync3(join(rootDir, rel)));
|
|
1147
|
+
}
|
|
1063
1148
|
var TAILWIND_STEMS = ["tailwind.config"];
|
|
1064
1149
|
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
1065
1150
|
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
@@ -1127,13 +1212,15 @@ function detectProject(rootDir) {
|
|
|
1127
1212
|
const packageManager = detectPackageManager(rootDir);
|
|
1128
1213
|
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
1129
1214
|
const tokenSources = detectTokenSources(rootDir);
|
|
1215
|
+
const globalCSSFiles = detectGlobalCSSFiles(rootDir);
|
|
1130
1216
|
return {
|
|
1131
1217
|
framework,
|
|
1132
1218
|
typescript,
|
|
1133
1219
|
tsconfigPath,
|
|
1134
1220
|
componentPatterns,
|
|
1135
1221
|
tokenSources,
|
|
1136
|
-
packageManager
|
|
1222
|
+
packageManager,
|
|
1223
|
+
globalCSSFiles
|
|
1137
1224
|
};
|
|
1138
1225
|
}
|
|
1139
1226
|
|
|
@@ -1145,7 +1232,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
|
1145
1232
|
components: {
|
|
1146
1233
|
include,
|
|
1147
1234
|
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
1148
|
-
wrappers: { providers: [], globalCSS: [] }
|
|
1235
|
+
wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
|
|
1149
1236
|
},
|
|
1150
1237
|
render: {
|
|
1151
1238
|
viewport: { default: { width: 1280, height: 800 } },
|
|
@@ -1176,9 +1263,9 @@ function createRL() {
|
|
|
1176
1263
|
});
|
|
1177
1264
|
}
|
|
1178
1265
|
async function ask(rl, question) {
|
|
1179
|
-
return new Promise((
|
|
1266
|
+
return new Promise((resolve18) => {
|
|
1180
1267
|
rl.question(question, (answer) => {
|
|
1181
|
-
|
|
1268
|
+
resolve18(answer.trim());
|
|
1182
1269
|
});
|
|
1183
1270
|
});
|
|
1184
1271
|
}
|
|
@@ -1203,18 +1290,118 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1203
1290
|
`);
|
|
1204
1291
|
}
|
|
1205
1292
|
}
|
|
1293
|
+
function extractTailwindTokens(tokenSources) {
|
|
1294
|
+
const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
|
|
1295
|
+
if (!tailwindSource) return null;
|
|
1296
|
+
try {
|
|
1297
|
+
let parseBlock2 = function(block) {
|
|
1298
|
+
const result = {};
|
|
1299
|
+
const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
|
|
1300
|
+
for (const m of block.matchAll(lineRe)) {
|
|
1301
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
1302
|
+
result[m[1]] = m[2];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return result;
|
|
1306
|
+
};
|
|
1307
|
+
var parseBlock = parseBlock2;
|
|
1308
|
+
const raw = readFileSync4(tailwindSource.path, "utf-8");
|
|
1309
|
+
const tokens = {};
|
|
1310
|
+
const colorsKeyIdx = raw.indexOf("colors:");
|
|
1311
|
+
if (colorsKeyIdx !== -1) {
|
|
1312
|
+
const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
|
|
1313
|
+
if (colorsBraceStart !== -1) {
|
|
1314
|
+
let colorDepth = 0;
|
|
1315
|
+
let colorsBraceEnd = -1;
|
|
1316
|
+
for (let ci = colorsBraceStart; ci < raw.length; ci++) {
|
|
1317
|
+
if (raw[ci] === "{") colorDepth++;
|
|
1318
|
+
else if (raw[ci] === "}") {
|
|
1319
|
+
colorDepth--;
|
|
1320
|
+
if (colorDepth === 0) {
|
|
1321
|
+
colorsBraceEnd = ci;
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (colorsBraceEnd > colorsBraceStart) {
|
|
1327
|
+
const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
|
|
1328
|
+
const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1329
|
+
const colorTokens = {};
|
|
1330
|
+
for (const sm of colorSection.matchAll(scaleRe)) {
|
|
1331
|
+
if (sm[1] === void 0 || sm[2] === void 0) continue;
|
|
1332
|
+
const scaleName = sm[1];
|
|
1333
|
+
const scaleValues = parseBlock2(sm[2]);
|
|
1334
|
+
if (Object.keys(scaleValues).length > 0) {
|
|
1335
|
+
const scaleTokens = {};
|
|
1336
|
+
for (const [step, hex] of Object.entries(scaleValues)) {
|
|
1337
|
+
scaleTokens[step] = { value: hex, type: "color" };
|
|
1338
|
+
}
|
|
1339
|
+
colorTokens[scaleName] = scaleTokens;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (Object.keys(colorTokens).length > 0) {
|
|
1343
|
+
tokens["color"] = colorTokens;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1349
|
+
if (spacingMatch?.[1] !== void 0) {
|
|
1350
|
+
const spacingValues = parseBlock2(spacingMatch[1]);
|
|
1351
|
+
if (Object.keys(spacingValues).length > 0) {
|
|
1352
|
+
const spacingTokens = {};
|
|
1353
|
+
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1354
|
+
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1355
|
+
}
|
|
1356
|
+
tokens["spacing"] = spacingTokens;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1360
|
+
if (fontFamilyMatch?.[1] !== void 0) {
|
|
1361
|
+
const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
|
|
1362
|
+
const fontTokens = {};
|
|
1363
|
+
for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
|
|
1364
|
+
if (fm[1] !== void 0 && fm[2] !== void 0) {
|
|
1365
|
+
fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (Object.keys(fontTokens).length > 0) {
|
|
1369
|
+
tokens["font"] = fontTokens;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1373
|
+
if (borderRadiusMatch?.[1] !== void 0) {
|
|
1374
|
+
const radiusValues = parseBlock2(borderRadiusMatch[1]);
|
|
1375
|
+
if (Object.keys(radiusValues).length > 0) {
|
|
1376
|
+
const radiusTokens = {};
|
|
1377
|
+
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1378
|
+
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1379
|
+
}
|
|
1380
|
+
tokens["radius"] = radiusTokens;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
1384
|
+
} catch {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1206
1388
|
function scaffoldConfig(rootDir, config) {
|
|
1207
1389
|
const path = join2(rootDir, "reactscope.config.json");
|
|
1208
1390
|
writeFileSync3(path, `${JSON.stringify(config, null, 2)}
|
|
1209
1391
|
`);
|
|
1210
1392
|
return path;
|
|
1211
1393
|
}
|
|
1212
|
-
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
1394
|
+
function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
|
|
1213
1395
|
const path = join2(rootDir, tokenFile);
|
|
1214
1396
|
if (!existsSync4(path)) {
|
|
1215
1397
|
const stub = {
|
|
1216
1398
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
1217
|
-
|
|
1399
|
+
version: "1.0.0",
|
|
1400
|
+
meta: {
|
|
1401
|
+
name: "Design Tokens",
|
|
1402
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1403
|
+
},
|
|
1404
|
+
tokens: extractedTokens ?? {}
|
|
1218
1405
|
};
|
|
1219
1406
|
writeFileSync3(path, `${JSON.stringify(stub, null, 2)}
|
|
1220
1407
|
`);
|
|
@@ -1292,7 +1479,13 @@ async function runInit(options) {
|
|
|
1292
1479
|
}
|
|
1293
1480
|
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1294
1481
|
created.push(cfgPath);
|
|
1295
|
-
const
|
|
1482
|
+
const extractedTokens = extractTailwindTokens(detected.tokenSources);
|
|
1483
|
+
if (extractedTokens !== null) {
|
|
1484
|
+
const tokenGroupCount = Object.keys(extractedTokens).length;
|
|
1485
|
+
process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
|
|
1486
|
+
`);
|
|
1487
|
+
}
|
|
1488
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
|
|
1296
1489
|
created.push(tokPath);
|
|
1297
1490
|
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1298
1491
|
created.push(outDirPath);
|
|
@@ -1404,7 +1597,10 @@ Available: ${available}${hint}`
|
|
|
1404
1597
|
});
|
|
1405
1598
|
}
|
|
1406
1599
|
function registerQuery(manifestCmd) {
|
|
1407
|
-
manifestCmd.command("query").description("Query components by attributes").option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option(
|
|
1600
|
+
manifestCmd.command("query").description("Query components by attributes").option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option(
|
|
1601
|
+
"--has-prop <spec>",
|
|
1602
|
+
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1603
|
+
).option("--composed-by <name>", "Find components that compose the named component").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
|
|
1408
1604
|
(opts) => {
|
|
1409
1605
|
try {
|
|
1410
1606
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1415,9 +1611,11 @@ function registerQuery(manifestCmd) {
|
|
|
1415
1611
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
1416
1612
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
1417
1613
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1614
|
+
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1615
|
+
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1418
1616
|
if (queryParts.length === 0) {
|
|
1419
1617
|
process.stderr.write(
|
|
1420
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects,
|
|
1618
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
|
|
1421
1619
|
);
|
|
1422
1620
|
process.exit(1);
|
|
1423
1621
|
}
|
|
@@ -1444,6 +1642,27 @@ function registerQuery(manifestCmd) {
|
|
|
1444
1642
|
if (opts.hasFetch) {
|
|
1445
1643
|
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1446
1644
|
}
|
|
1645
|
+
if (opts.hasProp !== void 0) {
|
|
1646
|
+
const spec = opts.hasProp;
|
|
1647
|
+
const colonIdx = spec.indexOf(":");
|
|
1648
|
+
const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
|
|
1649
|
+
const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
|
|
1650
|
+
entries = entries.filter(([, d]) => {
|
|
1651
|
+
const props = d.props;
|
|
1652
|
+
if (!props || !(propName in props)) return false;
|
|
1653
|
+
if (propType !== void 0) {
|
|
1654
|
+
return props[propName]?.type === propType;
|
|
1655
|
+
}
|
|
1656
|
+
return true;
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
if (opts.composedBy !== void 0) {
|
|
1660
|
+
const targetName = opts.composedBy;
|
|
1661
|
+
entries = entries.filter(([, d]) => {
|
|
1662
|
+
const composedBy = d.composedBy;
|
|
1663
|
+
return composedBy !== void 0 && composedBy.includes(targetName);
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1447
1666
|
const rows = entries.map(([name, d]) => ({
|
|
1448
1667
|
name,
|
|
1449
1668
|
file: d.filePath,
|
|
@@ -2996,8 +3215,8 @@ function createInstrumentCommand() {
|
|
|
2996
3215
|
}
|
|
2997
3216
|
|
|
2998
3217
|
// src/render-commands.ts
|
|
2999
|
-
import { mkdirSync as
|
|
3000
|
-
import { resolve as
|
|
3218
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
3219
|
+
import { resolve as resolve9 } from "path";
|
|
3001
3220
|
import {
|
|
3002
3221
|
ALL_CONTEXT_IDS,
|
|
3003
3222
|
ALL_STRESS_IDS,
|
|
@@ -3009,6 +3228,140 @@ import {
|
|
|
3009
3228
|
stressAxis
|
|
3010
3229
|
} from "@agent-scope/render";
|
|
3011
3230
|
import { Command as Command6 } from "commander";
|
|
3231
|
+
|
|
3232
|
+
// src/scope-file.ts
|
|
3233
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync } from "fs";
|
|
3234
|
+
import { createRequire as createRequire2 } from "module";
|
|
3235
|
+
import { tmpdir } from "os";
|
|
3236
|
+
import { dirname as dirname2, join as join3, resolve as resolve8 } from "path";
|
|
3237
|
+
import * as esbuild2 from "esbuild";
|
|
3238
|
+
var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
|
|
3239
|
+
function findScopeFile(componentFilePath) {
|
|
3240
|
+
const dir = dirname2(componentFilePath);
|
|
3241
|
+
const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
3242
|
+
const baseName = stem.slice(dir.length + 1);
|
|
3243
|
+
for (const ext of SCOPE_EXTENSIONS) {
|
|
3244
|
+
const candidate = join3(dir, `${baseName}${ext}`);
|
|
3245
|
+
if (existsSync6(candidate)) return candidate;
|
|
3246
|
+
}
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
async function loadScopeFile(scopeFilePath) {
|
|
3250
|
+
const tmpDir = join3(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3251
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
3252
|
+
const outFile = join3(tmpDir, "scope-file.cjs");
|
|
3253
|
+
try {
|
|
3254
|
+
const result = await esbuild2.build({
|
|
3255
|
+
entryPoints: [scopeFilePath],
|
|
3256
|
+
bundle: true,
|
|
3257
|
+
format: "cjs",
|
|
3258
|
+
platform: "node",
|
|
3259
|
+
target: "node18",
|
|
3260
|
+
outfile: outFile,
|
|
3261
|
+
write: true,
|
|
3262
|
+
jsx: "automatic",
|
|
3263
|
+
jsxImportSource: "react",
|
|
3264
|
+
// Externalize React — we don't need to execute JSX, just extract plain data
|
|
3265
|
+
external: ["react", "react-dom", "react/jsx-runtime"],
|
|
3266
|
+
define: {
|
|
3267
|
+
"process.env.NODE_ENV": '"development"'
|
|
3268
|
+
},
|
|
3269
|
+
logLevel: "silent"
|
|
3270
|
+
});
|
|
3271
|
+
if (result.errors.length > 0) {
|
|
3272
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3273
|
+
throw new Error(`Failed to bundle scope file ${scopeFilePath}:
|
|
3274
|
+
${msg}`);
|
|
3275
|
+
}
|
|
3276
|
+
const req = createRequire2(import.meta.url);
|
|
3277
|
+
delete req.cache[resolve8(outFile)];
|
|
3278
|
+
const mod = req(outFile);
|
|
3279
|
+
const scenarios = extractScenarios(mod, scopeFilePath);
|
|
3280
|
+
const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
|
|
3281
|
+
return { filePath: scopeFilePath, scenarios, hasWrapper };
|
|
3282
|
+
} finally {
|
|
3283
|
+
try {
|
|
3284
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
3285
|
+
} catch {
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
async function loadScopeFileForComponent(componentFilePath) {
|
|
3290
|
+
const scopeFilePath = findScopeFile(componentFilePath);
|
|
3291
|
+
if (scopeFilePath === null) return null;
|
|
3292
|
+
return loadScopeFile(scopeFilePath);
|
|
3293
|
+
}
|
|
3294
|
+
function extractScenarios(mod, filePath) {
|
|
3295
|
+
const raw = mod.scenarios ?? mod.default?.scenarios;
|
|
3296
|
+
if (raw === void 0) return {};
|
|
3297
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3298
|
+
console.warn(`[scope] ${filePath}: "scenarios" export is not a plain object \u2014 ignoring.`);
|
|
3299
|
+
return {};
|
|
3300
|
+
}
|
|
3301
|
+
const result = {};
|
|
3302
|
+
for (const [name, props] of Object.entries(raw)) {
|
|
3303
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) {
|
|
3304
|
+
console.warn(`[scope] ${filePath}: scenario "${name}" is not a plain object \u2014 skipping.`);
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
result[name] = props;
|
|
3308
|
+
}
|
|
3309
|
+
return result;
|
|
3310
|
+
}
|
|
3311
|
+
async function buildWrapperScript(scopeFilePath) {
|
|
3312
|
+
const wrapperEntry = (
|
|
3313
|
+
/* ts */
|
|
3314
|
+
`
|
|
3315
|
+
import * as __scopeMod from ${JSON.stringify(scopeFilePath)};
|
|
3316
|
+
// Expose the wrapper on window so the harness can access it
|
|
3317
|
+
var wrapper =
|
|
3318
|
+
__scopeMod.wrapper ??
|
|
3319
|
+
(__scopeMod.default && __scopeMod.default.wrapper) ??
|
|
3320
|
+
null;
|
|
3321
|
+
window.__SCOPE_WRAPPER__ = wrapper;
|
|
3322
|
+
`
|
|
3323
|
+
);
|
|
3324
|
+
const result = await esbuild2.build({
|
|
3325
|
+
stdin: {
|
|
3326
|
+
contents: wrapperEntry,
|
|
3327
|
+
resolveDir: dirname2(scopeFilePath),
|
|
3328
|
+
loader: "tsx",
|
|
3329
|
+
sourcefile: "__scope_wrapper_entry__.tsx"
|
|
3330
|
+
},
|
|
3331
|
+
bundle: true,
|
|
3332
|
+
format: "iife",
|
|
3333
|
+
platform: "browser",
|
|
3334
|
+
target: "es2020",
|
|
3335
|
+
write: false,
|
|
3336
|
+
jsx: "automatic",
|
|
3337
|
+
jsxImportSource: "react",
|
|
3338
|
+
external: [],
|
|
3339
|
+
define: {
|
|
3340
|
+
"process.env.NODE_ENV": '"development"',
|
|
3341
|
+
global: "globalThis"
|
|
3342
|
+
},
|
|
3343
|
+
logLevel: "silent"
|
|
3344
|
+
});
|
|
3345
|
+
if (result.errors.length > 0) {
|
|
3346
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3347
|
+
throw new Error(`Failed to build wrapper script from ${scopeFilePath}:
|
|
3348
|
+
${msg}`);
|
|
3349
|
+
}
|
|
3350
|
+
return result.outputFiles?.[0]?.text ?? "";
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// src/render-commands.ts
|
|
3354
|
+
function loadGlobalCssFilesFromConfig(cwd) {
|
|
3355
|
+
const configPath = resolve9(cwd, "reactscope.config.json");
|
|
3356
|
+
if (!existsSync7(configPath)) return [];
|
|
3357
|
+
try {
|
|
3358
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
3359
|
+
const cfg = JSON.parse(raw);
|
|
3360
|
+
return cfg.components?.wrappers?.globalCSS ?? [];
|
|
3361
|
+
} catch {
|
|
3362
|
+
return [];
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3012
3365
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
3013
3366
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
3014
3367
|
var _pool3 = null;
|
|
@@ -3029,7 +3382,7 @@ async function shutdownPool3() {
|
|
|
3029
3382
|
_pool3 = null;
|
|
3030
3383
|
}
|
|
3031
3384
|
}
|
|
3032
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
3385
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
|
|
3033
3386
|
const satori = new SatoriRenderer({
|
|
3034
3387
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
3035
3388
|
});
|
|
@@ -3038,11 +3391,14 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3038
3391
|
async renderCell(props, _complexityClass) {
|
|
3039
3392
|
const startMs = performance.now();
|
|
3040
3393
|
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
3394
|
+
const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
|
|
3041
3395
|
const htmlHarness = await buildComponentHarness(
|
|
3042
3396
|
filePath,
|
|
3043
3397
|
componentName,
|
|
3044
3398
|
props,
|
|
3045
|
-
viewportWidth
|
|
3399
|
+
viewportWidth,
|
|
3400
|
+
projectCss ?? void 0,
|
|
3401
|
+
wrapperScript
|
|
3046
3402
|
);
|
|
3047
3403
|
const slot = await pool.acquire();
|
|
3048
3404
|
const { page } = slot;
|
|
@@ -3071,9 +3427,9 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3071
3427
|
});
|
|
3072
3428
|
return [...set];
|
|
3073
3429
|
});
|
|
3074
|
-
const
|
|
3075
|
-
if (
|
|
3076
|
-
await page.addStyleTag({ content:
|
|
3430
|
+
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3431
|
+
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3432
|
+
await page.addStyleTag({ content: projectCss2 });
|
|
3077
3433
|
}
|
|
3078
3434
|
const renderTimeMs = performance.now() - startMs;
|
|
3079
3435
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
@@ -3133,8 +3489,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3133
3489
|
}
|
|
3134
3490
|
};
|
|
3135
3491
|
}
|
|
3492
|
+
function buildScenarioMap(opts, scopeData) {
|
|
3493
|
+
if (opts.scenario !== void 0) {
|
|
3494
|
+
if (scopeData === null) {
|
|
3495
|
+
throw new Error(`--scenario "${opts.scenario}" requires a .scope file next to the component`);
|
|
3496
|
+
}
|
|
3497
|
+
const props = scopeData.scenarios[opts.scenario];
|
|
3498
|
+
if (props === void 0) {
|
|
3499
|
+
const available = Object.keys(scopeData.scenarios).join(", ") || "(none)";
|
|
3500
|
+
throw new Error(
|
|
3501
|
+
`Scenario "${opts.scenario}" not found in scope file.
|
|
3502
|
+
Available: ${available}`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
return { [opts.scenario]: props };
|
|
3506
|
+
}
|
|
3507
|
+
if (opts.props !== void 0) {
|
|
3508
|
+
let parsed;
|
|
3509
|
+
try {
|
|
3510
|
+
parsed = JSON.parse(opts.props);
|
|
3511
|
+
} catch {
|
|
3512
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3513
|
+
}
|
|
3514
|
+
return { __default__: parsed };
|
|
3515
|
+
}
|
|
3516
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length > 0) {
|
|
3517
|
+
return scopeData.scenarios;
|
|
3518
|
+
}
|
|
3519
|
+
return { __default__: {} };
|
|
3520
|
+
}
|
|
3136
3521
|
function registerRenderSingle(renderCmd) {
|
|
3137
|
-
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(
|
|
3522
|
+
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("--scenario <name>", "Run a named scenario from the component's .scope file").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(
|
|
3138
3523
|
async (componentName, opts) => {
|
|
3139
3524
|
try {
|
|
3140
3525
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3154,72 +3539,96 @@ Available: ${available}`
|
|
|
3154
3539
|
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3155
3540
|
}
|
|
3156
3541
|
}
|
|
3542
|
+
if (descriptor.props !== void 0) {
|
|
3543
|
+
const propDefs = descriptor.props;
|
|
3544
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
3545
|
+
if (propName in props) continue;
|
|
3546
|
+
if (!propDef.required && propDef.default !== void 0) continue;
|
|
3547
|
+
if (propDef.type === "node" || propDef.type === "string") {
|
|
3548
|
+
props[propName] = propName === "children" ? componentName : propName;
|
|
3549
|
+
} else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
|
|
3550
|
+
props[propName] = propDef.values[0];
|
|
3551
|
+
} else if (propDef.type === "boolean") {
|
|
3552
|
+
props[propName] = false;
|
|
3553
|
+
} else if (propDef.type === "number") {
|
|
3554
|
+
props[propName] = 0;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3157
3558
|
const { width, height } = parseViewport(opts.viewport);
|
|
3158
3559
|
const rootDir = process.cwd();
|
|
3159
|
-
const filePath =
|
|
3160
|
-
const
|
|
3560
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3561
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3562
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3563
|
+
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3564
|
+
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3565
|
+
const renderer = buildRenderer(
|
|
3566
|
+
filePath,
|
|
3567
|
+
componentName,
|
|
3568
|
+
width,
|
|
3569
|
+
height,
|
|
3570
|
+
globalCssFiles,
|
|
3571
|
+
rootDir,
|
|
3572
|
+
wrapperScript
|
|
3573
|
+
);
|
|
3161
3574
|
process.stderr.write(
|
|
3162
3575
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3163
3576
|
`
|
|
3164
3577
|
);
|
|
3165
|
-
const
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3578
|
+
const fmt2 = resolveSingleFormat(opts.format);
|
|
3579
|
+
let anyFailed = false;
|
|
3580
|
+
for (const [scenarioName, props2] of Object.entries(scenarios)) {
|
|
3581
|
+
const isNamed = scenarioName !== "__default__";
|
|
3582
|
+
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3583
|
+
const outcome = await safeRender2(
|
|
3584
|
+
() => renderer.renderCell(props2, descriptor.complexityClass),
|
|
3585
|
+
{
|
|
3586
|
+
props: props2,
|
|
3587
|
+
sourceLocation: {
|
|
3588
|
+
file: descriptor.filePath,
|
|
3589
|
+
line: descriptor.loc.start,
|
|
3590
|
+
column: 0
|
|
3591
|
+
}
|
|
3173
3592
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
if (outcome.crashed) {
|
|
3178
|
-
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
3593
|
+
);
|
|
3594
|
+
if (outcome.crashed) {
|
|
3595
|
+
process.stderr.write(`\u2717 ${label} render failed: ${outcome.error.message}
|
|
3179
3596
|
`);
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3597
|
+
const hintList = outcome.error.heuristicFlags.join(", ");
|
|
3598
|
+
if (hintList.length > 0) {
|
|
3599
|
+
process.stderr.write(` Hints: ${hintList}
|
|
3183
3600
|
`);
|
|
3601
|
+
}
|
|
3602
|
+
anyFailed = true;
|
|
3603
|
+
continue;
|
|
3184
3604
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3605
|
+
const result = outcome.result;
|
|
3606
|
+
const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
|
|
3607
|
+
if (opts.output !== void 0 && !isNamed) {
|
|
3608
|
+
const outPath = resolve9(process.cwd(), opts.output);
|
|
3609
|
+
writeFileSync5(outPath, result.screenshot);
|
|
3610
|
+
process.stdout.write(
|
|
3611
|
+
`\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3193
3612
|
`
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
if (fmt2 === "json") {
|
|
3199
|
-
const json = formatRenderJson(componentName, props, result);
|
|
3200
|
-
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3613
|
+
);
|
|
3614
|
+
} else if (fmt2 === "json") {
|
|
3615
|
+
const json = formatRenderJson(label, props2, result);
|
|
3616
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3201
3617
|
`);
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
`
|
|
3211
|
-
);
|
|
3212
|
-
} else {
|
|
3213
|
-
const dir = resolve8(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3214
|
-
mkdirSync3(dir, { recursive: true });
|
|
3215
|
-
const outPath = resolve8(dir, `${componentName}.png`);
|
|
3216
|
-
writeFileSync5(outPath, result.screenshot);
|
|
3217
|
-
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
3218
|
-
process.stdout.write(
|
|
3219
|
-
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3618
|
+
} else {
|
|
3619
|
+
const dir = resolve9(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3620
|
+
mkdirSync4(dir, { recursive: true });
|
|
3621
|
+
const outPath = resolve9(dir, outFileName);
|
|
3622
|
+
writeFileSync5(outPath, result.screenshot);
|
|
3623
|
+
const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
|
|
3624
|
+
process.stdout.write(
|
|
3625
|
+
`\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3220
3626
|
`
|
|
3221
|
-
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3222
3629
|
}
|
|
3630
|
+
await shutdownPool3();
|
|
3631
|
+
if (anyFailed) process.exit(1);
|
|
3223
3632
|
} catch (err) {
|
|
3224
3633
|
await shutdownPool3();
|
|
3225
3634
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3230,7 +3639,10 @@ Available: ${available}`
|
|
|
3230
3639
|
);
|
|
3231
3640
|
}
|
|
3232
3641
|
function registerRenderMatrix(renderCmd) {
|
|
3233
|
-
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3642
|
+
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3643
|
+
"--axes <spec>",
|
|
3644
|
+
`Axis definitions: key:v1,v2 space-separated OR JSON object e.g. 'variant:primary,ghost size:sm,lg' or '{"variant":["primary","ghost"],"size":["sm","lg"]}'`
|
|
3645
|
+
).option(
|
|
3234
3646
|
"--contexts <ids>",
|
|
3235
3647
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
3236
3648
|
).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(
|
|
@@ -3248,22 +3660,48 @@ Available: ${available}`
|
|
|
3248
3660
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
3249
3661
|
const { width, height } = { width: 375, height: 812 };
|
|
3250
3662
|
const rootDir = process.cwd();
|
|
3251
|
-
const filePath =
|
|
3252
|
-
const
|
|
3663
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3664
|
+
const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3665
|
+
const renderer = buildRenderer(
|
|
3666
|
+
filePath,
|
|
3667
|
+
componentName,
|
|
3668
|
+
width,
|
|
3669
|
+
height,
|
|
3670
|
+
matrixCssFiles,
|
|
3671
|
+
rootDir
|
|
3672
|
+
);
|
|
3253
3673
|
const axes = [];
|
|
3254
3674
|
if (opts.axes !== void 0) {
|
|
3255
|
-
const
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3675
|
+
const axesRaw = opts.axes.trim();
|
|
3676
|
+
if (axesRaw.startsWith("{")) {
|
|
3677
|
+
let parsed;
|
|
3678
|
+
try {
|
|
3679
|
+
parsed = JSON.parse(axesRaw);
|
|
3680
|
+
} catch {
|
|
3681
|
+
throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
|
|
3260
3682
|
}
|
|
3261
|
-
const name
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3683
|
+
for (const [name, vals] of Object.entries(parsed)) {
|
|
3684
|
+
if (!Array.isArray(vals)) {
|
|
3685
|
+
throw new Error(`Axis "${name}" must be an array of values in JSON format`);
|
|
3686
|
+
}
|
|
3687
|
+
axes.push({ name, values: vals.map(String) });
|
|
3688
|
+
}
|
|
3689
|
+
} else {
|
|
3690
|
+
const axisSpecs = axesRaw.split(/\s+/);
|
|
3691
|
+
for (const spec of axisSpecs) {
|
|
3692
|
+
const colonIdx = spec.indexOf(":");
|
|
3693
|
+
if (colonIdx < 0) {
|
|
3694
|
+
throw new Error(
|
|
3695
|
+
`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
const name = spec.slice(0, colonIdx);
|
|
3699
|
+
const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
|
|
3700
|
+
if (name.length === 0 || values.length === 0) {
|
|
3701
|
+
throw new Error(`Invalid axis spec "${spec}"`);
|
|
3702
|
+
}
|
|
3703
|
+
axes.push({ name, values });
|
|
3265
3704
|
}
|
|
3266
|
-
axes.push({ name, values });
|
|
3267
3705
|
}
|
|
3268
3706
|
}
|
|
3269
3707
|
if (opts.contexts !== void 0) {
|
|
@@ -3315,7 +3753,7 @@ Available: ${available}`
|
|
|
3315
3753
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3316
3754
|
const gen = new SpriteSheetGenerator2();
|
|
3317
3755
|
const sheet = await gen.generate(result);
|
|
3318
|
-
const spritePath =
|
|
3756
|
+
const spritePath = resolve9(process.cwd(), opts.sprite);
|
|
3319
3757
|
writeFileSync5(spritePath, sheet.png);
|
|
3320
3758
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
3321
3759
|
`);
|
|
@@ -3325,9 +3763,9 @@ Available: ${available}`
|
|
|
3325
3763
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3326
3764
|
const gen = new SpriteSheetGenerator2();
|
|
3327
3765
|
const sheet = await gen.generate(result);
|
|
3328
|
-
const dir =
|
|
3329
|
-
|
|
3330
|
-
const outPath =
|
|
3766
|
+
const dir = resolve9(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3767
|
+
mkdirSync4(dir, { recursive: true });
|
|
3768
|
+
const outPath = resolve9(dir, `${componentName}-matrix.png`);
|
|
3331
3769
|
writeFileSync5(outPath, sheet.png);
|
|
3332
3770
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
3333
3771
|
process.stdout.write(
|
|
@@ -3371,8 +3809,8 @@ function registerRenderAll(renderCmd) {
|
|
|
3371
3809
|
return;
|
|
3372
3810
|
}
|
|
3373
3811
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
3374
|
-
const outputDir =
|
|
3375
|
-
|
|
3812
|
+
const outputDir = resolve9(process.cwd(), opts.outputDir);
|
|
3813
|
+
mkdirSync4(outputDir, { recursive: true });
|
|
3376
3814
|
const rootDir = process.cwd();
|
|
3377
3815
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
3378
3816
|
`);
|
|
@@ -3381,8 +3819,9 @@ function registerRenderAll(renderCmd) {
|
|
|
3381
3819
|
const renderOne = async (name) => {
|
|
3382
3820
|
const descriptor = manifest.components[name];
|
|
3383
3821
|
if (descriptor === void 0) return;
|
|
3384
|
-
const filePath =
|
|
3385
|
-
const
|
|
3822
|
+
const filePath = resolve9(rootDir, descriptor.filePath);
|
|
3823
|
+
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
3824
|
+
const renderer = buildRenderer(filePath, name, 375, 812, allCssFiles, process.cwd());
|
|
3386
3825
|
const outcome = await safeRender2(
|
|
3387
3826
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
3388
3827
|
{
|
|
@@ -3404,7 +3843,7 @@ function registerRenderAll(renderCmd) {
|
|
|
3404
3843
|
success: false,
|
|
3405
3844
|
errorMessage: outcome.error.message
|
|
3406
3845
|
});
|
|
3407
|
-
const errPath =
|
|
3846
|
+
const errPath = resolve9(outputDir, `${name}.error.json`);
|
|
3408
3847
|
writeFileSync5(
|
|
3409
3848
|
errPath,
|
|
3410
3849
|
JSON.stringify(
|
|
@@ -3422,9 +3861,9 @@ function registerRenderAll(renderCmd) {
|
|
|
3422
3861
|
}
|
|
3423
3862
|
const result = outcome.result;
|
|
3424
3863
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
3425
|
-
const pngPath =
|
|
3864
|
+
const pngPath = resolve9(outputDir, `${name}.png`);
|
|
3426
3865
|
writeFileSync5(pngPath, result.screenshot);
|
|
3427
|
-
const jsonPath =
|
|
3866
|
+
const jsonPath = resolve9(outputDir, `${name}.json`);
|
|
3428
3867
|
writeFileSync5(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
3429
3868
|
if (isTTY()) {
|
|
3430
3869
|
process.stdout.write(
|
|
@@ -3497,8 +3936,8 @@ function createRenderCommand() {
|
|
|
3497
3936
|
}
|
|
3498
3937
|
|
|
3499
3938
|
// src/report/baseline.ts
|
|
3500
|
-
import { existsSync as
|
|
3501
|
-
import { resolve as
|
|
3939
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
3940
|
+
import { resolve as resolve10 } from "path";
|
|
3502
3941
|
import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
|
|
3503
3942
|
import { BrowserPool as BrowserPool4, safeRender as safeRender3 } from "@agent-scope/render";
|
|
3504
3943
|
import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
|
|
@@ -3647,20 +4086,20 @@ async function runBaseline(options = {}) {
|
|
|
3647
4086
|
} = options;
|
|
3648
4087
|
const startTime = performance.now();
|
|
3649
4088
|
const rootDir = process.cwd();
|
|
3650
|
-
const baselineDir =
|
|
3651
|
-
const rendersDir =
|
|
3652
|
-
if (
|
|
3653
|
-
|
|
4089
|
+
const baselineDir = resolve10(rootDir, outputDir);
|
|
4090
|
+
const rendersDir = resolve10(baselineDir, "renders");
|
|
4091
|
+
if (existsSync8(baselineDir)) {
|
|
4092
|
+
rmSync2(baselineDir, { recursive: true, force: true });
|
|
3654
4093
|
}
|
|
3655
|
-
|
|
4094
|
+
mkdirSync5(rendersDir, { recursive: true });
|
|
3656
4095
|
let manifest;
|
|
3657
4096
|
if (manifestPath !== void 0) {
|
|
3658
|
-
const { readFileSync:
|
|
3659
|
-
const absPath =
|
|
3660
|
-
if (!
|
|
4097
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
4098
|
+
const absPath = resolve10(rootDir, manifestPath);
|
|
4099
|
+
if (!existsSync8(absPath)) {
|
|
3661
4100
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3662
4101
|
}
|
|
3663
|
-
manifest = JSON.parse(
|
|
4102
|
+
manifest = JSON.parse(readFileSync13(absPath, "utf-8"));
|
|
3664
4103
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3665
4104
|
`);
|
|
3666
4105
|
} else {
|
|
@@ -3670,7 +4109,7 @@ async function runBaseline(options = {}) {
|
|
|
3670
4109
|
process.stderr.write(`Found ${count} components.
|
|
3671
4110
|
`);
|
|
3672
4111
|
}
|
|
3673
|
-
writeFileSync6(
|
|
4112
|
+
writeFileSync6(resolve10(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
3674
4113
|
let componentNames = Object.keys(manifest.components);
|
|
3675
4114
|
if (componentsGlob !== void 0) {
|
|
3676
4115
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -3691,7 +4130,7 @@ async function runBaseline(options = {}) {
|
|
|
3691
4130
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3692
4131
|
};
|
|
3693
4132
|
writeFileSync6(
|
|
3694
|
-
|
|
4133
|
+
resolve10(baselineDir, "compliance.json"),
|
|
3695
4134
|
JSON.stringify(emptyReport, null, 2),
|
|
3696
4135
|
"utf-8"
|
|
3697
4136
|
);
|
|
@@ -3712,7 +4151,7 @@ async function runBaseline(options = {}) {
|
|
|
3712
4151
|
const renderOne = async (name) => {
|
|
3713
4152
|
const descriptor = manifest.components[name];
|
|
3714
4153
|
if (descriptor === void 0) return;
|
|
3715
|
-
const filePath =
|
|
4154
|
+
const filePath = resolve10(rootDir, descriptor.filePath);
|
|
3716
4155
|
const outcome = await safeRender3(
|
|
3717
4156
|
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3718
4157
|
{
|
|
@@ -3731,7 +4170,7 @@ async function runBaseline(options = {}) {
|
|
|
3731
4170
|
}
|
|
3732
4171
|
if (outcome.crashed) {
|
|
3733
4172
|
failureCount++;
|
|
3734
|
-
const errPath =
|
|
4173
|
+
const errPath = resolve10(rendersDir, `${name}.error.json`);
|
|
3735
4174
|
writeFileSync6(
|
|
3736
4175
|
errPath,
|
|
3737
4176
|
JSON.stringify(
|
|
@@ -3749,10 +4188,10 @@ async function runBaseline(options = {}) {
|
|
|
3749
4188
|
return;
|
|
3750
4189
|
}
|
|
3751
4190
|
const result = outcome.result;
|
|
3752
|
-
writeFileSync6(
|
|
4191
|
+
writeFileSync6(resolve10(rendersDir, `${name}.png`), result.screenshot);
|
|
3753
4192
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
3754
4193
|
writeFileSync6(
|
|
3755
|
-
|
|
4194
|
+
resolve10(rendersDir, `${name}.json`),
|
|
3756
4195
|
JSON.stringify(jsonOutput, null, 2),
|
|
3757
4196
|
"utf-8"
|
|
3758
4197
|
);
|
|
@@ -3780,7 +4219,7 @@ async function runBaseline(options = {}) {
|
|
|
3780
4219
|
const engine = new ComplianceEngine2(resolver);
|
|
3781
4220
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
3782
4221
|
writeFileSync6(
|
|
3783
|
-
|
|
4222
|
+
resolve10(baselineDir, "compliance.json"),
|
|
3784
4223
|
JSON.stringify(batchReport, null, 2),
|
|
3785
4224
|
"utf-8"
|
|
3786
4225
|
);
|
|
@@ -3823,22 +4262,22 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3823
4262
|
}
|
|
3824
4263
|
|
|
3825
4264
|
// src/report/diff.ts
|
|
3826
|
-
import { existsSync as
|
|
3827
|
-
import { resolve as
|
|
4265
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4266
|
+
import { resolve as resolve11 } from "path";
|
|
3828
4267
|
import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
|
|
3829
4268
|
import { BrowserPool as BrowserPool5, safeRender as safeRender4 } from "@agent-scope/render";
|
|
3830
4269
|
import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3 } from "@agent-scope/tokens";
|
|
3831
4270
|
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3832
4271
|
function loadBaselineCompliance(baselineDir) {
|
|
3833
|
-
const compliancePath =
|
|
3834
|
-
if (!
|
|
3835
|
-
const raw = JSON.parse(
|
|
4272
|
+
const compliancePath = resolve11(baselineDir, "compliance.json");
|
|
4273
|
+
if (!existsSync9(compliancePath)) return null;
|
|
4274
|
+
const raw = JSON.parse(readFileSync7(compliancePath, "utf-8"));
|
|
3836
4275
|
return raw;
|
|
3837
4276
|
}
|
|
3838
4277
|
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3839
|
-
const jsonPath =
|
|
3840
|
-
if (!
|
|
3841
|
-
return JSON.parse(
|
|
4278
|
+
const jsonPath = resolve11(baselineDir, "renders", `${componentName}.json`);
|
|
4279
|
+
if (!existsSync9(jsonPath)) return null;
|
|
4280
|
+
return JSON.parse(readFileSync7(jsonPath, "utf-8"));
|
|
3842
4281
|
}
|
|
3843
4282
|
var _pool5 = null;
|
|
3844
4283
|
async function getPool5(viewportWidth, viewportHeight) {
|
|
@@ -4005,19 +4444,19 @@ async function runDiff(options = {}) {
|
|
|
4005
4444
|
} = options;
|
|
4006
4445
|
const startTime = performance.now();
|
|
4007
4446
|
const rootDir = process.cwd();
|
|
4008
|
-
const baselineDir =
|
|
4009
|
-
if (!
|
|
4447
|
+
const baselineDir = resolve11(rootDir, baselineDirRaw);
|
|
4448
|
+
if (!existsSync9(baselineDir)) {
|
|
4010
4449
|
throw new Error(
|
|
4011
4450
|
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
4012
4451
|
);
|
|
4013
4452
|
}
|
|
4014
|
-
const baselineManifestPath =
|
|
4015
|
-
if (!
|
|
4453
|
+
const baselineManifestPath = resolve11(baselineDir, "manifest.json");
|
|
4454
|
+
if (!existsSync9(baselineManifestPath)) {
|
|
4016
4455
|
throw new Error(
|
|
4017
4456
|
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
4018
4457
|
);
|
|
4019
4458
|
}
|
|
4020
|
-
const baselineManifest = JSON.parse(
|
|
4459
|
+
const baselineManifest = JSON.parse(readFileSync7(baselineManifestPath, "utf-8"));
|
|
4021
4460
|
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
4022
4461
|
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
4023
4462
|
process.stderr.write(
|
|
@@ -4026,11 +4465,11 @@ async function runDiff(options = {}) {
|
|
|
4026
4465
|
);
|
|
4027
4466
|
let currentManifest;
|
|
4028
4467
|
if (manifestPath !== void 0) {
|
|
4029
|
-
const absPath =
|
|
4030
|
-
if (!
|
|
4468
|
+
const absPath = resolve11(rootDir, manifestPath);
|
|
4469
|
+
if (!existsSync9(absPath)) {
|
|
4031
4470
|
throw new Error(`Manifest not found at "${absPath}".`);
|
|
4032
4471
|
}
|
|
4033
|
-
currentManifest = JSON.parse(
|
|
4472
|
+
currentManifest = JSON.parse(readFileSync7(absPath, "utf-8"));
|
|
4034
4473
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
4035
4474
|
`);
|
|
4036
4475
|
} else {
|
|
@@ -4063,7 +4502,7 @@ async function runDiff(options = {}) {
|
|
|
4063
4502
|
const renderOne = async (name) => {
|
|
4064
4503
|
const descriptor = currentManifest.components[name];
|
|
4065
4504
|
if (descriptor === void 0) return;
|
|
4066
|
-
const filePath =
|
|
4505
|
+
const filePath = resolve11(rootDir, descriptor.filePath);
|
|
4067
4506
|
const outcome = await safeRender4(
|
|
4068
4507
|
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
4069
4508
|
{
|
|
@@ -4303,8 +4742,8 @@ function registerDiffSubCommand(reportCmd) {
|
|
|
4303
4742
|
}
|
|
4304
4743
|
|
|
4305
4744
|
// src/report/pr-comment.ts
|
|
4306
|
-
import { existsSync as
|
|
4307
|
-
import { resolve as
|
|
4745
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4746
|
+
import { resolve as resolve12 } from "path";
|
|
4308
4747
|
var STATUS_BADGE = {
|
|
4309
4748
|
added: "\u2705 added",
|
|
4310
4749
|
removed: "\u{1F5D1}\uFE0F removed",
|
|
@@ -4387,13 +4826,13 @@ function formatPrComment(diff) {
|
|
|
4387
4826
|
return lines.join("\n");
|
|
4388
4827
|
}
|
|
4389
4828
|
function loadDiffResult(filePath) {
|
|
4390
|
-
const abs =
|
|
4391
|
-
if (!
|
|
4829
|
+
const abs = resolve12(filePath);
|
|
4830
|
+
if (!existsSync10(abs)) {
|
|
4392
4831
|
throw new Error(`DiffResult file not found: ${abs}`);
|
|
4393
4832
|
}
|
|
4394
4833
|
let raw;
|
|
4395
4834
|
try {
|
|
4396
|
-
raw =
|
|
4835
|
+
raw = readFileSync8(abs, "utf-8");
|
|
4397
4836
|
} catch (err) {
|
|
4398
4837
|
throw new Error(
|
|
4399
4838
|
`Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4420,7 +4859,7 @@ function registerPrCommentSubCommand(reportCmd) {
|
|
|
4420
4859
|
const diff = loadDiffResult(opts.input);
|
|
4421
4860
|
const comment = formatPrComment(diff);
|
|
4422
4861
|
if (opts.output !== void 0) {
|
|
4423
|
-
writeFileSync8(
|
|
4862
|
+
writeFileSync8(resolve12(opts.output), comment, "utf-8");
|
|
4424
4863
|
process.stderr.write(`PR comment written to ${opts.output}
|
|
4425
4864
|
`);
|
|
4426
4865
|
} else {
|
|
@@ -4715,9 +5154,9 @@ function buildStructuredReport(report) {
|
|
|
4715
5154
|
}
|
|
4716
5155
|
|
|
4717
5156
|
// src/site-commands.ts
|
|
4718
|
-
import { createReadStream, existsSync as
|
|
5157
|
+
import { createReadStream, existsSync as existsSync11, statSync } from "fs";
|
|
4719
5158
|
import { createServer } from "http";
|
|
4720
|
-
import { extname, join as
|
|
5159
|
+
import { extname, join as join4, resolve as resolve13 } from "path";
|
|
4721
5160
|
import { buildSite } from "@agent-scope/site";
|
|
4722
5161
|
import { Command as Command7 } from "commander";
|
|
4723
5162
|
var MIME_TYPES = {
|
|
@@ -4735,16 +5174,16 @@ function registerBuild(siteCmd) {
|
|
|
4735
5174
|
siteCmd.command("build").description("Build a static HTML gallery from .reactscope/ output").option("-i, --input <path>", "Path to .reactscope input directory", ".reactscope").option("-o, --output <path>", "Output directory for generated site", ".reactscope/site").option("--base-path <path>", "Base URL path prefix for subdirectory deployment", "/").option("--compliance <path>", "Path to compliance batch report JSON").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
|
|
4736
5175
|
async (opts) => {
|
|
4737
5176
|
try {
|
|
4738
|
-
const inputDir =
|
|
4739
|
-
const outputDir =
|
|
4740
|
-
if (!
|
|
5177
|
+
const inputDir = resolve13(process.cwd(), opts.input);
|
|
5178
|
+
const outputDir = resolve13(process.cwd(), opts.output);
|
|
5179
|
+
if (!existsSync11(inputDir)) {
|
|
4741
5180
|
throw new Error(
|
|
4742
5181
|
`Input directory not found: ${inputDir}
|
|
4743
5182
|
Run \`scope manifest generate\` and \`scope render\` first.`
|
|
4744
5183
|
);
|
|
4745
5184
|
}
|
|
4746
|
-
const manifestPath =
|
|
4747
|
-
if (!
|
|
5185
|
+
const manifestPath = join4(inputDir, "manifest.json");
|
|
5186
|
+
if (!existsSync11(manifestPath)) {
|
|
4748
5187
|
throw new Error(
|
|
4749
5188
|
`Manifest not found at ${manifestPath}
|
|
4750
5189
|
Run \`scope manifest generate\` first.`
|
|
@@ -4757,7 +5196,7 @@ Run \`scope manifest generate\` first.`
|
|
|
4757
5196
|
outputDir,
|
|
4758
5197
|
basePath: opts.basePath,
|
|
4759
5198
|
...opts.compliance !== void 0 && {
|
|
4760
|
-
compliancePath:
|
|
5199
|
+
compliancePath: resolve13(process.cwd(), opts.compliance)
|
|
4761
5200
|
},
|
|
4762
5201
|
title: opts.title
|
|
4763
5202
|
});
|
|
@@ -4780,8 +5219,8 @@ function registerServe(siteCmd) {
|
|
|
4780
5219
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
4781
5220
|
throw new Error(`Invalid port: ${opts.port}`);
|
|
4782
5221
|
}
|
|
4783
|
-
const serveDir =
|
|
4784
|
-
if (!
|
|
5222
|
+
const serveDir = resolve13(process.cwd(), opts.dir);
|
|
5223
|
+
if (!existsSync11(serveDir)) {
|
|
4785
5224
|
throw new Error(
|
|
4786
5225
|
`Serve directory not found: ${serveDir}
|
|
4787
5226
|
Run \`scope site build\` first.`
|
|
@@ -4790,13 +5229,13 @@ Run \`scope site build\` first.`
|
|
|
4790
5229
|
const server = createServer((req, res) => {
|
|
4791
5230
|
const rawUrl = req.url ?? "/";
|
|
4792
5231
|
const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
|
|
4793
|
-
const filePath =
|
|
5232
|
+
const filePath = join4(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
|
|
4794
5233
|
if (!filePath.startsWith(serveDir)) {
|
|
4795
5234
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
4796
5235
|
res.end("Forbidden");
|
|
4797
5236
|
return;
|
|
4798
5237
|
}
|
|
4799
|
-
if (
|
|
5238
|
+
if (existsSync11(filePath) && statSync(filePath).isFile()) {
|
|
4800
5239
|
const ext = extname(filePath).toLowerCase();
|
|
4801
5240
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4802
5241
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -4804,7 +5243,7 @@ Run \`scope site build\` first.`
|
|
|
4804
5243
|
return;
|
|
4805
5244
|
}
|
|
4806
5245
|
const htmlPath = `${filePath}.html`;
|
|
4807
|
-
if (
|
|
5246
|
+
if (existsSync11(htmlPath) && statSync(htmlPath).isFile()) {
|
|
4808
5247
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4809
5248
|
createReadStream(htmlPath).pipe(res);
|
|
4810
5249
|
return;
|
|
@@ -4846,8 +5285,8 @@ function createSiteCommand() {
|
|
|
4846
5285
|
}
|
|
4847
5286
|
|
|
4848
5287
|
// src/tokens/commands.ts
|
|
4849
|
-
import { existsSync as
|
|
4850
|
-
import { resolve as
|
|
5288
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
|
|
5289
|
+
import { resolve as resolve17 } from "path";
|
|
4851
5290
|
import {
|
|
4852
5291
|
parseTokenFileSync as parseTokenFileSync2,
|
|
4853
5292
|
TokenParseError,
|
|
@@ -4858,23 +5297,23 @@ import {
|
|
|
4858
5297
|
import { Command as Command9 } from "commander";
|
|
4859
5298
|
|
|
4860
5299
|
// src/tokens/compliance.ts
|
|
4861
|
-
import { existsSync as
|
|
4862
|
-
import { resolve as
|
|
5300
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
5301
|
+
import { resolve as resolve14 } from "path";
|
|
4863
5302
|
import {
|
|
4864
5303
|
ComplianceEngine as ComplianceEngine4,
|
|
4865
5304
|
TokenResolver as TokenResolver4
|
|
4866
5305
|
} from "@agent-scope/tokens";
|
|
4867
5306
|
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
4868
5307
|
function loadStylesFile(stylesPath) {
|
|
4869
|
-
const absPath =
|
|
4870
|
-
if (!
|
|
5308
|
+
const absPath = resolve14(process.cwd(), stylesPath);
|
|
5309
|
+
if (!existsSync12(absPath)) {
|
|
4871
5310
|
throw new Error(
|
|
4872
5311
|
`Compliance styles file not found at ${absPath}.
|
|
4873
5312
|
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
4874
5313
|
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
4875
5314
|
);
|
|
4876
5315
|
}
|
|
4877
|
-
const raw =
|
|
5316
|
+
const raw = readFileSync9(absPath, "utf-8");
|
|
4878
5317
|
let parsed;
|
|
4879
5318
|
try {
|
|
4880
5319
|
parsed = JSON.parse(raw);
|
|
@@ -5034,8 +5473,8 @@ function registerCompliance(tokensCmd) {
|
|
|
5034
5473
|
}
|
|
5035
5474
|
|
|
5036
5475
|
// src/tokens/export.ts
|
|
5037
|
-
import { existsSync as
|
|
5038
|
-
import { resolve as
|
|
5476
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
5477
|
+
import { resolve as resolve15 } from "path";
|
|
5039
5478
|
import {
|
|
5040
5479
|
exportTokens,
|
|
5041
5480
|
parseTokenFileSync,
|
|
@@ -5048,21 +5487,21 @@ var CONFIG_FILE = "reactscope.config.json";
|
|
|
5048
5487
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
5049
5488
|
function resolveTokenFilePath2(fileFlag) {
|
|
5050
5489
|
if (fileFlag !== void 0) {
|
|
5051
|
-
return
|
|
5490
|
+
return resolve15(process.cwd(), fileFlag);
|
|
5052
5491
|
}
|
|
5053
|
-
const configPath =
|
|
5054
|
-
if (
|
|
5492
|
+
const configPath = resolve15(process.cwd(), CONFIG_FILE);
|
|
5493
|
+
if (existsSync13(configPath)) {
|
|
5055
5494
|
try {
|
|
5056
|
-
const raw =
|
|
5495
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
5057
5496
|
const config = JSON.parse(raw);
|
|
5058
5497
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
5059
5498
|
const file = config.tokens.file;
|
|
5060
|
-
return
|
|
5499
|
+
return resolve15(process.cwd(), file);
|
|
5061
5500
|
}
|
|
5062
5501
|
} catch {
|
|
5063
5502
|
}
|
|
5064
5503
|
}
|
|
5065
|
-
return
|
|
5504
|
+
return resolve15(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
5066
5505
|
}
|
|
5067
5506
|
function createTokensExportCommand() {
|
|
5068
5507
|
return new Command8("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(
|
|
@@ -5071,23 +5510,33 @@ function createTokensExportCommand() {
|
|
|
5071
5510
|
).action(
|
|
5072
5511
|
(opts) => {
|
|
5073
5512
|
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
5513
|
+
const FORMAT_ALIASES = {
|
|
5514
|
+
json: "flat-json",
|
|
5515
|
+
"json-flat": "flat-json",
|
|
5516
|
+
javascript: "ts",
|
|
5517
|
+
js: "ts",
|
|
5518
|
+
sass: "scss",
|
|
5519
|
+
tw: "tailwind"
|
|
5520
|
+
};
|
|
5521
|
+
const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
|
|
5074
5522
|
process.stderr.write(
|
|
5075
5523
|
`Error: unsupported format "${opts.format}".
|
|
5076
5524
|
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
5077
|
-
`
|
|
5525
|
+
` + (hint ? `Did you mean "${hint}"?
|
|
5526
|
+
` : "")
|
|
5078
5527
|
);
|
|
5079
5528
|
process.exit(1);
|
|
5080
5529
|
}
|
|
5081
5530
|
const format = opts.format;
|
|
5082
5531
|
try {
|
|
5083
5532
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
5084
|
-
if (!
|
|
5533
|
+
if (!existsSync13(filePath)) {
|
|
5085
5534
|
throw new Error(
|
|
5086
5535
|
`Token file not found at ${filePath}.
|
|
5087
5536
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5088
5537
|
);
|
|
5089
5538
|
}
|
|
5090
|
-
const raw =
|
|
5539
|
+
const raw = readFileSync10(filePath, "utf-8");
|
|
5091
5540
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
5092
5541
|
let themesMap;
|
|
5093
5542
|
if (opts.theme !== void 0) {
|
|
@@ -5126,7 +5575,7 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
5126
5575
|
themes: themesMap
|
|
5127
5576
|
});
|
|
5128
5577
|
if (opts.out !== void 0) {
|
|
5129
|
-
const outPath =
|
|
5578
|
+
const outPath = resolve15(process.cwd(), opts.out);
|
|
5130
5579
|
writeFileSync9(outPath, output, "utf-8");
|
|
5131
5580
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
5132
5581
|
`);
|
|
@@ -5241,8 +5690,8 @@ ${formatImpactSummary(report)}
|
|
|
5241
5690
|
}
|
|
5242
5691
|
|
|
5243
5692
|
// src/tokens/preview.ts
|
|
5244
|
-
import { mkdirSync as
|
|
5245
|
-
import { resolve as
|
|
5693
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
5694
|
+
import { resolve as resolve16 } from "path";
|
|
5246
5695
|
import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
|
|
5247
5696
|
import { ComplianceEngine as ComplianceEngine6, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver7 } from "@agent-scope/tokens";
|
|
5248
5697
|
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
@@ -5423,9 +5872,9 @@ function registerPreview(tokensCmd) {
|
|
|
5423
5872
|
});
|
|
5424
5873
|
const spriteResult = await generator.generate(matrixResult);
|
|
5425
5874
|
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
5426
|
-
const outputPath = opts.output ??
|
|
5427
|
-
const outputDir =
|
|
5428
|
-
|
|
5875
|
+
const outputPath = opts.output ?? resolve16(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
5876
|
+
const outputDir = resolve16(outputPath, "..");
|
|
5877
|
+
mkdirSync6(outputDir, { recursive: true });
|
|
5429
5878
|
writeFileSync10(outputPath, spriteResult.png);
|
|
5430
5879
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
5431
5880
|
if (useJson) {
|
|
@@ -5485,30 +5934,30 @@ function buildTable2(headers, rows) {
|
|
|
5485
5934
|
}
|
|
5486
5935
|
function resolveTokenFilePath(fileFlag) {
|
|
5487
5936
|
if (fileFlag !== void 0) {
|
|
5488
|
-
return
|
|
5937
|
+
return resolve17(process.cwd(), fileFlag);
|
|
5489
5938
|
}
|
|
5490
|
-
const configPath =
|
|
5491
|
-
if (
|
|
5939
|
+
const configPath = resolve17(process.cwd(), CONFIG_FILE2);
|
|
5940
|
+
if (existsSync14(configPath)) {
|
|
5492
5941
|
try {
|
|
5493
|
-
const raw =
|
|
5942
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
5494
5943
|
const config = JSON.parse(raw);
|
|
5495
5944
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
5496
5945
|
const file = config.tokens.file;
|
|
5497
|
-
return
|
|
5946
|
+
return resolve17(process.cwd(), file);
|
|
5498
5947
|
}
|
|
5499
5948
|
} catch {
|
|
5500
5949
|
}
|
|
5501
5950
|
}
|
|
5502
|
-
return
|
|
5951
|
+
return resolve17(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
5503
5952
|
}
|
|
5504
5953
|
function loadTokens(absPath) {
|
|
5505
|
-
if (!
|
|
5954
|
+
if (!existsSync14(absPath)) {
|
|
5506
5955
|
throw new Error(
|
|
5507
5956
|
`Token file not found at ${absPath}.
|
|
5508
5957
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5509
5958
|
);
|
|
5510
5959
|
}
|
|
5511
|
-
const raw =
|
|
5960
|
+
const raw = readFileSync11(absPath, "utf-8");
|
|
5512
5961
|
return parseTokenFileSync2(raw);
|
|
5513
5962
|
}
|
|
5514
5963
|
function getRawValue(node, segments) {
|
|
@@ -5722,13 +6171,13 @@ function registerValidate(tokensCmd) {
|
|
|
5722
6171
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
5723
6172
|
try {
|
|
5724
6173
|
const filePath = resolveTokenFilePath(opts.file);
|
|
5725
|
-
if (!
|
|
6174
|
+
if (!existsSync14(filePath)) {
|
|
5726
6175
|
throw new Error(
|
|
5727
6176
|
`Token file not found at ${filePath}.
|
|
5728
6177
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5729
6178
|
);
|
|
5730
6179
|
}
|
|
5731
|
-
const raw =
|
|
6180
|
+
const raw = readFileSync11(filePath, "utf-8");
|
|
5732
6181
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
5733
6182
|
const errors = [];
|
|
5734
6183
|
let parsed;
|
|
@@ -5886,7 +6335,7 @@ function createProgram(options = {}) {
|
|
|
5886
6335
|
}
|
|
5887
6336
|
);
|
|
5888
6337
|
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) => {
|
|
5889
|
-
const raw =
|
|
6338
|
+
const raw = readFileSync12(tracePath, "utf-8");
|
|
5890
6339
|
const trace = loadTrace(raw);
|
|
5891
6340
|
const source = generateTest(trace, {
|
|
5892
6341
|
description: opts.description,
|