@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/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 esbuild from 'esbuild';
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, preScript) {
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, preScript);
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
- createRoot(rootEl).render(createElement(Component, props));
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 esbuild.build({
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, preScript) {
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 preScriptBlock = preScript != null && preScript.length > 0 ? `<script>${preScript}</script>
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
- ${preScriptBlock}<script>${bundledScript}</script>
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 build2 = result.build.bind(result);
488
- compilerCache = { cwd, build: build2 };
489
- return build2;
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 build2 = await getTailwindCompiler(cwd);
493
- if (build2 === null) return null;
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 build2(deduped);
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((resolve17) => {
1202
+ return new Promise((resolve18) => {
1115
1203
  rl.question(question, (answer) => {
1116
- resolve17(answer.trim());
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
- tokens: {}
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 tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
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("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
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, or --has-fetch.\n"
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 projectCss = await getCompiledCssForClasses(rootDir, classes);
3001
- if (projectCss != null && projectCss.length > 0) {
3002
- await page.addStyleTag({ content: projectCss });
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 renderer = buildRenderer(filePath, componentName, width, height);
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 outcome = await safeRender(
3092
- () => renderer.renderCell(props, descriptor.complexityClass),
3093
- {
3094
- props,
3095
- sourceLocation: {
3096
- file: descriptor.filePath,
3097
- line: descriptor.loc.start,
3098
- column: 0
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
- await shutdownPool3();
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
- const hintList = outcome.error.heuristicFlags.join(", ");
3107
- if (hintList.length > 0) {
3108
- process.stderr.write(` Hints: ${hintList}
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
- process.exit(1);
3112
- }
3113
- const result = outcome.result;
3114
- if (opts.output !== void 0) {
3115
- const outPath = resolve(process.cwd(), opts.output);
3116
- writeFileSync(outPath, result.screenshot);
3117
- process.stdout.write(
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
- return;
3122
- }
3123
- const fmt2 = resolveSingleFormat(opts.format);
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
- } else if (fmt2 === "file") {
3129
- const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3130
- mkdirSync(dir, { recursive: true });
3131
- const outPath = resolve(dir, `${componentName}.png`);
3132
- writeFileSync(outPath, result.screenshot);
3133
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3134
- process.stdout.write(
3135
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
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("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").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 renderer = buildRenderer(filePath, componentName, width, height);
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 axisSpecs = opts.axes.trim().split(/\s+/);
3182
- for (const spec of axisSpecs) {
3183
- const colonIdx = spec.indexOf(":");
3184
- if (colonIdx < 0) {
3185
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
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 = spec.slice(0, colonIdx);
3188
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3189
- if (name.length === 0 || values.length === 0) {
3190
- throw new Error(`Invalid axis spec "${spec}"`);
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 renderer = buildRenderer(filePath, name, 375, 812);
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: readFileSync12 } = await import('fs');
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(readFileSync12(absPath, "utf-8"));
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
  }