@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.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 esbuild = require('esbuild');
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 esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild);
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, preScript) {
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, preScript);
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
- createRoot(rootEl).render(createElement(Component, props));
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 esbuild__namespace.build({
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, preScript) {
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 preScriptBlock = preScript != null && preScript.length > 0 ? `<script>${preScript}</script>
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
- ${preScriptBlock}<script>${bundledScript}</script>
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 build2 = result.build.bind(result);
511
- compilerCache = { cwd, build: build2 };
512
- return build2;
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 build2 = await getTailwindCompiler(cwd);
516
- if (build2 === null) return null;
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 build2(deduped);
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((resolve17) => {
1226
+ return new Promise((resolve18) => {
1138
1227
  rl.question(question, (answer) => {
1139
- resolve17(answer.trim());
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
- tokens: {}
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 tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
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("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
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, or --has-fetch.\n"
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 projectCss = await getCompiledCssForClasses(rootDir, classes);
3024
- if (projectCss != null && projectCss.length > 0) {
3025
- await page.addStyleTag({ content: projectCss });
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 renderer = buildRenderer(filePath, componentName, width, height);
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 outcome = await render.safeRender(
3115
- () => renderer.renderCell(props, descriptor.complexityClass),
3116
- {
3117
- props,
3118
- sourceLocation: {
3119
- file: descriptor.filePath,
3120
- line: descriptor.loc.start,
3121
- column: 0
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
- await shutdownPool3();
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
- const hintList = outcome.error.heuristicFlags.join(", ");
3130
- if (hintList.length > 0) {
3131
- process.stderr.write(` Hints: ${hintList}
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
- process.exit(1);
3135
- }
3136
- const result = outcome.result;
3137
- if (opts.output !== void 0) {
3138
- const outPath = path.resolve(process.cwd(), opts.output);
3139
- fs.writeFileSync(outPath, result.screenshot);
3140
- process.stdout.write(
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
- return;
3145
- }
3146
- const fmt2 = resolveSingleFormat(opts.format);
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
- } else if (fmt2 === "file") {
3152
- const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3153
- fs.mkdirSync(dir, { recursive: true });
3154
- const outPath = path.resolve(dir, `${componentName}.png`);
3155
- fs.writeFileSync(outPath, result.screenshot);
3156
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3157
- process.stdout.write(
3158
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
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("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").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 renderer = buildRenderer(filePath, componentName, width, height);
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 axisSpecs = opts.axes.trim().split(/\s+/);
3205
- for (const spec of axisSpecs) {
3206
- const colonIdx = spec.indexOf(":");
3207
- if (colonIdx < 0) {
3208
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
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 = spec.slice(0, colonIdx);
3211
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3212
- if (name.length === 0 || values.length === 0) {
3213
- throw new Error(`Invalid axis spec "${spec}"`);
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 renderer = buildRenderer(filePath, name, 375, 812);
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: readFileSync12 } = await import('fs');
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(readFileSync12(absPath, "utf-8"));
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
  }