@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/index.js
CHANGED
|
@@ -4,18 +4,19 @@ import { generateManifest } from '@agent-scope/manifest';
|
|
|
4
4
|
import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
5
5
|
import { TokenResolver, ComplianceEngine, parseTokenFileSync, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import * as
|
|
7
|
+
import * as esbuild2 from 'esbuild';
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import * as readline from 'readline';
|
|
10
10
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
11
11
|
import { chromium } from 'playwright';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
12
13
|
import { createServer } from 'http';
|
|
13
14
|
import { buildSite } from '@agent-scope/site';
|
|
14
15
|
|
|
15
16
|
// src/ci/commands.ts
|
|
16
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss,
|
|
17
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
|
|
17
18
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
18
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
19
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
|
|
19
20
|
}
|
|
20
21
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
21
22
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -51,7 +52,12 @@ import { createElement } from "react";
|
|
|
51
52
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
// If a scope file wrapper was injected, use it to wrap the component
|
|
56
|
+
var Wrapper = (window).__SCOPE_WRAPPER__;
|
|
57
|
+
var element = Wrapper
|
|
58
|
+
? createElement(Wrapper, null, createElement(Component, props))
|
|
59
|
+
: createElement(Component, props);
|
|
60
|
+
createRoot(rootEl).render(element);
|
|
55
61
|
// Use requestAnimationFrame to let React flush the render
|
|
56
62
|
requestAnimationFrame(function() {
|
|
57
63
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
@@ -63,7 +69,7 @@ import { createElement } from "react";
|
|
|
63
69
|
})();
|
|
64
70
|
`
|
|
65
71
|
);
|
|
66
|
-
const result = await
|
|
72
|
+
const result = await esbuild2.build({
|
|
67
73
|
stdin: {
|
|
68
74
|
contents: wrapperCode,
|
|
69
75
|
// Resolve relative imports (within the component's dir)
|
|
@@ -88,6 +94,21 @@ import { createElement } from "react";
|
|
|
88
94
|
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
89
95
|
banner: {
|
|
90
96
|
js: "/* @agent-scope/cli component harness */"
|
|
97
|
+
},
|
|
98
|
+
// CSS imports (e.g. `import './styles.css'`) are handled at the page level via
|
|
99
|
+
// globalCSS injection. Tell esbuild to treat CSS files as empty modules so
|
|
100
|
+
// components that import CSS directly (e.g. App.tsx) don't error during bundling.
|
|
101
|
+
loader: {
|
|
102
|
+
".css": "empty",
|
|
103
|
+
".svg": "dataurl",
|
|
104
|
+
".png": "dataurl",
|
|
105
|
+
".jpg": "dataurl",
|
|
106
|
+
".jpeg": "dataurl",
|
|
107
|
+
".gif": "dataurl",
|
|
108
|
+
".webp": "dataurl",
|
|
109
|
+
".ttf": "dataurl",
|
|
110
|
+
".woff": "dataurl",
|
|
111
|
+
".woff2": "dataurl"
|
|
91
112
|
}
|
|
92
113
|
});
|
|
93
114
|
if (result.errors.length > 0) {
|
|
@@ -101,12 +122,11 @@ ${msg}`);
|
|
|
101
122
|
}
|
|
102
123
|
return outputFile.text;
|
|
103
124
|
}
|
|
104
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
125
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
|
|
105
126
|
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
106
127
|
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
107
128
|
</style>` : "";
|
|
108
|
-
const
|
|
109
|
-
` : "";
|
|
129
|
+
const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
|
|
110
130
|
return `<!DOCTYPE html>
|
|
111
131
|
<html lang="en">
|
|
112
132
|
<head>
|
|
@@ -121,7 +141,8 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
|
121
141
|
</head>
|
|
122
142
|
<body>
|
|
123
143
|
<div id="scope-root" data-reactscope-root></div>
|
|
124
|
-
${
|
|
144
|
+
${wrapperScriptBlock}
|
|
145
|
+
<script>${bundledScript}</script>
|
|
125
146
|
</body>
|
|
126
147
|
</html>`;
|
|
127
148
|
}
|
|
@@ -484,16 +505,67 @@ async function getTailwindCompiler(cwd) {
|
|
|
484
505
|
from: entryPath,
|
|
485
506
|
loadStylesheet
|
|
486
507
|
});
|
|
487
|
-
const
|
|
488
|
-
compilerCache = { cwd, build:
|
|
489
|
-
return
|
|
508
|
+
const build3 = result.build.bind(result);
|
|
509
|
+
compilerCache = { cwd, build: build3 };
|
|
510
|
+
return build3;
|
|
490
511
|
}
|
|
491
512
|
async function getCompiledCssForClasses(cwd, classes) {
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
513
|
+
const build3 = await getTailwindCompiler(cwd);
|
|
514
|
+
if (build3 === null) return null;
|
|
494
515
|
const deduped = [...new Set(classes)].filter(Boolean);
|
|
495
516
|
if (deduped.length === 0) return null;
|
|
496
|
-
return
|
|
517
|
+
return build3(deduped);
|
|
518
|
+
}
|
|
519
|
+
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
520
|
+
const { existsSync: existsSync15, readFileSync: readFileSync13 } = await import('fs');
|
|
521
|
+
const { createRequire: createRequire3 } = await import('module');
|
|
522
|
+
if (!existsSync15(cssFilePath)) return null;
|
|
523
|
+
const raw = readFileSync13(cssFilePath, "utf-8");
|
|
524
|
+
const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
|
|
525
|
+
if (!needsCompile) {
|
|
526
|
+
return raw;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const require2 = createRequire3(resolve(cwd, "package.json"));
|
|
530
|
+
let postcss;
|
|
531
|
+
let twPlugin;
|
|
532
|
+
try {
|
|
533
|
+
postcss = require2("postcss");
|
|
534
|
+
twPlugin = require2("tailwindcss");
|
|
535
|
+
} catch {
|
|
536
|
+
return raw;
|
|
537
|
+
}
|
|
538
|
+
let autoprefixerPlugin;
|
|
539
|
+
try {
|
|
540
|
+
autoprefixerPlugin = require2("autoprefixer");
|
|
541
|
+
} catch {
|
|
542
|
+
autoprefixerPlugin = null;
|
|
543
|
+
}
|
|
544
|
+
const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
|
|
545
|
+
const result = await postcss(plugins).process(raw, {
|
|
546
|
+
from: cssFilePath,
|
|
547
|
+
to: cssFilePath
|
|
548
|
+
});
|
|
549
|
+
return result.css;
|
|
550
|
+
} catch (err) {
|
|
551
|
+
process.stderr.write(
|
|
552
|
+
`[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
|
|
553
|
+
`
|
|
554
|
+
);
|
|
555
|
+
return raw;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function loadGlobalCss(globalCssFiles, cwd) {
|
|
559
|
+
if (globalCssFiles.length === 0) return null;
|
|
560
|
+
const parts = [];
|
|
561
|
+
for (const relPath of globalCssFiles) {
|
|
562
|
+
const absPath = resolve(cwd, relPath);
|
|
563
|
+
const css = await compileGlobalCssFile(absPath, cwd);
|
|
564
|
+
if (css !== null && css.trim().length > 0) {
|
|
565
|
+
parts.push(css);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
497
569
|
}
|
|
498
570
|
|
|
499
571
|
// src/ci/commands.ts
|
|
@@ -998,6 +1070,20 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
998
1070
|
}
|
|
999
1071
|
return unique;
|
|
1000
1072
|
}
|
|
1073
|
+
var GLOBAL_CSS_CANDIDATES = [
|
|
1074
|
+
"src/styles.css",
|
|
1075
|
+
"src/index.css",
|
|
1076
|
+
"src/global.css",
|
|
1077
|
+
"src/globals.css",
|
|
1078
|
+
"src/app.css",
|
|
1079
|
+
"src/main.css",
|
|
1080
|
+
"styles/globals.css",
|
|
1081
|
+
"styles/global.css",
|
|
1082
|
+
"styles/index.css"
|
|
1083
|
+
];
|
|
1084
|
+
function detectGlobalCSSFiles(rootDir) {
|
|
1085
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync(join(rootDir, rel)));
|
|
1086
|
+
}
|
|
1001
1087
|
var TAILWIND_STEMS = ["tailwind.config"];
|
|
1002
1088
|
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
1003
1089
|
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
@@ -1065,13 +1151,15 @@ function detectProject(rootDir) {
|
|
|
1065
1151
|
const packageManager = detectPackageManager(rootDir);
|
|
1066
1152
|
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
1067
1153
|
const tokenSources = detectTokenSources(rootDir);
|
|
1154
|
+
const globalCSSFiles = detectGlobalCSSFiles(rootDir);
|
|
1068
1155
|
return {
|
|
1069
1156
|
framework,
|
|
1070
1157
|
typescript,
|
|
1071
1158
|
tsconfigPath,
|
|
1072
1159
|
componentPatterns,
|
|
1073
1160
|
tokenSources,
|
|
1074
|
-
packageManager
|
|
1161
|
+
packageManager,
|
|
1162
|
+
globalCSSFiles
|
|
1075
1163
|
};
|
|
1076
1164
|
}
|
|
1077
1165
|
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
@@ -1080,7 +1168,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
|
1080
1168
|
components: {
|
|
1081
1169
|
include,
|
|
1082
1170
|
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
1083
|
-
wrappers: { providers: [], globalCSS: [] }
|
|
1171
|
+
wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
|
|
1084
1172
|
},
|
|
1085
1173
|
render: {
|
|
1086
1174
|
viewport: { default: { width: 1280, height: 800 } },
|
|
@@ -1111,9 +1199,9 @@ function createRL() {
|
|
|
1111
1199
|
});
|
|
1112
1200
|
}
|
|
1113
1201
|
async function ask(rl, question) {
|
|
1114
|
-
return new Promise((
|
|
1202
|
+
return new Promise((resolve18) => {
|
|
1115
1203
|
rl.question(question, (answer) => {
|
|
1116
|
-
|
|
1204
|
+
resolve18(answer.trim());
|
|
1117
1205
|
});
|
|
1118
1206
|
});
|
|
1119
1207
|
}
|
|
@@ -1138,18 +1226,118 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1138
1226
|
`);
|
|
1139
1227
|
}
|
|
1140
1228
|
}
|
|
1229
|
+
function extractTailwindTokens(tokenSources) {
|
|
1230
|
+
const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
|
|
1231
|
+
if (!tailwindSource) return null;
|
|
1232
|
+
try {
|
|
1233
|
+
let parseBlock2 = function(block) {
|
|
1234
|
+
const result = {};
|
|
1235
|
+
const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
|
|
1236
|
+
for (const m of block.matchAll(lineRe)) {
|
|
1237
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
1238
|
+
result[m[1]] = m[2];
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return result;
|
|
1242
|
+
};
|
|
1243
|
+
var parseBlock = parseBlock2;
|
|
1244
|
+
const raw = readFileSync(tailwindSource.path, "utf-8");
|
|
1245
|
+
const tokens = {};
|
|
1246
|
+
const colorsKeyIdx = raw.indexOf("colors:");
|
|
1247
|
+
if (colorsKeyIdx !== -1) {
|
|
1248
|
+
const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
|
|
1249
|
+
if (colorsBraceStart !== -1) {
|
|
1250
|
+
let colorDepth = 0;
|
|
1251
|
+
let colorsBraceEnd = -1;
|
|
1252
|
+
for (let ci = colorsBraceStart; ci < raw.length; ci++) {
|
|
1253
|
+
if (raw[ci] === "{") colorDepth++;
|
|
1254
|
+
else if (raw[ci] === "}") {
|
|
1255
|
+
colorDepth--;
|
|
1256
|
+
if (colorDepth === 0) {
|
|
1257
|
+
colorsBraceEnd = ci;
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (colorsBraceEnd > colorsBraceStart) {
|
|
1263
|
+
const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
|
|
1264
|
+
const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1265
|
+
const colorTokens = {};
|
|
1266
|
+
for (const sm of colorSection.matchAll(scaleRe)) {
|
|
1267
|
+
if (sm[1] === void 0 || sm[2] === void 0) continue;
|
|
1268
|
+
const scaleName = sm[1];
|
|
1269
|
+
const scaleValues = parseBlock2(sm[2]);
|
|
1270
|
+
if (Object.keys(scaleValues).length > 0) {
|
|
1271
|
+
const scaleTokens = {};
|
|
1272
|
+
for (const [step, hex] of Object.entries(scaleValues)) {
|
|
1273
|
+
scaleTokens[step] = { value: hex, type: "color" };
|
|
1274
|
+
}
|
|
1275
|
+
colorTokens[scaleName] = scaleTokens;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (Object.keys(colorTokens).length > 0) {
|
|
1279
|
+
tokens["color"] = colorTokens;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1285
|
+
if (spacingMatch?.[1] !== void 0) {
|
|
1286
|
+
const spacingValues = parseBlock2(spacingMatch[1]);
|
|
1287
|
+
if (Object.keys(spacingValues).length > 0) {
|
|
1288
|
+
const spacingTokens = {};
|
|
1289
|
+
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1290
|
+
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1291
|
+
}
|
|
1292
|
+
tokens["spacing"] = spacingTokens;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1296
|
+
if (fontFamilyMatch?.[1] !== void 0) {
|
|
1297
|
+
const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
|
|
1298
|
+
const fontTokens = {};
|
|
1299
|
+
for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
|
|
1300
|
+
if (fm[1] !== void 0 && fm[2] !== void 0) {
|
|
1301
|
+
fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (Object.keys(fontTokens).length > 0) {
|
|
1305
|
+
tokens["font"] = fontTokens;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1309
|
+
if (borderRadiusMatch?.[1] !== void 0) {
|
|
1310
|
+
const radiusValues = parseBlock2(borderRadiusMatch[1]);
|
|
1311
|
+
if (Object.keys(radiusValues).length > 0) {
|
|
1312
|
+
const radiusTokens = {};
|
|
1313
|
+
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1314
|
+
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1315
|
+
}
|
|
1316
|
+
tokens["radius"] = radiusTokens;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1141
1324
|
function scaffoldConfig(rootDir, config) {
|
|
1142
1325
|
const path = join(rootDir, "reactscope.config.json");
|
|
1143
1326
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
1144
1327
|
`);
|
|
1145
1328
|
return path;
|
|
1146
1329
|
}
|
|
1147
|
-
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
1330
|
+
function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
|
|
1148
1331
|
const path = join(rootDir, tokenFile);
|
|
1149
1332
|
if (!existsSync(path)) {
|
|
1150
1333
|
const stub = {
|
|
1151
1334
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
1152
|
-
|
|
1335
|
+
version: "1.0.0",
|
|
1336
|
+
meta: {
|
|
1337
|
+
name: "Design Tokens",
|
|
1338
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1339
|
+
},
|
|
1340
|
+
tokens: extractedTokens ?? {}
|
|
1153
1341
|
};
|
|
1154
1342
|
writeFileSync(path, `${JSON.stringify(stub, null, 2)}
|
|
1155
1343
|
`);
|
|
@@ -1227,7 +1415,13 @@ async function runInit(options) {
|
|
|
1227
1415
|
}
|
|
1228
1416
|
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1229
1417
|
created.push(cfgPath);
|
|
1230
|
-
const
|
|
1418
|
+
const extractedTokens = extractTailwindTokens(detected.tokenSources);
|
|
1419
|
+
if (extractedTokens !== null) {
|
|
1420
|
+
const tokenGroupCount = Object.keys(extractedTokens).length;
|
|
1421
|
+
process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
|
|
1422
|
+
`);
|
|
1423
|
+
}
|
|
1424
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
|
|
1231
1425
|
created.push(tokPath);
|
|
1232
1426
|
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1233
1427
|
created.push(outDirPath);
|
|
@@ -1327,7 +1521,10 @@ Available: ${available}${hint}`
|
|
|
1327
1521
|
});
|
|
1328
1522
|
}
|
|
1329
1523
|
function registerQuery(manifestCmd) {
|
|
1330
|
-
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(
|
|
1524
|
+
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(
|
|
1525
|
+
"--has-prop <spec>",
|
|
1526
|
+
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1527
|
+
).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(
|
|
1331
1528
|
(opts) => {
|
|
1332
1529
|
try {
|
|
1333
1530
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1338,9 +1535,11 @@ function registerQuery(manifestCmd) {
|
|
|
1338
1535
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
1339
1536
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
1340
1537
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1538
|
+
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1539
|
+
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1341
1540
|
if (queryParts.length === 0) {
|
|
1342
1541
|
process.stderr.write(
|
|
1343
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects,
|
|
1542
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
|
|
1344
1543
|
);
|
|
1345
1544
|
process.exit(1);
|
|
1346
1545
|
}
|
|
@@ -1367,6 +1566,27 @@ function registerQuery(manifestCmd) {
|
|
|
1367
1566
|
if (opts.hasFetch) {
|
|
1368
1567
|
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1369
1568
|
}
|
|
1569
|
+
if (opts.hasProp !== void 0) {
|
|
1570
|
+
const spec = opts.hasProp;
|
|
1571
|
+
const colonIdx = spec.indexOf(":");
|
|
1572
|
+
const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
|
|
1573
|
+
const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
|
|
1574
|
+
entries = entries.filter(([, d]) => {
|
|
1575
|
+
const props = d.props;
|
|
1576
|
+
if (!props || !(propName in props)) return false;
|
|
1577
|
+
if (propType !== void 0) {
|
|
1578
|
+
return props[propName]?.type === propType;
|
|
1579
|
+
}
|
|
1580
|
+
return true;
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
if (opts.composedBy !== void 0) {
|
|
1584
|
+
const targetName = opts.composedBy;
|
|
1585
|
+
entries = entries.filter(([, d]) => {
|
|
1586
|
+
const composedBy = d.composedBy;
|
|
1587
|
+
return composedBy !== void 0 && composedBy.includes(targetName);
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1370
1590
|
const rows = entries.map(([name, d]) => ({
|
|
1371
1591
|
name,
|
|
1372
1592
|
file: d.filePath,
|
|
@@ -2935,6 +3155,133 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
2935
3155
|
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2936
3156
|
writeFileSync(outputPath, json, "utf-8");
|
|
2937
3157
|
}
|
|
3158
|
+
var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
|
|
3159
|
+
function findScopeFile(componentFilePath) {
|
|
3160
|
+
const dir = dirname(componentFilePath);
|
|
3161
|
+
const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
3162
|
+
const baseName = stem.slice(dir.length + 1);
|
|
3163
|
+
for (const ext of SCOPE_EXTENSIONS) {
|
|
3164
|
+
const candidate = join(dir, `${baseName}${ext}`);
|
|
3165
|
+
if (existsSync(candidate)) return candidate;
|
|
3166
|
+
}
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
async function loadScopeFile(scopeFilePath) {
|
|
3170
|
+
const tmpDir = join(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3171
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
3172
|
+
const outFile = join(tmpDir, "scope-file.cjs");
|
|
3173
|
+
try {
|
|
3174
|
+
const result = await esbuild2.build({
|
|
3175
|
+
entryPoints: [scopeFilePath],
|
|
3176
|
+
bundle: true,
|
|
3177
|
+
format: "cjs",
|
|
3178
|
+
platform: "node",
|
|
3179
|
+
target: "node18",
|
|
3180
|
+
outfile: outFile,
|
|
3181
|
+
write: true,
|
|
3182
|
+
jsx: "automatic",
|
|
3183
|
+
jsxImportSource: "react",
|
|
3184
|
+
// Externalize React — we don't need to execute JSX, just extract plain data
|
|
3185
|
+
external: ["react", "react-dom", "react/jsx-runtime"],
|
|
3186
|
+
define: {
|
|
3187
|
+
"process.env.NODE_ENV": '"development"'
|
|
3188
|
+
},
|
|
3189
|
+
logLevel: "silent"
|
|
3190
|
+
});
|
|
3191
|
+
if (result.errors.length > 0) {
|
|
3192
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3193
|
+
throw new Error(`Failed to bundle scope file ${scopeFilePath}:
|
|
3194
|
+
${msg}`);
|
|
3195
|
+
}
|
|
3196
|
+
const req = createRequire(import.meta.url);
|
|
3197
|
+
delete req.cache[resolve(outFile)];
|
|
3198
|
+
const mod = req(outFile);
|
|
3199
|
+
const scenarios = extractScenarios(mod, scopeFilePath);
|
|
3200
|
+
const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
|
|
3201
|
+
return { filePath: scopeFilePath, scenarios, hasWrapper };
|
|
3202
|
+
} finally {
|
|
3203
|
+
try {
|
|
3204
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
3205
|
+
} catch {
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
async function loadScopeFileForComponent(componentFilePath) {
|
|
3210
|
+
const scopeFilePath = findScopeFile(componentFilePath);
|
|
3211
|
+
if (scopeFilePath === null) return null;
|
|
3212
|
+
return loadScopeFile(scopeFilePath);
|
|
3213
|
+
}
|
|
3214
|
+
function extractScenarios(mod, filePath) {
|
|
3215
|
+
const raw = mod.scenarios ?? mod.default?.scenarios;
|
|
3216
|
+
if (raw === void 0) return {};
|
|
3217
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3218
|
+
console.warn(`[scope] ${filePath}: "scenarios" export is not a plain object \u2014 ignoring.`);
|
|
3219
|
+
return {};
|
|
3220
|
+
}
|
|
3221
|
+
const result = {};
|
|
3222
|
+
for (const [name, props] of Object.entries(raw)) {
|
|
3223
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) {
|
|
3224
|
+
console.warn(`[scope] ${filePath}: scenario "${name}" is not a plain object \u2014 skipping.`);
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
result[name] = props;
|
|
3228
|
+
}
|
|
3229
|
+
return result;
|
|
3230
|
+
}
|
|
3231
|
+
async function buildWrapperScript(scopeFilePath) {
|
|
3232
|
+
const wrapperEntry = (
|
|
3233
|
+
/* ts */
|
|
3234
|
+
`
|
|
3235
|
+
import * as __scopeMod from ${JSON.stringify(scopeFilePath)};
|
|
3236
|
+
// Expose the wrapper on window so the harness can access it
|
|
3237
|
+
var wrapper =
|
|
3238
|
+
__scopeMod.wrapper ??
|
|
3239
|
+
(__scopeMod.default && __scopeMod.default.wrapper) ??
|
|
3240
|
+
null;
|
|
3241
|
+
window.__SCOPE_WRAPPER__ = wrapper;
|
|
3242
|
+
`
|
|
3243
|
+
);
|
|
3244
|
+
const result = await esbuild2.build({
|
|
3245
|
+
stdin: {
|
|
3246
|
+
contents: wrapperEntry,
|
|
3247
|
+
resolveDir: dirname(scopeFilePath),
|
|
3248
|
+
loader: "tsx",
|
|
3249
|
+
sourcefile: "__scope_wrapper_entry__.tsx"
|
|
3250
|
+
},
|
|
3251
|
+
bundle: true,
|
|
3252
|
+
format: "iife",
|
|
3253
|
+
platform: "browser",
|
|
3254
|
+
target: "es2020",
|
|
3255
|
+
write: false,
|
|
3256
|
+
jsx: "automatic",
|
|
3257
|
+
jsxImportSource: "react",
|
|
3258
|
+
external: [],
|
|
3259
|
+
define: {
|
|
3260
|
+
"process.env.NODE_ENV": '"development"',
|
|
3261
|
+
global: "globalThis"
|
|
3262
|
+
},
|
|
3263
|
+
logLevel: "silent"
|
|
3264
|
+
});
|
|
3265
|
+
if (result.errors.length > 0) {
|
|
3266
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3267
|
+
throw new Error(`Failed to build wrapper script from ${scopeFilePath}:
|
|
3268
|
+
${msg}`);
|
|
3269
|
+
}
|
|
3270
|
+
return result.outputFiles?.[0]?.text ?? "";
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// src/render-commands.ts
|
|
3274
|
+
function loadGlobalCssFilesFromConfig(cwd) {
|
|
3275
|
+
const configPath = resolve(cwd, "reactscope.config.json");
|
|
3276
|
+
if (!existsSync(configPath)) return [];
|
|
3277
|
+
try {
|
|
3278
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
3279
|
+
const cfg = JSON.parse(raw);
|
|
3280
|
+
return cfg.components?.wrappers?.globalCSS ?? [];
|
|
3281
|
+
} catch {
|
|
3282
|
+
return [];
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
2938
3285
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2939
3286
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2940
3287
|
var _pool3 = null;
|
|
@@ -2955,7 +3302,7 @@ async function shutdownPool3() {
|
|
|
2955
3302
|
_pool3 = null;
|
|
2956
3303
|
}
|
|
2957
3304
|
}
|
|
2958
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
3305
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
|
|
2959
3306
|
const satori = new SatoriRenderer({
|
|
2960
3307
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
2961
3308
|
});
|
|
@@ -2964,11 +3311,14 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2964
3311
|
async renderCell(props, _complexityClass) {
|
|
2965
3312
|
const startMs = performance.now();
|
|
2966
3313
|
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
3314
|
+
const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
|
|
2967
3315
|
const htmlHarness = await buildComponentHarness(
|
|
2968
3316
|
filePath,
|
|
2969
3317
|
componentName,
|
|
2970
3318
|
props,
|
|
2971
|
-
viewportWidth
|
|
3319
|
+
viewportWidth,
|
|
3320
|
+
projectCss ?? void 0,
|
|
3321
|
+
wrapperScript
|
|
2972
3322
|
);
|
|
2973
3323
|
const slot = await pool.acquire();
|
|
2974
3324
|
const { page } = slot;
|
|
@@ -2997,9 +3347,9 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2997
3347
|
});
|
|
2998
3348
|
return [...set];
|
|
2999
3349
|
});
|
|
3000
|
-
const
|
|
3001
|
-
if (
|
|
3002
|
-
await page.addStyleTag({ content:
|
|
3350
|
+
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3351
|
+
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3352
|
+
await page.addStyleTag({ content: projectCss2 });
|
|
3003
3353
|
}
|
|
3004
3354
|
const renderTimeMs = performance.now() - startMs;
|
|
3005
3355
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
@@ -3059,8 +3409,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3059
3409
|
}
|
|
3060
3410
|
};
|
|
3061
3411
|
}
|
|
3412
|
+
function buildScenarioMap(opts, scopeData) {
|
|
3413
|
+
if (opts.scenario !== void 0) {
|
|
3414
|
+
if (scopeData === null) {
|
|
3415
|
+
throw new Error(`--scenario "${opts.scenario}" requires a .scope file next to the component`);
|
|
3416
|
+
}
|
|
3417
|
+
const props = scopeData.scenarios[opts.scenario];
|
|
3418
|
+
if (props === void 0) {
|
|
3419
|
+
const available = Object.keys(scopeData.scenarios).join(", ") || "(none)";
|
|
3420
|
+
throw new Error(
|
|
3421
|
+
`Scenario "${opts.scenario}" not found in scope file.
|
|
3422
|
+
Available: ${available}`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
return { [opts.scenario]: props };
|
|
3426
|
+
}
|
|
3427
|
+
if (opts.props !== void 0) {
|
|
3428
|
+
let parsed;
|
|
3429
|
+
try {
|
|
3430
|
+
parsed = JSON.parse(opts.props);
|
|
3431
|
+
} catch {
|
|
3432
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3433
|
+
}
|
|
3434
|
+
return { __default__: parsed };
|
|
3435
|
+
}
|
|
3436
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length > 0) {
|
|
3437
|
+
return scopeData.scenarios;
|
|
3438
|
+
}
|
|
3439
|
+
return { __default__: {} };
|
|
3440
|
+
}
|
|
3062
3441
|
function registerRenderSingle(renderCmd) {
|
|
3063
|
-
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(
|
|
3442
|
+
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(
|
|
3064
3443
|
async (componentName, opts) => {
|
|
3065
3444
|
try {
|
|
3066
3445
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3080,72 +3459,96 @@ Available: ${available}`
|
|
|
3080
3459
|
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3081
3460
|
}
|
|
3082
3461
|
}
|
|
3462
|
+
if (descriptor.props !== void 0) {
|
|
3463
|
+
const propDefs = descriptor.props;
|
|
3464
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
3465
|
+
if (propName in props) continue;
|
|
3466
|
+
if (!propDef.required && propDef.default !== void 0) continue;
|
|
3467
|
+
if (propDef.type === "node" || propDef.type === "string") {
|
|
3468
|
+
props[propName] = propName === "children" ? componentName : propName;
|
|
3469
|
+
} else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
|
|
3470
|
+
props[propName] = propDef.values[0];
|
|
3471
|
+
} else if (propDef.type === "boolean") {
|
|
3472
|
+
props[propName] = false;
|
|
3473
|
+
} else if (propDef.type === "number") {
|
|
3474
|
+
props[propName] = 0;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3083
3478
|
const { width, height } = parseViewport(opts.viewport);
|
|
3084
3479
|
const rootDir = process.cwd();
|
|
3085
3480
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3086
|
-
const
|
|
3481
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3482
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3483
|
+
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3484
|
+
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3485
|
+
const renderer = buildRenderer(
|
|
3486
|
+
filePath,
|
|
3487
|
+
componentName,
|
|
3488
|
+
width,
|
|
3489
|
+
height,
|
|
3490
|
+
globalCssFiles,
|
|
3491
|
+
rootDir,
|
|
3492
|
+
wrapperScript
|
|
3493
|
+
);
|
|
3087
3494
|
process.stderr.write(
|
|
3088
3495
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3089
3496
|
`
|
|
3090
3497
|
);
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3498
|
+
const fmt2 = resolveSingleFormat(opts.format);
|
|
3499
|
+
let anyFailed = false;
|
|
3500
|
+
for (const [scenarioName, props2] of Object.entries(scenarios)) {
|
|
3501
|
+
const isNamed = scenarioName !== "__default__";
|
|
3502
|
+
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3503
|
+
const outcome = await safeRender(
|
|
3504
|
+
() => renderer.renderCell(props2, descriptor.complexityClass),
|
|
3505
|
+
{
|
|
3506
|
+
props: props2,
|
|
3507
|
+
sourceLocation: {
|
|
3508
|
+
file: descriptor.filePath,
|
|
3509
|
+
line: descriptor.loc.start,
|
|
3510
|
+
column: 0
|
|
3511
|
+
}
|
|
3099
3512
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
if (outcome.crashed) {
|
|
3104
|
-
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
3513
|
+
);
|
|
3514
|
+
if (outcome.crashed) {
|
|
3515
|
+
process.stderr.write(`\u2717 ${label} render failed: ${outcome.error.message}
|
|
3105
3516
|
`);
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3517
|
+
const hintList = outcome.error.heuristicFlags.join(", ");
|
|
3518
|
+
if (hintList.length > 0) {
|
|
3519
|
+
process.stderr.write(` Hints: ${hintList}
|
|
3109
3520
|
`);
|
|
3521
|
+
}
|
|
3522
|
+
anyFailed = true;
|
|
3523
|
+
continue;
|
|
3110
3524
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3525
|
+
const result = outcome.result;
|
|
3526
|
+
const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
|
|
3527
|
+
if (opts.output !== void 0 && !isNamed) {
|
|
3528
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
3529
|
+
writeFileSync(outPath, result.screenshot);
|
|
3530
|
+
process.stdout.write(
|
|
3531
|
+
`\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3119
3532
|
`
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
if (fmt2 === "json") {
|
|
3125
|
-
const json = formatRenderJson(componentName, props, result);
|
|
3126
|
-
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3533
|
+
);
|
|
3534
|
+
} else if (fmt2 === "json") {
|
|
3535
|
+
const json = formatRenderJson(label, props2, result);
|
|
3536
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3127
3537
|
`);
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
`
|
|
3137
|
-
);
|
|
3138
|
-
} else {
|
|
3139
|
-
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3140
|
-
mkdirSync(dir, { recursive: true });
|
|
3141
|
-
const outPath = resolve(dir, `${componentName}.png`);
|
|
3142
|
-
writeFileSync(outPath, result.screenshot);
|
|
3143
|
-
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
3144
|
-
process.stdout.write(
|
|
3145
|
-
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3538
|
+
} else {
|
|
3539
|
+
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3540
|
+
mkdirSync(dir, { recursive: true });
|
|
3541
|
+
const outPath = resolve(dir, outFileName);
|
|
3542
|
+
writeFileSync(outPath, result.screenshot);
|
|
3543
|
+
const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
|
|
3544
|
+
process.stdout.write(
|
|
3545
|
+
`\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3146
3546
|
`
|
|
3147
|
-
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3148
3549
|
}
|
|
3550
|
+
await shutdownPool3();
|
|
3551
|
+
if (anyFailed) process.exit(1);
|
|
3149
3552
|
} catch (err) {
|
|
3150
3553
|
await shutdownPool3();
|
|
3151
3554
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3156,7 +3559,10 @@ Available: ${available}`
|
|
|
3156
3559
|
);
|
|
3157
3560
|
}
|
|
3158
3561
|
function registerRenderMatrix(renderCmd) {
|
|
3159
|
-
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3562
|
+
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3563
|
+
"--axes <spec>",
|
|
3564
|
+
`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"]}'`
|
|
3565
|
+
).option(
|
|
3160
3566
|
"--contexts <ids>",
|
|
3161
3567
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
3162
3568
|
).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(
|
|
@@ -3175,21 +3581,47 @@ Available: ${available}`
|
|
|
3175
3581
|
const { width, height } = { width: 375, height: 812 };
|
|
3176
3582
|
const rootDir = process.cwd();
|
|
3177
3583
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3178
|
-
const
|
|
3584
|
+
const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3585
|
+
const renderer = buildRenderer(
|
|
3586
|
+
filePath,
|
|
3587
|
+
componentName,
|
|
3588
|
+
width,
|
|
3589
|
+
height,
|
|
3590
|
+
matrixCssFiles,
|
|
3591
|
+
rootDir
|
|
3592
|
+
);
|
|
3179
3593
|
const axes = [];
|
|
3180
3594
|
if (opts.axes !== void 0) {
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3595
|
+
const axesRaw = opts.axes.trim();
|
|
3596
|
+
if (axesRaw.startsWith("{")) {
|
|
3597
|
+
let parsed;
|
|
3598
|
+
try {
|
|
3599
|
+
parsed = JSON.parse(axesRaw);
|
|
3600
|
+
} catch {
|
|
3601
|
+
throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
|
|
3186
3602
|
}
|
|
3187
|
-
const name
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3603
|
+
for (const [name, vals] of Object.entries(parsed)) {
|
|
3604
|
+
if (!Array.isArray(vals)) {
|
|
3605
|
+
throw new Error(`Axis "${name}" must be an array of values in JSON format`);
|
|
3606
|
+
}
|
|
3607
|
+
axes.push({ name, values: vals.map(String) });
|
|
3608
|
+
}
|
|
3609
|
+
} else {
|
|
3610
|
+
const axisSpecs = axesRaw.split(/\s+/);
|
|
3611
|
+
for (const spec of axisSpecs) {
|
|
3612
|
+
const colonIdx = spec.indexOf(":");
|
|
3613
|
+
if (colonIdx < 0) {
|
|
3614
|
+
throw new Error(
|
|
3615
|
+
`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
const name = spec.slice(0, colonIdx);
|
|
3619
|
+
const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
|
|
3620
|
+
if (name.length === 0 || values.length === 0) {
|
|
3621
|
+
throw new Error(`Invalid axis spec "${spec}"`);
|
|
3622
|
+
}
|
|
3623
|
+
axes.push({ name, values });
|
|
3191
3624
|
}
|
|
3192
|
-
axes.push({ name, values });
|
|
3193
3625
|
}
|
|
3194
3626
|
}
|
|
3195
3627
|
if (opts.contexts !== void 0) {
|
|
@@ -3308,7 +3740,8 @@ function registerRenderAll(renderCmd) {
|
|
|
3308
3740
|
const descriptor = manifest.components[name];
|
|
3309
3741
|
if (descriptor === void 0) return;
|
|
3310
3742
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3311
|
-
const
|
|
3743
|
+
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
3744
|
+
const renderer = buildRenderer(filePath, name, 375, 812, allCssFiles, process.cwd());
|
|
3312
3745
|
const outcome = await safeRender(
|
|
3313
3746
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
3314
3747
|
{
|
|
@@ -3574,12 +4007,12 @@ async function runBaseline(options = {}) {
|
|
|
3574
4007
|
mkdirSync(rendersDir, { recursive: true });
|
|
3575
4008
|
let manifest;
|
|
3576
4009
|
if (manifestPath !== void 0) {
|
|
3577
|
-
const { readFileSync:
|
|
4010
|
+
const { readFileSync: readFileSync13 } = await import('fs');
|
|
3578
4011
|
const absPath = resolve(rootDir, manifestPath);
|
|
3579
4012
|
if (!existsSync(absPath)) {
|
|
3580
4013
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3581
4014
|
}
|
|
3582
|
-
manifest = JSON.parse(
|
|
4015
|
+
manifest = JSON.parse(readFileSync13(absPath, "utf-8"));
|
|
3583
4016
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3584
4017
|
`);
|
|
3585
4018
|
} else {
|
|
@@ -4940,10 +5373,20 @@ function createTokensExportCommand() {
|
|
|
4940
5373
|
).action(
|
|
4941
5374
|
(opts) => {
|
|
4942
5375
|
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
5376
|
+
const FORMAT_ALIASES = {
|
|
5377
|
+
json: "flat-json",
|
|
5378
|
+
"json-flat": "flat-json",
|
|
5379
|
+
javascript: "ts",
|
|
5380
|
+
js: "ts",
|
|
5381
|
+
sass: "scss",
|
|
5382
|
+
tw: "tailwind"
|
|
5383
|
+
};
|
|
5384
|
+
const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
|
|
4943
5385
|
process.stderr.write(
|
|
4944
5386
|
`Error: unsupported format "${opts.format}".
|
|
4945
5387
|
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
4946
|
-
`
|
|
5388
|
+
` + (hint ? `Did you mean "${hint}"?
|
|
5389
|
+
` : "")
|
|
4947
5390
|
);
|
|
4948
5391
|
process.exit(1);
|
|
4949
5392
|
}
|