@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.cjs
CHANGED
|
@@ -6,14 +6,16 @@ var manifest = require('@agent-scope/manifest');
|
|
|
6
6
|
var render = require('@agent-scope/render');
|
|
7
7
|
var tokens = require('@agent-scope/tokens');
|
|
8
8
|
var commander = require('commander');
|
|
9
|
-
var
|
|
9
|
+
var esbuild2 = require('esbuild');
|
|
10
10
|
var module$1 = require('module');
|
|
11
11
|
var readline = require('readline');
|
|
12
12
|
var playwright = require('@agent-scope/playwright');
|
|
13
13
|
var playwright$1 = require('playwright');
|
|
14
|
+
var os = require('os');
|
|
14
15
|
var http = require('http');
|
|
15
16
|
var site = require('@agent-scope/site');
|
|
16
17
|
|
|
18
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
17
19
|
function _interopNamespace(e) {
|
|
18
20
|
if (e && e.__esModule) return e;
|
|
19
21
|
var n = Object.create(null);
|
|
@@ -32,13 +34,13 @@ function _interopNamespace(e) {
|
|
|
32
34
|
return Object.freeze(n);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
var
|
|
37
|
+
var esbuild2__namespace = /*#__PURE__*/_interopNamespace(esbuild2);
|
|
36
38
|
var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
|
|
37
39
|
|
|
38
40
|
// src/ci/commands.ts
|
|
39
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss,
|
|
41
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
|
|
40
42
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
41
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
43
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
|
|
42
44
|
}
|
|
43
45
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
44
46
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -74,7 +76,12 @@ import { createElement } from "react";
|
|
|
74
76
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
75
77
|
return;
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
// If a scope file wrapper was injected, use it to wrap the component
|
|
80
|
+
var Wrapper = (window).__SCOPE_WRAPPER__;
|
|
81
|
+
var element = Wrapper
|
|
82
|
+
? createElement(Wrapper, null, createElement(Component, props))
|
|
83
|
+
: createElement(Component, props);
|
|
84
|
+
createRoot(rootEl).render(element);
|
|
78
85
|
// Use requestAnimationFrame to let React flush the render
|
|
79
86
|
requestAnimationFrame(function() {
|
|
80
87
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
@@ -86,7 +93,7 @@ import { createElement } from "react";
|
|
|
86
93
|
})();
|
|
87
94
|
`
|
|
88
95
|
);
|
|
89
|
-
const result = await
|
|
96
|
+
const result = await esbuild2__namespace.build({
|
|
90
97
|
stdin: {
|
|
91
98
|
contents: wrapperCode,
|
|
92
99
|
// Resolve relative imports (within the component's dir)
|
|
@@ -111,6 +118,21 @@ import { createElement } from "react";
|
|
|
111
118
|
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
112
119
|
banner: {
|
|
113
120
|
js: "/* @agent-scope/cli component harness */"
|
|
121
|
+
},
|
|
122
|
+
// CSS imports (e.g. `import './styles.css'`) are handled at the page level via
|
|
123
|
+
// globalCSS injection. Tell esbuild to treat CSS files as empty modules so
|
|
124
|
+
// components that import CSS directly (e.g. App.tsx) don't error during bundling.
|
|
125
|
+
loader: {
|
|
126
|
+
".css": "empty",
|
|
127
|
+
".svg": "dataurl",
|
|
128
|
+
".png": "dataurl",
|
|
129
|
+
".jpg": "dataurl",
|
|
130
|
+
".jpeg": "dataurl",
|
|
131
|
+
".gif": "dataurl",
|
|
132
|
+
".webp": "dataurl",
|
|
133
|
+
".ttf": "dataurl",
|
|
134
|
+
".woff": "dataurl",
|
|
135
|
+
".woff2": "dataurl"
|
|
114
136
|
}
|
|
115
137
|
});
|
|
116
138
|
if (result.errors.length > 0) {
|
|
@@ -124,12 +146,11 @@ ${msg}`);
|
|
|
124
146
|
}
|
|
125
147
|
return outputFile.text;
|
|
126
148
|
}
|
|
127
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
149
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
|
|
128
150
|
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
129
151
|
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
130
152
|
</style>` : "";
|
|
131
|
-
const
|
|
132
|
-
` : "";
|
|
153
|
+
const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
|
|
133
154
|
return `<!DOCTYPE html>
|
|
134
155
|
<html lang="en">
|
|
135
156
|
<head>
|
|
@@ -144,7 +165,8 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
|
144
165
|
</head>
|
|
145
166
|
<body>
|
|
146
167
|
<div id="scope-root" data-reactscope-root></div>
|
|
147
|
-
${
|
|
168
|
+
${wrapperScriptBlock}
|
|
169
|
+
<script>${bundledScript}</script>
|
|
148
170
|
</body>
|
|
149
171
|
</html>`;
|
|
150
172
|
}
|
|
@@ -507,16 +529,67 @@ async function getTailwindCompiler(cwd) {
|
|
|
507
529
|
from: entryPath,
|
|
508
530
|
loadStylesheet
|
|
509
531
|
});
|
|
510
|
-
const
|
|
511
|
-
compilerCache = { cwd, build:
|
|
512
|
-
return
|
|
532
|
+
const build3 = result.build.bind(result);
|
|
533
|
+
compilerCache = { cwd, build: build3 };
|
|
534
|
+
return build3;
|
|
513
535
|
}
|
|
514
536
|
async function getCompiledCssForClasses(cwd, classes) {
|
|
515
|
-
const
|
|
516
|
-
if (
|
|
537
|
+
const build3 = await getTailwindCompiler(cwd);
|
|
538
|
+
if (build3 === null) return null;
|
|
517
539
|
const deduped = [...new Set(classes)].filter(Boolean);
|
|
518
540
|
if (deduped.length === 0) return null;
|
|
519
|
-
return
|
|
541
|
+
return build3(deduped);
|
|
542
|
+
}
|
|
543
|
+
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
544
|
+
const { existsSync: existsSync15, readFileSync: readFileSync13 } = await import('fs');
|
|
545
|
+
const { createRequire: createRequire3 } = await import('module');
|
|
546
|
+
if (!existsSync15(cssFilePath)) return null;
|
|
547
|
+
const raw = readFileSync13(cssFilePath, "utf-8");
|
|
548
|
+
const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
|
|
549
|
+
if (!needsCompile) {
|
|
550
|
+
return raw;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const require2 = createRequire3(path.resolve(cwd, "package.json"));
|
|
554
|
+
let postcss;
|
|
555
|
+
let twPlugin;
|
|
556
|
+
try {
|
|
557
|
+
postcss = require2("postcss");
|
|
558
|
+
twPlugin = require2("tailwindcss");
|
|
559
|
+
} catch {
|
|
560
|
+
return raw;
|
|
561
|
+
}
|
|
562
|
+
let autoprefixerPlugin;
|
|
563
|
+
try {
|
|
564
|
+
autoprefixerPlugin = require2("autoprefixer");
|
|
565
|
+
} catch {
|
|
566
|
+
autoprefixerPlugin = null;
|
|
567
|
+
}
|
|
568
|
+
const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
|
|
569
|
+
const result = await postcss(plugins).process(raw, {
|
|
570
|
+
from: cssFilePath,
|
|
571
|
+
to: cssFilePath
|
|
572
|
+
});
|
|
573
|
+
return result.css;
|
|
574
|
+
} catch (err) {
|
|
575
|
+
process.stderr.write(
|
|
576
|
+
`[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
|
|
577
|
+
`
|
|
578
|
+
);
|
|
579
|
+
return raw;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function loadGlobalCss(globalCssFiles, cwd) {
|
|
583
|
+
if (globalCssFiles.length === 0) return null;
|
|
584
|
+
const parts = [];
|
|
585
|
+
for (const relPath of globalCssFiles) {
|
|
586
|
+
const absPath = path.resolve(cwd, relPath);
|
|
587
|
+
const css = await compileGlobalCssFile(absPath, cwd);
|
|
588
|
+
if (css !== null && css.trim().length > 0) {
|
|
589
|
+
parts.push(css);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
520
593
|
}
|
|
521
594
|
|
|
522
595
|
// src/ci/commands.ts
|
|
@@ -1021,6 +1094,20 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1021
1094
|
}
|
|
1022
1095
|
return unique;
|
|
1023
1096
|
}
|
|
1097
|
+
var GLOBAL_CSS_CANDIDATES = [
|
|
1098
|
+
"src/styles.css",
|
|
1099
|
+
"src/index.css",
|
|
1100
|
+
"src/global.css",
|
|
1101
|
+
"src/globals.css",
|
|
1102
|
+
"src/app.css",
|
|
1103
|
+
"src/main.css",
|
|
1104
|
+
"styles/globals.css",
|
|
1105
|
+
"styles/global.css",
|
|
1106
|
+
"styles/index.css"
|
|
1107
|
+
];
|
|
1108
|
+
function detectGlobalCSSFiles(rootDir) {
|
|
1109
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(rootDir, rel)));
|
|
1110
|
+
}
|
|
1024
1111
|
var TAILWIND_STEMS = ["tailwind.config"];
|
|
1025
1112
|
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
1026
1113
|
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
@@ -1088,13 +1175,15 @@ function detectProject(rootDir) {
|
|
|
1088
1175
|
const packageManager = detectPackageManager(rootDir);
|
|
1089
1176
|
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
1090
1177
|
const tokenSources = detectTokenSources(rootDir);
|
|
1178
|
+
const globalCSSFiles = detectGlobalCSSFiles(rootDir);
|
|
1091
1179
|
return {
|
|
1092
1180
|
framework,
|
|
1093
1181
|
typescript,
|
|
1094
1182
|
tsconfigPath,
|
|
1095
1183
|
componentPatterns,
|
|
1096
1184
|
tokenSources,
|
|
1097
|
-
packageManager
|
|
1185
|
+
packageManager,
|
|
1186
|
+
globalCSSFiles
|
|
1098
1187
|
};
|
|
1099
1188
|
}
|
|
1100
1189
|
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
@@ -1103,7 +1192,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
|
1103
1192
|
components: {
|
|
1104
1193
|
include,
|
|
1105
1194
|
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
1106
|
-
wrappers: { providers: [], globalCSS: [] }
|
|
1195
|
+
wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
|
|
1107
1196
|
},
|
|
1108
1197
|
render: {
|
|
1109
1198
|
viewport: { default: { width: 1280, height: 800 } },
|
|
@@ -1134,9 +1223,9 @@ function createRL() {
|
|
|
1134
1223
|
});
|
|
1135
1224
|
}
|
|
1136
1225
|
async function ask(rl, question) {
|
|
1137
|
-
return new Promise((
|
|
1226
|
+
return new Promise((resolve18) => {
|
|
1138
1227
|
rl.question(question, (answer) => {
|
|
1139
|
-
|
|
1228
|
+
resolve18(answer.trim());
|
|
1140
1229
|
});
|
|
1141
1230
|
});
|
|
1142
1231
|
}
|
|
@@ -1161,18 +1250,118 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1161
1250
|
`);
|
|
1162
1251
|
}
|
|
1163
1252
|
}
|
|
1253
|
+
function extractTailwindTokens(tokenSources) {
|
|
1254
|
+
const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
|
|
1255
|
+
if (!tailwindSource) return null;
|
|
1256
|
+
try {
|
|
1257
|
+
let parseBlock2 = function(block) {
|
|
1258
|
+
const result = {};
|
|
1259
|
+
const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
|
|
1260
|
+
for (const m of block.matchAll(lineRe)) {
|
|
1261
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
1262
|
+
result[m[1]] = m[2];
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
};
|
|
1267
|
+
var parseBlock = parseBlock2;
|
|
1268
|
+
const raw = fs.readFileSync(tailwindSource.path, "utf-8");
|
|
1269
|
+
const tokens = {};
|
|
1270
|
+
const colorsKeyIdx = raw.indexOf("colors:");
|
|
1271
|
+
if (colorsKeyIdx !== -1) {
|
|
1272
|
+
const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
|
|
1273
|
+
if (colorsBraceStart !== -1) {
|
|
1274
|
+
let colorDepth = 0;
|
|
1275
|
+
let colorsBraceEnd = -1;
|
|
1276
|
+
for (let ci = colorsBraceStart; ci < raw.length; ci++) {
|
|
1277
|
+
if (raw[ci] === "{") colorDepth++;
|
|
1278
|
+
else if (raw[ci] === "}") {
|
|
1279
|
+
colorDepth--;
|
|
1280
|
+
if (colorDepth === 0) {
|
|
1281
|
+
colorsBraceEnd = ci;
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (colorsBraceEnd > colorsBraceStart) {
|
|
1287
|
+
const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
|
|
1288
|
+
const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1289
|
+
const colorTokens = {};
|
|
1290
|
+
for (const sm of colorSection.matchAll(scaleRe)) {
|
|
1291
|
+
if (sm[1] === void 0 || sm[2] === void 0) continue;
|
|
1292
|
+
const scaleName = sm[1];
|
|
1293
|
+
const scaleValues = parseBlock2(sm[2]);
|
|
1294
|
+
if (Object.keys(scaleValues).length > 0) {
|
|
1295
|
+
const scaleTokens = {};
|
|
1296
|
+
for (const [step, hex] of Object.entries(scaleValues)) {
|
|
1297
|
+
scaleTokens[step] = { value: hex, type: "color" };
|
|
1298
|
+
}
|
|
1299
|
+
colorTokens[scaleName] = scaleTokens;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (Object.keys(colorTokens).length > 0) {
|
|
1303
|
+
tokens["color"] = colorTokens;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1309
|
+
if (spacingMatch?.[1] !== void 0) {
|
|
1310
|
+
const spacingValues = parseBlock2(spacingMatch[1]);
|
|
1311
|
+
if (Object.keys(spacingValues).length > 0) {
|
|
1312
|
+
const spacingTokens = {};
|
|
1313
|
+
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1314
|
+
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1315
|
+
}
|
|
1316
|
+
tokens["spacing"] = spacingTokens;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1320
|
+
if (fontFamilyMatch?.[1] !== void 0) {
|
|
1321
|
+
const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
|
|
1322
|
+
const fontTokens = {};
|
|
1323
|
+
for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
|
|
1324
|
+
if (fm[1] !== void 0 && fm[2] !== void 0) {
|
|
1325
|
+
fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (Object.keys(fontTokens).length > 0) {
|
|
1329
|
+
tokens["font"] = fontTokens;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1333
|
+
if (borderRadiusMatch?.[1] !== void 0) {
|
|
1334
|
+
const radiusValues = parseBlock2(borderRadiusMatch[1]);
|
|
1335
|
+
if (Object.keys(radiusValues).length > 0) {
|
|
1336
|
+
const radiusTokens = {};
|
|
1337
|
+
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1338
|
+
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1339
|
+
}
|
|
1340
|
+
tokens["radius"] = radiusTokens;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
1344
|
+
} catch {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1164
1348
|
function scaffoldConfig(rootDir, config) {
|
|
1165
1349
|
const path$1 = path.join(rootDir, "reactscope.config.json");
|
|
1166
1350
|
fs.writeFileSync(path$1, `${JSON.stringify(config, null, 2)}
|
|
1167
1351
|
`);
|
|
1168
1352
|
return path$1;
|
|
1169
1353
|
}
|
|
1170
|
-
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
1354
|
+
function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
|
|
1171
1355
|
const path$1 = path.join(rootDir, tokenFile);
|
|
1172
1356
|
if (!fs.existsSync(path$1)) {
|
|
1173
1357
|
const stub = {
|
|
1174
1358
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
1175
|
-
|
|
1359
|
+
version: "1.0.0",
|
|
1360
|
+
meta: {
|
|
1361
|
+
name: "Design Tokens",
|
|
1362
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1363
|
+
},
|
|
1364
|
+
tokens: extractedTokens ?? {}
|
|
1176
1365
|
};
|
|
1177
1366
|
fs.writeFileSync(path$1, `${JSON.stringify(stub, null, 2)}
|
|
1178
1367
|
`);
|
|
@@ -1250,7 +1439,13 @@ async function runInit(options) {
|
|
|
1250
1439
|
}
|
|
1251
1440
|
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1252
1441
|
created.push(cfgPath);
|
|
1253
|
-
const
|
|
1442
|
+
const extractedTokens = extractTailwindTokens(detected.tokenSources);
|
|
1443
|
+
if (extractedTokens !== null) {
|
|
1444
|
+
const tokenGroupCount = Object.keys(extractedTokens).length;
|
|
1445
|
+
process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
|
|
1446
|
+
`);
|
|
1447
|
+
}
|
|
1448
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
|
|
1254
1449
|
created.push(tokPath);
|
|
1255
1450
|
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1256
1451
|
created.push(outDirPath);
|
|
@@ -1350,7 +1545,10 @@ Available: ${available}${hint}`
|
|
|
1350
1545
|
});
|
|
1351
1546
|
}
|
|
1352
1547
|
function registerQuery(manifestCmd) {
|
|
1353
|
-
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(
|
|
1548
|
+
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(
|
|
1549
|
+
"--has-prop <spec>",
|
|
1550
|
+
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1551
|
+
).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(
|
|
1354
1552
|
(opts) => {
|
|
1355
1553
|
try {
|
|
1356
1554
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1361,9 +1559,11 @@ function registerQuery(manifestCmd) {
|
|
|
1361
1559
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
1362
1560
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
1363
1561
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1562
|
+
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1563
|
+
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1364
1564
|
if (queryParts.length === 0) {
|
|
1365
1565
|
process.stderr.write(
|
|
1366
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects,
|
|
1566
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
|
|
1367
1567
|
);
|
|
1368
1568
|
process.exit(1);
|
|
1369
1569
|
}
|
|
@@ -1390,6 +1590,27 @@ function registerQuery(manifestCmd) {
|
|
|
1390
1590
|
if (opts.hasFetch) {
|
|
1391
1591
|
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1392
1592
|
}
|
|
1593
|
+
if (opts.hasProp !== void 0) {
|
|
1594
|
+
const spec = opts.hasProp;
|
|
1595
|
+
const colonIdx = spec.indexOf(":");
|
|
1596
|
+
const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
|
|
1597
|
+
const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
|
|
1598
|
+
entries = entries.filter(([, d]) => {
|
|
1599
|
+
const props = d.props;
|
|
1600
|
+
if (!props || !(propName in props)) return false;
|
|
1601
|
+
if (propType !== void 0) {
|
|
1602
|
+
return props[propName]?.type === propType;
|
|
1603
|
+
}
|
|
1604
|
+
return true;
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (opts.composedBy !== void 0) {
|
|
1608
|
+
const targetName = opts.composedBy;
|
|
1609
|
+
entries = entries.filter(([, d]) => {
|
|
1610
|
+
const composedBy = d.composedBy;
|
|
1611
|
+
return composedBy !== void 0 && composedBy.includes(targetName);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1393
1614
|
const rows = entries.map(([name, d]) => ({
|
|
1394
1615
|
name,
|
|
1395
1616
|
file: d.filePath,
|
|
@@ -2958,6 +3179,133 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
2958
3179
|
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2959
3180
|
fs.writeFileSync(outputPath, json, "utf-8");
|
|
2960
3181
|
}
|
|
3182
|
+
var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
|
|
3183
|
+
function findScopeFile(componentFilePath) {
|
|
3184
|
+
const dir = path.dirname(componentFilePath);
|
|
3185
|
+
const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
3186
|
+
const baseName = stem.slice(dir.length + 1);
|
|
3187
|
+
for (const ext of SCOPE_EXTENSIONS) {
|
|
3188
|
+
const candidate = path.join(dir, `${baseName}${ext}`);
|
|
3189
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
3190
|
+
}
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
async function loadScopeFile(scopeFilePath) {
|
|
3194
|
+
const tmpDir = path.join(os.tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3195
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3196
|
+
const outFile = path.join(tmpDir, "scope-file.cjs");
|
|
3197
|
+
try {
|
|
3198
|
+
const result = await esbuild2__namespace.build({
|
|
3199
|
+
entryPoints: [scopeFilePath],
|
|
3200
|
+
bundle: true,
|
|
3201
|
+
format: "cjs",
|
|
3202
|
+
platform: "node",
|
|
3203
|
+
target: "node18",
|
|
3204
|
+
outfile: outFile,
|
|
3205
|
+
write: true,
|
|
3206
|
+
jsx: "automatic",
|
|
3207
|
+
jsxImportSource: "react",
|
|
3208
|
+
// Externalize React — we don't need to execute JSX, just extract plain data
|
|
3209
|
+
external: ["react", "react-dom", "react/jsx-runtime"],
|
|
3210
|
+
define: {
|
|
3211
|
+
"process.env.NODE_ENV": '"development"'
|
|
3212
|
+
},
|
|
3213
|
+
logLevel: "silent"
|
|
3214
|
+
});
|
|
3215
|
+
if (result.errors.length > 0) {
|
|
3216
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3217
|
+
throw new Error(`Failed to bundle scope file ${scopeFilePath}:
|
|
3218
|
+
${msg}`);
|
|
3219
|
+
}
|
|
3220
|
+
const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
3221
|
+
delete req.cache[path.resolve(outFile)];
|
|
3222
|
+
const mod = req(outFile);
|
|
3223
|
+
const scenarios = extractScenarios(mod, scopeFilePath);
|
|
3224
|
+
const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
|
|
3225
|
+
return { filePath: scopeFilePath, scenarios, hasWrapper };
|
|
3226
|
+
} finally {
|
|
3227
|
+
try {
|
|
3228
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3229
|
+
} catch {
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
async function loadScopeFileForComponent(componentFilePath) {
|
|
3234
|
+
const scopeFilePath = findScopeFile(componentFilePath);
|
|
3235
|
+
if (scopeFilePath === null) return null;
|
|
3236
|
+
return loadScopeFile(scopeFilePath);
|
|
3237
|
+
}
|
|
3238
|
+
function extractScenarios(mod, filePath) {
|
|
3239
|
+
const raw = mod.scenarios ?? mod.default?.scenarios;
|
|
3240
|
+
if (raw === void 0) return {};
|
|
3241
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3242
|
+
console.warn(`[scope] ${filePath}: "scenarios" export is not a plain object \u2014 ignoring.`);
|
|
3243
|
+
return {};
|
|
3244
|
+
}
|
|
3245
|
+
const result = {};
|
|
3246
|
+
for (const [name, props] of Object.entries(raw)) {
|
|
3247
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) {
|
|
3248
|
+
console.warn(`[scope] ${filePath}: scenario "${name}" is not a plain object \u2014 skipping.`);
|
|
3249
|
+
continue;
|
|
3250
|
+
}
|
|
3251
|
+
result[name] = props;
|
|
3252
|
+
}
|
|
3253
|
+
return result;
|
|
3254
|
+
}
|
|
3255
|
+
async function buildWrapperScript(scopeFilePath) {
|
|
3256
|
+
const wrapperEntry = (
|
|
3257
|
+
/* ts */
|
|
3258
|
+
`
|
|
3259
|
+
import * as __scopeMod from ${JSON.stringify(scopeFilePath)};
|
|
3260
|
+
// Expose the wrapper on window so the harness can access it
|
|
3261
|
+
var wrapper =
|
|
3262
|
+
__scopeMod.wrapper ??
|
|
3263
|
+
(__scopeMod.default && __scopeMod.default.wrapper) ??
|
|
3264
|
+
null;
|
|
3265
|
+
window.__SCOPE_WRAPPER__ = wrapper;
|
|
3266
|
+
`
|
|
3267
|
+
);
|
|
3268
|
+
const result = await esbuild2__namespace.build({
|
|
3269
|
+
stdin: {
|
|
3270
|
+
contents: wrapperEntry,
|
|
3271
|
+
resolveDir: path.dirname(scopeFilePath),
|
|
3272
|
+
loader: "tsx",
|
|
3273
|
+
sourcefile: "__scope_wrapper_entry__.tsx"
|
|
3274
|
+
},
|
|
3275
|
+
bundle: true,
|
|
3276
|
+
format: "iife",
|
|
3277
|
+
platform: "browser",
|
|
3278
|
+
target: "es2020",
|
|
3279
|
+
write: false,
|
|
3280
|
+
jsx: "automatic",
|
|
3281
|
+
jsxImportSource: "react",
|
|
3282
|
+
external: [],
|
|
3283
|
+
define: {
|
|
3284
|
+
"process.env.NODE_ENV": '"development"',
|
|
3285
|
+
global: "globalThis"
|
|
3286
|
+
},
|
|
3287
|
+
logLevel: "silent"
|
|
3288
|
+
});
|
|
3289
|
+
if (result.errors.length > 0) {
|
|
3290
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3291
|
+
throw new Error(`Failed to build wrapper script from ${scopeFilePath}:
|
|
3292
|
+
${msg}`);
|
|
3293
|
+
}
|
|
3294
|
+
return result.outputFiles?.[0]?.text ?? "";
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/render-commands.ts
|
|
3298
|
+
function loadGlobalCssFilesFromConfig(cwd) {
|
|
3299
|
+
const configPath = path.resolve(cwd, "reactscope.config.json");
|
|
3300
|
+
if (!fs.existsSync(configPath)) return [];
|
|
3301
|
+
try {
|
|
3302
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
3303
|
+
const cfg = JSON.parse(raw);
|
|
3304
|
+
return cfg.components?.wrappers?.globalCSS ?? [];
|
|
3305
|
+
} catch {
|
|
3306
|
+
return [];
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
2961
3309
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2962
3310
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2963
3311
|
var _pool3 = null;
|
|
@@ -2978,7 +3326,7 @@ async function shutdownPool3() {
|
|
|
2978
3326
|
_pool3 = null;
|
|
2979
3327
|
}
|
|
2980
3328
|
}
|
|
2981
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
3329
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
|
|
2982
3330
|
const satori = new render.SatoriRenderer({
|
|
2983
3331
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
2984
3332
|
});
|
|
@@ -2987,11 +3335,14 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2987
3335
|
async renderCell(props, _complexityClass) {
|
|
2988
3336
|
const startMs = performance.now();
|
|
2989
3337
|
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
3338
|
+
const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
|
|
2990
3339
|
const htmlHarness = await buildComponentHarness(
|
|
2991
3340
|
filePath,
|
|
2992
3341
|
componentName,
|
|
2993
3342
|
props,
|
|
2994
|
-
viewportWidth
|
|
3343
|
+
viewportWidth,
|
|
3344
|
+
projectCss ?? void 0,
|
|
3345
|
+
wrapperScript
|
|
2995
3346
|
);
|
|
2996
3347
|
const slot = await pool.acquire();
|
|
2997
3348
|
const { page } = slot;
|
|
@@ -3020,9 +3371,9 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3020
3371
|
});
|
|
3021
3372
|
return [...set];
|
|
3022
3373
|
});
|
|
3023
|
-
const
|
|
3024
|
-
if (
|
|
3025
|
-
await page.addStyleTag({ content:
|
|
3374
|
+
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3375
|
+
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3376
|
+
await page.addStyleTag({ content: projectCss2 });
|
|
3026
3377
|
}
|
|
3027
3378
|
const renderTimeMs = performance.now() - startMs;
|
|
3028
3379
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
@@ -3082,8 +3433,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3082
3433
|
}
|
|
3083
3434
|
};
|
|
3084
3435
|
}
|
|
3436
|
+
function buildScenarioMap(opts, scopeData) {
|
|
3437
|
+
if (opts.scenario !== void 0) {
|
|
3438
|
+
if (scopeData === null) {
|
|
3439
|
+
throw new Error(`--scenario "${opts.scenario}" requires a .scope file next to the component`);
|
|
3440
|
+
}
|
|
3441
|
+
const props = scopeData.scenarios[opts.scenario];
|
|
3442
|
+
if (props === void 0) {
|
|
3443
|
+
const available = Object.keys(scopeData.scenarios).join(", ") || "(none)";
|
|
3444
|
+
throw new Error(
|
|
3445
|
+
`Scenario "${opts.scenario}" not found in scope file.
|
|
3446
|
+
Available: ${available}`
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
return { [opts.scenario]: props };
|
|
3450
|
+
}
|
|
3451
|
+
if (opts.props !== void 0) {
|
|
3452
|
+
let parsed;
|
|
3453
|
+
try {
|
|
3454
|
+
parsed = JSON.parse(opts.props);
|
|
3455
|
+
} catch {
|
|
3456
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3457
|
+
}
|
|
3458
|
+
return { __default__: parsed };
|
|
3459
|
+
}
|
|
3460
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length > 0) {
|
|
3461
|
+
return scopeData.scenarios;
|
|
3462
|
+
}
|
|
3463
|
+
return { __default__: {} };
|
|
3464
|
+
}
|
|
3085
3465
|
function registerRenderSingle(renderCmd) {
|
|
3086
|
-
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(
|
|
3466
|
+
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(
|
|
3087
3467
|
async (componentName, opts) => {
|
|
3088
3468
|
try {
|
|
3089
3469
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3103,72 +3483,96 @@ Available: ${available}`
|
|
|
3103
3483
|
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3104
3484
|
}
|
|
3105
3485
|
}
|
|
3486
|
+
if (descriptor.props !== void 0) {
|
|
3487
|
+
const propDefs = descriptor.props;
|
|
3488
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
3489
|
+
if (propName in props) continue;
|
|
3490
|
+
if (!propDef.required && propDef.default !== void 0) continue;
|
|
3491
|
+
if (propDef.type === "node" || propDef.type === "string") {
|
|
3492
|
+
props[propName] = propName === "children" ? componentName : propName;
|
|
3493
|
+
} else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
|
|
3494
|
+
props[propName] = propDef.values[0];
|
|
3495
|
+
} else if (propDef.type === "boolean") {
|
|
3496
|
+
props[propName] = false;
|
|
3497
|
+
} else if (propDef.type === "number") {
|
|
3498
|
+
props[propName] = 0;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3106
3502
|
const { width, height } = parseViewport(opts.viewport);
|
|
3107
3503
|
const rootDir = process.cwd();
|
|
3108
3504
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3109
|
-
const
|
|
3505
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3506
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3507
|
+
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3508
|
+
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3509
|
+
const renderer = buildRenderer(
|
|
3510
|
+
filePath,
|
|
3511
|
+
componentName,
|
|
3512
|
+
width,
|
|
3513
|
+
height,
|
|
3514
|
+
globalCssFiles,
|
|
3515
|
+
rootDir,
|
|
3516
|
+
wrapperScript
|
|
3517
|
+
);
|
|
3110
3518
|
process.stderr.write(
|
|
3111
3519
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3112
3520
|
`
|
|
3113
3521
|
);
|
|
3114
|
-
const
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3522
|
+
const fmt2 = resolveSingleFormat(opts.format);
|
|
3523
|
+
let anyFailed = false;
|
|
3524
|
+
for (const [scenarioName, props2] of Object.entries(scenarios)) {
|
|
3525
|
+
const isNamed = scenarioName !== "__default__";
|
|
3526
|
+
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3527
|
+
const outcome = await render.safeRender(
|
|
3528
|
+
() => renderer.renderCell(props2, descriptor.complexityClass),
|
|
3529
|
+
{
|
|
3530
|
+
props: props2,
|
|
3531
|
+
sourceLocation: {
|
|
3532
|
+
file: descriptor.filePath,
|
|
3533
|
+
line: descriptor.loc.start,
|
|
3534
|
+
column: 0
|
|
3535
|
+
}
|
|
3122
3536
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
if (outcome.crashed) {
|
|
3127
|
-
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
3537
|
+
);
|
|
3538
|
+
if (outcome.crashed) {
|
|
3539
|
+
process.stderr.write(`\u2717 ${label} render failed: ${outcome.error.message}
|
|
3128
3540
|
`);
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3541
|
+
const hintList = outcome.error.heuristicFlags.join(", ");
|
|
3542
|
+
if (hintList.length > 0) {
|
|
3543
|
+
process.stderr.write(` Hints: ${hintList}
|
|
3132
3544
|
`);
|
|
3545
|
+
}
|
|
3546
|
+
anyFailed = true;
|
|
3547
|
+
continue;
|
|
3133
3548
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3549
|
+
const result = outcome.result;
|
|
3550
|
+
const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
|
|
3551
|
+
if (opts.output !== void 0 && !isNamed) {
|
|
3552
|
+
const outPath = path.resolve(process.cwd(), opts.output);
|
|
3553
|
+
fs.writeFileSync(outPath, result.screenshot);
|
|
3554
|
+
process.stdout.write(
|
|
3555
|
+
`\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3142
3556
|
`
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
if (fmt2 === "json") {
|
|
3148
|
-
const json = formatRenderJson(componentName, props, result);
|
|
3149
|
-
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3557
|
+
);
|
|
3558
|
+
} else if (fmt2 === "json") {
|
|
3559
|
+
const json = formatRenderJson(label, props2, result);
|
|
3560
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3150
3561
|
`);
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
`
|
|
3160
|
-
);
|
|
3161
|
-
} else {
|
|
3162
|
-
const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3163
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
3164
|
-
const outPath = path.resolve(dir, `${componentName}.png`);
|
|
3165
|
-
fs.writeFileSync(outPath, result.screenshot);
|
|
3166
|
-
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
3167
|
-
process.stdout.write(
|
|
3168
|
-
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3562
|
+
} else {
|
|
3563
|
+
const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3564
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3565
|
+
const outPath = path.resolve(dir, outFileName);
|
|
3566
|
+
fs.writeFileSync(outPath, result.screenshot);
|
|
3567
|
+
const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
|
|
3568
|
+
process.stdout.write(
|
|
3569
|
+
`\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3169
3570
|
`
|
|
3170
|
-
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3171
3573
|
}
|
|
3574
|
+
await shutdownPool3();
|
|
3575
|
+
if (anyFailed) process.exit(1);
|
|
3172
3576
|
} catch (err) {
|
|
3173
3577
|
await shutdownPool3();
|
|
3174
3578
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3179,7 +3583,10 @@ Available: ${available}`
|
|
|
3179
3583
|
);
|
|
3180
3584
|
}
|
|
3181
3585
|
function registerRenderMatrix(renderCmd) {
|
|
3182
|
-
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3586
|
+
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3587
|
+
"--axes <spec>",
|
|
3588
|
+
`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"]}'`
|
|
3589
|
+
).option(
|
|
3183
3590
|
"--contexts <ids>",
|
|
3184
3591
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
3185
3592
|
).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(
|
|
@@ -3198,21 +3605,47 @@ Available: ${available}`
|
|
|
3198
3605
|
const { width, height } = { width: 375, height: 812 };
|
|
3199
3606
|
const rootDir = process.cwd();
|
|
3200
3607
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3201
|
-
const
|
|
3608
|
+
const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3609
|
+
const renderer = buildRenderer(
|
|
3610
|
+
filePath,
|
|
3611
|
+
componentName,
|
|
3612
|
+
width,
|
|
3613
|
+
height,
|
|
3614
|
+
matrixCssFiles,
|
|
3615
|
+
rootDir
|
|
3616
|
+
);
|
|
3202
3617
|
const axes = [];
|
|
3203
3618
|
if (opts.axes !== void 0) {
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3619
|
+
const axesRaw = opts.axes.trim();
|
|
3620
|
+
if (axesRaw.startsWith("{")) {
|
|
3621
|
+
let parsed;
|
|
3622
|
+
try {
|
|
3623
|
+
parsed = JSON.parse(axesRaw);
|
|
3624
|
+
} catch {
|
|
3625
|
+
throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
|
|
3209
3626
|
}
|
|
3210
|
-
const name
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3627
|
+
for (const [name, vals] of Object.entries(parsed)) {
|
|
3628
|
+
if (!Array.isArray(vals)) {
|
|
3629
|
+
throw new Error(`Axis "${name}" must be an array of values in JSON format`);
|
|
3630
|
+
}
|
|
3631
|
+
axes.push({ name, values: vals.map(String) });
|
|
3632
|
+
}
|
|
3633
|
+
} else {
|
|
3634
|
+
const axisSpecs = axesRaw.split(/\s+/);
|
|
3635
|
+
for (const spec of axisSpecs) {
|
|
3636
|
+
const colonIdx = spec.indexOf(":");
|
|
3637
|
+
if (colonIdx < 0) {
|
|
3638
|
+
throw new Error(
|
|
3639
|
+
`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
const name = spec.slice(0, colonIdx);
|
|
3643
|
+
const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
|
|
3644
|
+
if (name.length === 0 || values.length === 0) {
|
|
3645
|
+
throw new Error(`Invalid axis spec "${spec}"`);
|
|
3646
|
+
}
|
|
3647
|
+
axes.push({ name, values });
|
|
3214
3648
|
}
|
|
3215
|
-
axes.push({ name, values });
|
|
3216
3649
|
}
|
|
3217
3650
|
}
|
|
3218
3651
|
if (opts.contexts !== void 0) {
|
|
@@ -3331,7 +3764,8 @@ function registerRenderAll(renderCmd) {
|
|
|
3331
3764
|
const descriptor = manifest.components[name];
|
|
3332
3765
|
if (descriptor === void 0) return;
|
|
3333
3766
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3334
|
-
const
|
|
3767
|
+
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
3768
|
+
const renderer = buildRenderer(filePath, name, 375, 812, allCssFiles, process.cwd());
|
|
3335
3769
|
const outcome = await render.safeRender(
|
|
3336
3770
|
() => renderer.renderCell({}, descriptor.complexityClass),
|
|
3337
3771
|
{
|
|
@@ -3597,12 +4031,12 @@ async function runBaseline(options = {}) {
|
|
|
3597
4031
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3598
4032
|
let manifest$1;
|
|
3599
4033
|
if (manifestPath !== void 0) {
|
|
3600
|
-
const { readFileSync:
|
|
4034
|
+
const { readFileSync: readFileSync13 } = await import('fs');
|
|
3601
4035
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3602
4036
|
if (!fs.existsSync(absPath)) {
|
|
3603
4037
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3604
4038
|
}
|
|
3605
|
-
manifest$1 = JSON.parse(
|
|
4039
|
+
manifest$1 = JSON.parse(readFileSync13(absPath, "utf-8"));
|
|
3606
4040
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3607
4041
|
`);
|
|
3608
4042
|
} else {
|
|
@@ -4963,10 +5397,20 @@ function createTokensExportCommand() {
|
|
|
4963
5397
|
).action(
|
|
4964
5398
|
(opts) => {
|
|
4965
5399
|
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
5400
|
+
const FORMAT_ALIASES = {
|
|
5401
|
+
json: "flat-json",
|
|
5402
|
+
"json-flat": "flat-json",
|
|
5403
|
+
javascript: "ts",
|
|
5404
|
+
js: "ts",
|
|
5405
|
+
sass: "scss",
|
|
5406
|
+
tw: "tailwind"
|
|
5407
|
+
};
|
|
5408
|
+
const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
|
|
4966
5409
|
process.stderr.write(
|
|
4967
5410
|
`Error: unsupported format "${opts.format}".
|
|
4968
5411
|
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
4969
|
-
`
|
|
5412
|
+
` + (hint ? `Did you mean "${hint}"?
|
|
5413
|
+
` : "")
|
|
4970
5414
|
);
|
|
4971
5415
|
process.exit(1);
|
|
4972
5416
|
}
|