@agent-scope/cli 1.18.0 → 1.19.0

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
@@ -1,4 +1,4 @@
1
- import { existsSync, writeFileSync, readFileSync, mkdirSync, appendFileSync, readdirSync, rmSync, statSync, createReadStream } from 'fs';
1
+ import { existsSync, writeFileSync, mkdirSync, readFileSync, statSync, appendFileSync, readdirSync, rmSync, createReadStream } from 'fs';
2
2
  import { resolve, join, extname, dirname } from 'path';
3
3
  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';
@@ -94,6 +94,21 @@ import { createElement } from "react";
94
94
  // Suppress "React must be in scope" warnings from old JSX (we use automatic)
95
95
  banner: {
96
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"
97
112
  }
98
113
  });
99
114
  if (result.errors.length > 0) {
@@ -501,6 +516,57 @@ async function getCompiledCssForClasses(cwd, classes) {
501
516
  if (deduped.length === 0) return null;
502
517
  return build3(deduped);
503
518
  }
519
+ async function compileGlobalCssFile(cssFilePath, cwd) {
520
+ const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import('fs');
521
+ const { createRequire: createRequire3 } = await import('module');
522
+ if (!existsSync16(cssFilePath)) return null;
523
+ const raw = readFileSync14(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;
569
+ }
504
570
 
505
571
  // src/ci/commands.ts
506
572
  var CI_EXIT = {
@@ -923,6 +989,157 @@ function createCiCommand() {
923
989
  }
924
990
  );
925
991
  }
992
+ function collectSourceFiles(dir) {
993
+ if (!existsSync(dir)) return [];
994
+ const results = [];
995
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
996
+ const full = join(dir, entry.name);
997
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
998
+ results.push(...collectSourceFiles(full));
999
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
1000
+ results.push(full);
1001
+ }
1002
+ }
1003
+ return results;
1004
+ }
1005
+ function checkConfig(cwd) {
1006
+ const configPath = resolve(cwd, "reactscope.config.json");
1007
+ if (!existsSync(configPath)) {
1008
+ return {
1009
+ name: "config",
1010
+ status: "error",
1011
+ message: "reactscope.config.json not found \u2014 run `scope init`"
1012
+ };
1013
+ }
1014
+ try {
1015
+ JSON.parse(readFileSync(configPath, "utf-8"));
1016
+ return { name: "config", status: "ok", message: "reactscope.config.json valid" };
1017
+ } catch {
1018
+ return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
1019
+ }
1020
+ }
1021
+ function checkTokens(cwd) {
1022
+ const configPath = resolve(cwd, "reactscope.config.json");
1023
+ let tokensPath = resolve(cwd, "reactscope.tokens.json");
1024
+ if (existsSync(configPath)) {
1025
+ try {
1026
+ const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
1027
+ if (cfg.tokens?.file) tokensPath = resolve(cwd, cfg.tokens.file);
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ if (!existsSync(tokensPath)) {
1032
+ return {
1033
+ name: "tokens",
1034
+ status: "warn",
1035
+ message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
1036
+ };
1037
+ }
1038
+ try {
1039
+ const raw = JSON.parse(readFileSync(tokensPath, "utf-8"));
1040
+ if (!raw.version) {
1041
+ return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
1042
+ }
1043
+ return { name: "tokens", status: "ok", message: "Token file valid" };
1044
+ } catch {
1045
+ return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
1046
+ }
1047
+ }
1048
+ function checkGlobalCss(cwd) {
1049
+ const configPath = resolve(cwd, "reactscope.config.json");
1050
+ let globalCss = [];
1051
+ if (existsSync(configPath)) {
1052
+ try {
1053
+ const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
1054
+ globalCss = cfg.components?.wrappers?.globalCSS ?? [];
1055
+ } catch {
1056
+ }
1057
+ }
1058
+ if (globalCss.length === 0) {
1059
+ return {
1060
+ name: "globalCSS",
1061
+ status: "warn",
1062
+ message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
1063
+ };
1064
+ }
1065
+ const missing = globalCss.filter((f) => !existsSync(resolve(cwd, f)));
1066
+ if (missing.length > 0) {
1067
+ return {
1068
+ name: "globalCSS",
1069
+ status: "error",
1070
+ message: `globalCSS file(s) not found: ${missing.join(", ")}`
1071
+ };
1072
+ }
1073
+ return {
1074
+ name: "globalCSS",
1075
+ status: "ok",
1076
+ message: `${globalCss.length} globalCSS file(s) present`
1077
+ };
1078
+ }
1079
+ function checkManifest(cwd) {
1080
+ const manifestPath = resolve(cwd, ".reactscope", "manifest.json");
1081
+ if (!existsSync(manifestPath)) {
1082
+ return {
1083
+ name: "manifest",
1084
+ status: "warn",
1085
+ message: "Manifest not found \u2014 run `scope manifest generate`"
1086
+ };
1087
+ }
1088
+ const manifestMtime = statSync(manifestPath).mtimeMs;
1089
+ const sourceDir = resolve(cwd, "src");
1090
+ const sourceFiles = collectSourceFiles(sourceDir);
1091
+ const stale = sourceFiles.filter((f) => statSync(f).mtimeMs > manifestMtime);
1092
+ if (stale.length > 0) {
1093
+ return {
1094
+ name: "manifest",
1095
+ status: "warn",
1096
+ message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
1097
+ };
1098
+ }
1099
+ return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
1100
+ }
1101
+ var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
1102
+ function formatCheck(check) {
1103
+ return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
1104
+ }
1105
+ function createDoctorCommand() {
1106
+ return new Command("doctor").description("Check the health of your Scope setup (config, tokens, CSS, manifest)").option("--json", "Emit structured JSON output", false).action((opts) => {
1107
+ const cwd = process.cwd();
1108
+ const checks = [
1109
+ checkConfig(cwd),
1110
+ checkTokens(cwd),
1111
+ checkGlobalCss(cwd),
1112
+ checkManifest(cwd)
1113
+ ];
1114
+ const errors = checks.filter((c) => c.status === "error").length;
1115
+ const warnings = checks.filter((c) => c.status === "warn").length;
1116
+ if (opts.json) {
1117
+ process.stdout.write(
1118
+ `${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
1119
+ `
1120
+ );
1121
+ if (errors > 0) process.exit(1);
1122
+ return;
1123
+ }
1124
+ process.stdout.write("\nScope Doctor\n");
1125
+ process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1126
+ for (const check of checks) process.stdout.write(`${formatCheck(check)}
1127
+ `);
1128
+ process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1129
+ if (errors > 0) {
1130
+ process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
1131
+
1132
+ `);
1133
+ process.exit(1);
1134
+ } else if (warnings > 0) {
1135
+ process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
1136
+
1137
+ `);
1138
+ } else {
1139
+ process.stdout.write(" All checks passed!\n\n");
1140
+ }
1141
+ });
1142
+ }
926
1143
  function hasConfigFile(dir, stem) {
927
1144
  if (!existsSync(dir)) return false;
928
1145
  try {
@@ -1004,6 +1221,20 @@ function detectComponentPatterns(rootDir, typescript) {
1004
1221
  }
1005
1222
  return unique;
1006
1223
  }
1224
+ var GLOBAL_CSS_CANDIDATES = [
1225
+ "src/styles.css",
1226
+ "src/index.css",
1227
+ "src/global.css",
1228
+ "src/globals.css",
1229
+ "src/app.css",
1230
+ "src/main.css",
1231
+ "styles/globals.css",
1232
+ "styles/global.css",
1233
+ "styles/index.css"
1234
+ ];
1235
+ function detectGlobalCSSFiles(rootDir) {
1236
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync(join(rootDir, rel)));
1237
+ }
1007
1238
  var TAILWIND_STEMS = ["tailwind.config"];
1008
1239
  var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
1009
1240
  var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
@@ -1071,13 +1302,15 @@ function detectProject(rootDir) {
1071
1302
  const packageManager = detectPackageManager(rootDir);
1072
1303
  const componentPatterns = detectComponentPatterns(rootDir, typescript);
1073
1304
  const tokenSources = detectTokenSources(rootDir);
1305
+ const globalCSSFiles = detectGlobalCSSFiles(rootDir);
1074
1306
  return {
1075
1307
  framework,
1076
1308
  typescript,
1077
1309
  tsconfigPath,
1078
1310
  componentPatterns,
1079
1311
  tokenSources,
1080
- packageManager
1312
+ packageManager,
1313
+ globalCSSFiles
1081
1314
  };
1082
1315
  }
1083
1316
  function buildDefaultConfig(detected, tokenFile, outputDir) {
@@ -1086,7 +1319,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
1086
1319
  components: {
1087
1320
  include,
1088
1321
  exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1089
- wrappers: { providers: [], globalCSS: [] }
1322
+ wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
1090
1323
  },
1091
1324
  render: {
1092
1325
  viewport: { default: { width: 1280, height: 800 } },
@@ -1117,9 +1350,9 @@ function createRL() {
1117
1350
  });
1118
1351
  }
1119
1352
  async function ask(rl, question) {
1120
- return new Promise((resolve18) => {
1353
+ return new Promise((resolve19) => {
1121
1354
  rl.question(question, (answer) => {
1122
- resolve18(answer.trim());
1355
+ resolve19(answer.trim());
1123
1356
  });
1124
1357
  });
1125
1358
  }
@@ -1144,18 +1377,118 @@ function ensureGitignoreEntry(rootDir, entry) {
1144
1377
  `);
1145
1378
  }
1146
1379
  }
1380
+ function extractTailwindTokens(tokenSources) {
1381
+ const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
1382
+ if (!tailwindSource) return null;
1383
+ try {
1384
+ let parseBlock2 = function(block) {
1385
+ const result = {};
1386
+ const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
1387
+ for (const m of block.matchAll(lineRe)) {
1388
+ if (m[1] !== void 0 && m[2] !== void 0) {
1389
+ result[m[1]] = m[2];
1390
+ }
1391
+ }
1392
+ return result;
1393
+ };
1394
+ var parseBlock = parseBlock2;
1395
+ const raw = readFileSync(tailwindSource.path, "utf-8");
1396
+ const tokens = {};
1397
+ const colorsKeyIdx = raw.indexOf("colors:");
1398
+ if (colorsKeyIdx !== -1) {
1399
+ const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
1400
+ if (colorsBraceStart !== -1) {
1401
+ let colorDepth = 0;
1402
+ let colorsBraceEnd = -1;
1403
+ for (let ci = colorsBraceStart; ci < raw.length; ci++) {
1404
+ if (raw[ci] === "{") colorDepth++;
1405
+ else if (raw[ci] === "}") {
1406
+ colorDepth--;
1407
+ if (colorDepth === 0) {
1408
+ colorsBraceEnd = ci;
1409
+ break;
1410
+ }
1411
+ }
1412
+ }
1413
+ if (colorsBraceEnd > colorsBraceStart) {
1414
+ const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
1415
+ const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
1416
+ const colorTokens = {};
1417
+ for (const sm of colorSection.matchAll(scaleRe)) {
1418
+ if (sm[1] === void 0 || sm[2] === void 0) continue;
1419
+ const scaleName = sm[1];
1420
+ const scaleValues = parseBlock2(sm[2]);
1421
+ if (Object.keys(scaleValues).length > 0) {
1422
+ const scaleTokens = {};
1423
+ for (const [step, hex] of Object.entries(scaleValues)) {
1424
+ scaleTokens[step] = { value: hex, type: "color" };
1425
+ }
1426
+ colorTokens[scaleName] = scaleTokens;
1427
+ }
1428
+ }
1429
+ if (Object.keys(colorTokens).length > 0) {
1430
+ tokens.color = colorTokens;
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
1436
+ if (spacingMatch?.[1] !== void 0) {
1437
+ const spacingValues = parseBlock2(spacingMatch[1]);
1438
+ if (Object.keys(spacingValues).length > 0) {
1439
+ const spacingTokens = {};
1440
+ for (const [key, val] of Object.entries(spacingValues)) {
1441
+ spacingTokens[key] = { value: val, type: "dimension" };
1442
+ }
1443
+ tokens.spacing = spacingTokens;
1444
+ }
1445
+ }
1446
+ const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
1447
+ if (fontFamilyMatch?.[1] !== void 0) {
1448
+ const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
1449
+ const fontTokens = {};
1450
+ for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
1451
+ if (fm[1] !== void 0 && fm[2] !== void 0) {
1452
+ fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
1453
+ }
1454
+ }
1455
+ if (Object.keys(fontTokens).length > 0) {
1456
+ tokens.font = fontTokens;
1457
+ }
1458
+ }
1459
+ const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
1460
+ if (borderRadiusMatch?.[1] !== void 0) {
1461
+ const radiusValues = parseBlock2(borderRadiusMatch[1]);
1462
+ if (Object.keys(radiusValues).length > 0) {
1463
+ const radiusTokens = {};
1464
+ for (const [key, val] of Object.entries(radiusValues)) {
1465
+ radiusTokens[key] = { value: val, type: "dimension" };
1466
+ }
1467
+ tokens.radius = radiusTokens;
1468
+ }
1469
+ }
1470
+ return Object.keys(tokens).length > 0 ? tokens : null;
1471
+ } catch {
1472
+ return null;
1473
+ }
1474
+ }
1147
1475
  function scaffoldConfig(rootDir, config) {
1148
1476
  const path = join(rootDir, "reactscope.config.json");
1149
1477
  writeFileSync(path, `${JSON.stringify(config, null, 2)}
1150
1478
  `);
1151
1479
  return path;
1152
1480
  }
1153
- function scaffoldTokenFile(rootDir, tokenFile) {
1481
+ function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1154
1482
  const path = join(rootDir, tokenFile);
1155
1483
  if (!existsSync(path)) {
1156
1484
  const stub = {
1157
1485
  $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
1158
- tokens: {}
1486
+ version: "1.0.0",
1487
+ meta: {
1488
+ name: "Design Tokens",
1489
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1490
+ },
1491
+ tokens: extractedTokens ?? {}
1159
1492
  };
1160
1493
  writeFileSync(path, `${JSON.stringify(stub, null, 2)}
1161
1494
  `);
@@ -1233,7 +1566,13 @@ async function runInit(options) {
1233
1566
  }
1234
1567
  const cfgPath = scaffoldConfig(rootDir, config);
1235
1568
  created.push(cfgPath);
1236
- const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
1569
+ const extractedTokens = extractTailwindTokens(detected.tokenSources);
1570
+ if (extractedTokens !== null) {
1571
+ const tokenGroupCount = Object.keys(extractedTokens).length;
1572
+ process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
1573
+ `);
1574
+ }
1575
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
1237
1576
  created.push(tokPath);
1238
1577
  const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
1239
1578
  created.push(outDirPath);
@@ -1244,7 +1583,28 @@ async function runInit(options) {
1244
1583
  process.stdout.write(` ${p}
1245
1584
  `);
1246
1585
  }
1247
- process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
1586
+ process.stdout.write("\n Scanning components...\n");
1587
+ try {
1588
+ const manifestConfig = {
1589
+ include: config.components.include,
1590
+ rootDir
1591
+ };
1592
+ const manifest = await generateManifest(manifestConfig);
1593
+ const manifestCount = Object.keys(manifest.components).length;
1594
+ const manifestOutPath = join(rootDir, config.output.dir, "manifest.json");
1595
+ mkdirSync(join(rootDir, config.output.dir), { recursive: true });
1596
+ writeFileSync(manifestOutPath, `${JSON.stringify(manifest, null, 2)}
1597
+ `);
1598
+ process.stdout.write(
1599
+ ` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
1600
+ `
1601
+ );
1602
+ } catch {
1603
+ process.stdout.write(
1604
+ " (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
1605
+ );
1606
+ }
1607
+ process.stdout.write("\n");
1248
1608
  return {
1249
1609
  success: true,
1250
1610
  message: "Project initialised successfully.",
@@ -1333,7 +1693,10 @@ Available: ${available}${hint}`
1333
1693
  });
1334
1694
  }
1335
1695
  function registerQuery(manifestCmd) {
1336
- 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(
1696
+ 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(
1697
+ "--has-prop <spec>",
1698
+ "Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
1699
+ ).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(
1337
1700
  (opts) => {
1338
1701
  try {
1339
1702
  const manifest = loadManifest(opts.manifest);
@@ -1344,9 +1707,11 @@ function registerQuery(manifestCmd) {
1344
1707
  if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
1345
1708
  if (opts.sideEffects) queryParts.push("side-effects");
1346
1709
  if (opts.hasFetch) queryParts.push("has-fetch");
1710
+ if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
1711
+ if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
1347
1712
  if (queryParts.length === 0) {
1348
1713
  process.stderr.write(
1349
- "No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
1714
+ "No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
1350
1715
  );
1351
1716
  process.exit(1);
1352
1717
  }
@@ -1373,6 +1738,27 @@ function registerQuery(manifestCmd) {
1373
1738
  if (opts.hasFetch) {
1374
1739
  entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
1375
1740
  }
1741
+ if (opts.hasProp !== void 0) {
1742
+ const spec = opts.hasProp;
1743
+ const colonIdx = spec.indexOf(":");
1744
+ const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
1745
+ const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
1746
+ entries = entries.filter(([, d]) => {
1747
+ const props = d.props;
1748
+ if (!props || !(propName in props)) return false;
1749
+ if (propType !== void 0) {
1750
+ return props[propName]?.type === propType;
1751
+ }
1752
+ return true;
1753
+ });
1754
+ }
1755
+ if (opts.composedBy !== void 0) {
1756
+ const targetName = opts.composedBy;
1757
+ entries = entries.filter(([, d]) => {
1758
+ const composedBy = d.composedBy;
1759
+ return composedBy !== void 0 && composedBy.includes(targetName);
1760
+ });
1761
+ }
1376
1762
  const rows = entries.map(([name, d]) => ({
1377
1763
  name,
1378
1764
  file: d.filePath,
@@ -3057,6 +3443,17 @@ ${msg}`);
3057
3443
  }
3058
3444
 
3059
3445
  // src/render-commands.ts
3446
+ function loadGlobalCssFilesFromConfig(cwd) {
3447
+ const configPath = resolve(cwd, "reactscope.config.json");
3448
+ if (!existsSync(configPath)) return [];
3449
+ try {
3450
+ const raw = readFileSync(configPath, "utf-8");
3451
+ const cfg = JSON.parse(raw);
3452
+ return cfg.components?.wrappers?.globalCSS ?? [];
3453
+ } catch {
3454
+ return [];
3455
+ }
3456
+ }
3060
3457
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
3061
3458
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
3062
3459
  var _pool3 = null;
@@ -3077,7 +3474,7 @@ async function shutdownPool3() {
3077
3474
  _pool3 = null;
3078
3475
  }
3079
3476
  }
3080
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
3477
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
3081
3478
  const satori = new SatoriRenderer({
3082
3479
  defaultViewport: { width: viewportWidth, height: viewportHeight }
3083
3480
  });
@@ -3086,13 +3483,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3086
3483
  async renderCell(props, _complexityClass) {
3087
3484
  const startMs = performance.now();
3088
3485
  const pool = await getPool3(viewportWidth, viewportHeight);
3486
+ const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
3089
3487
  const htmlHarness = await buildComponentHarness(
3090
3488
  filePath,
3091
3489
  componentName,
3092
3490
  props,
3093
3491
  viewportWidth,
3094
- void 0,
3095
- // projectCss (handled separately)
3492
+ projectCss ?? void 0,
3096
3493
  wrapperScript
3097
3494
  );
3098
3495
  const slot = await pool.acquire();
@@ -3121,10 +3518,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3121
3518
  }
3122
3519
  });
3123
3520
  return [...set];
3124
- });
3125
- const projectCss = await getCompiledCssForClasses(rootDir, classes);
3126
- if (projectCss != null && projectCss.length > 0) {
3127
- await page.addStyleTag({ content: projectCss });
3521
+ }) ?? [];
3522
+ const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
3523
+ if (projectCss2 != null && projectCss2.length > 0) {
3524
+ await page.addStyleTag({ content: projectCss2 });
3128
3525
  }
3129
3526
  const renderTimeMs = performance.now() - startMs;
3130
3527
  const rootLocator = page.locator("[data-reactscope-root]");
@@ -3134,49 +3531,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3134
3531
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3135
3532
  );
3136
3533
  }
3137
- const PAD = 24;
3138
- const MIN_W = 320;
3139
- const MIN_H = 200;
3534
+ const PAD = 8;
3140
3535
  const clipX = Math.max(0, boundingBox.x - PAD);
3141
3536
  const clipY = Math.max(0, boundingBox.y - PAD);
3142
3537
  const rawW = boundingBox.width + PAD * 2;
3143
3538
  const rawH = boundingBox.height + PAD * 2;
3144
- const clipW = Math.max(rawW, MIN_W);
3145
- const clipH = Math.max(rawH, MIN_H);
3146
- const safeW = Math.min(clipW, viewportWidth - clipX);
3147
- const safeH = Math.min(clipH, viewportHeight - clipY);
3539
+ const safeW = Math.min(rawW, viewportWidth - clipX);
3540
+ const safeH = Math.min(rawH, viewportHeight - clipY);
3148
3541
  const screenshot = await page.screenshot({
3149
3542
  clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3150
3543
  type: "png"
3151
3544
  });
3545
+ const STYLE_PROPS = [
3546
+ "display",
3547
+ "width",
3548
+ "height",
3549
+ "color",
3550
+ "backgroundColor",
3551
+ "fontSize",
3552
+ "fontFamily",
3553
+ "fontWeight",
3554
+ "lineHeight",
3555
+ "padding",
3556
+ "paddingTop",
3557
+ "paddingRight",
3558
+ "paddingBottom",
3559
+ "paddingLeft",
3560
+ "margin",
3561
+ "marginTop",
3562
+ "marginRight",
3563
+ "marginBottom",
3564
+ "marginLeft",
3565
+ "gap",
3566
+ "borderRadius",
3567
+ "borderWidth",
3568
+ "borderColor",
3569
+ "borderStyle",
3570
+ "boxShadow",
3571
+ "opacity",
3572
+ "position",
3573
+ "flexDirection",
3574
+ "alignItems",
3575
+ "justifyContent",
3576
+ "overflow"
3577
+ ];
3578
+ const _domResult = await page.evaluate(
3579
+ (args) => {
3580
+ let count = 0;
3581
+ const styles = {};
3582
+ function captureStyles(el, id, propList) {
3583
+ const computed = window.getComputedStyle(el);
3584
+ const out = {};
3585
+ for (const prop of propList) {
3586
+ const val = computed[prop] ?? "";
3587
+ if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
3588
+ }
3589
+ styles[id] = out;
3590
+ }
3591
+ function walk(node) {
3592
+ if (node.nodeType === Node.TEXT_NODE) {
3593
+ return {
3594
+ tag: "#text",
3595
+ attrs: {},
3596
+ text: node.textContent?.trim() ?? "",
3597
+ children: []
3598
+ };
3599
+ }
3600
+ const el = node;
3601
+ const id = count++;
3602
+ captureStyles(el, id, args.props);
3603
+ const attrs = {};
3604
+ for (const attr of Array.from(el.attributes)) {
3605
+ attrs[attr.name] = attr.value;
3606
+ }
3607
+ const children = Array.from(el.childNodes).filter(
3608
+ (n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
3609
+ ).map(walk);
3610
+ return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
3611
+ }
3612
+ const root = document.querySelector(args.sel);
3613
+ if (!root)
3614
+ return {
3615
+ tree: { tag: "div", attrs: {}, children: [] },
3616
+ elementCount: 0,
3617
+ nodeStyles: {}
3618
+ };
3619
+ return { tree: walk(root), elementCount: count, nodeStyles: styles };
3620
+ },
3621
+ { sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
3622
+ );
3623
+ const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
3624
+ const elementCount = _domResult?.elementCount ?? 0;
3625
+ const nodeStyles = _domResult?.nodeStyles ?? {};
3152
3626
  const computedStyles = {};
3153
- const styles = await page.evaluate((sel) => {
3154
- const el = document.querySelector(sel);
3155
- if (el === null) return {};
3156
- const computed = window.getComputedStyle(el);
3157
- const out = {};
3158
- for (const prop of [
3159
- "display",
3160
- "width",
3161
- "height",
3162
- "color",
3163
- "backgroundColor",
3164
- "fontSize",
3165
- "fontFamily",
3166
- "padding",
3167
- "margin"
3168
- ]) {
3169
- out[prop] = computed.getPropertyValue(prop);
3627
+ if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
3628
+ for (const [nodeId, styles] of Object.entries(nodeStyles)) {
3629
+ computedStyles[`#node-${nodeId}`] = styles;
3630
+ }
3631
+ const dom = {
3632
+ tree: domTree,
3633
+ elementCount,
3634
+ boundingBox: {
3635
+ x: boundingBox.x,
3636
+ y: boundingBox.y,
3637
+ width: boundingBox.width,
3638
+ height: boundingBox.height
3170
3639
  }
3171
- return out;
3172
- }, "[data-reactscope-root] > *");
3173
- computedStyles["[data-reactscope-root] > *"] = styles;
3640
+ };
3641
+ const a11yInfo = await page.evaluate((sel) => {
3642
+ const wrapper = document.querySelector(sel);
3643
+ const el = wrapper?.firstElementChild ?? wrapper;
3644
+ if (!el) return { role: "generic", name: "" };
3645
+ return {
3646
+ role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
3647
+ name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
3648
+ };
3649
+ }, "[data-reactscope-root]") ?? {
3650
+ role: "generic",
3651
+ name: ""
3652
+ };
3653
+ const imgViolations = await page.evaluate((sel) => {
3654
+ const container = document.querySelector(sel);
3655
+ if (!container) return [];
3656
+ const issues = [];
3657
+ container.querySelectorAll("img").forEach((img) => {
3658
+ if (!img.alt) issues.push("Image missing accessible name");
3659
+ });
3660
+ return issues;
3661
+ }, "[data-reactscope-root]") ?? [];
3662
+ const accessibility = {
3663
+ role: a11yInfo.role,
3664
+ name: a11yInfo.name,
3665
+ violations: imgViolations
3666
+ };
3174
3667
  return {
3175
3668
  screenshot,
3176
3669
  width: Math.round(safeW),
3177
3670
  height: Math.round(safeH),
3178
3671
  renderTimeMs,
3179
- computedStyles
3672
+ computedStyles,
3673
+ dom,
3674
+ accessibility
3180
3675
  };
3181
3676
  } finally {
3182
3677
  pool.release(slot);
@@ -3226,26 +3721,64 @@ function registerRenderSingle(renderCmd) {
3226
3721
  Available: ${available}`
3227
3722
  );
3228
3723
  }
3724
+ let props = {};
3725
+ if (opts.props !== void 0) {
3726
+ try {
3727
+ props = JSON.parse(opts.props);
3728
+ } catch {
3729
+ throw new Error(`Invalid props JSON: ${opts.props}`);
3730
+ }
3731
+ }
3732
+ if (descriptor.props !== void 0) {
3733
+ const propDefs = descriptor.props;
3734
+ for (const [propName, propDef] of Object.entries(propDefs)) {
3735
+ if (propName in props) continue;
3736
+ if (!propDef.required && propDef.default !== void 0) continue;
3737
+ if (propDef.type === "node" || propDef.type === "string") {
3738
+ props[propName] = propName === "children" ? componentName : propName;
3739
+ } else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
3740
+ props[propName] = propDef.values[0];
3741
+ } else if (propDef.type === "boolean") {
3742
+ props[propName] = false;
3743
+ } else if (propDef.type === "number") {
3744
+ props[propName] = 0;
3745
+ }
3746
+ }
3747
+ }
3229
3748
  const { width, height } = parseViewport(opts.viewport);
3230
3749
  const rootDir = process.cwd();
3231
3750
  const filePath = resolve(rootDir, descriptor.filePath);
3232
3751
  const scopeData = await loadScopeFileForComponent(filePath);
3233
3752
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3234
3753
  const scenarios = buildScenarioMap(opts, scopeData);
3235
- const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
3754
+ const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3755
+ if (globalCssFiles.length === 0) {
3756
+ process.stderr.write(
3757
+ "warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
3758
+ );
3759
+ }
3760
+ const renderer = buildRenderer(
3761
+ filePath,
3762
+ componentName,
3763
+ width,
3764
+ height,
3765
+ globalCssFiles,
3766
+ rootDir,
3767
+ wrapperScript
3768
+ );
3236
3769
  process.stderr.write(
3237
3770
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3238
3771
  `
3239
3772
  );
3240
3773
  const fmt2 = resolveSingleFormat(opts.format);
3241
3774
  let anyFailed = false;
3242
- for (const [scenarioName, props] of Object.entries(scenarios)) {
3775
+ for (const [scenarioName, props2] of Object.entries(scenarios)) {
3243
3776
  const isNamed = scenarioName !== "__default__";
3244
3777
  const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
3245
3778
  const outcome = await safeRender(
3246
- () => renderer.renderCell(props, descriptor.complexityClass),
3779
+ () => renderer.renderCell(props2, descriptor.complexityClass),
3247
3780
  {
3248
- props,
3781
+ props: props2,
3249
3782
  sourceLocation: {
3250
3783
  file: descriptor.filePath,
3251
3784
  line: descriptor.loc.start,
@@ -3274,7 +3807,7 @@ Available: ${available}`
3274
3807
  `
3275
3808
  );
3276
3809
  } else if (fmt2 === "json") {
3277
- const json = formatRenderJson(label, props, result);
3810
+ const json = formatRenderJson(label, props2, result);
3278
3811
  process.stdout.write(`${JSON.stringify(json, null, 2)}
3279
3812
  `);
3280
3813
  } else {
@@ -3301,7 +3834,10 @@ Available: ${available}`
3301
3834
  );
3302
3835
  }
3303
3836
  function registerRenderMatrix(renderCmd) {
3304
- 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(
3837
+ renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
3838
+ "--axes <spec>",
3839
+ `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"]}'`
3840
+ ).option(
3305
3841
  "--contexts <ids>",
3306
3842
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
3307
3843
  ).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(
@@ -3320,21 +3856,47 @@ Available: ${available}`
3320
3856
  const { width, height } = { width: 375, height: 812 };
3321
3857
  const rootDir = process.cwd();
3322
3858
  const filePath = resolve(rootDir, descriptor.filePath);
3323
- const renderer = buildRenderer(filePath, componentName, width, height);
3859
+ const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3860
+ const renderer = buildRenderer(
3861
+ filePath,
3862
+ componentName,
3863
+ width,
3864
+ height,
3865
+ matrixCssFiles,
3866
+ rootDir
3867
+ );
3324
3868
  const axes = [];
3325
3869
  if (opts.axes !== void 0) {
3326
- const axisSpecs = opts.axes.trim().split(/\s+/);
3327
- for (const spec of axisSpecs) {
3328
- const colonIdx = spec.indexOf(":");
3329
- if (colonIdx < 0) {
3330
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
3870
+ const axesRaw = opts.axes.trim();
3871
+ if (axesRaw.startsWith("{")) {
3872
+ let parsed;
3873
+ try {
3874
+ parsed = JSON.parse(axesRaw);
3875
+ } catch {
3876
+ throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
3877
+ }
3878
+ for (const [name, vals] of Object.entries(parsed)) {
3879
+ if (!Array.isArray(vals)) {
3880
+ throw new Error(`Axis "${name}" must be an array of values in JSON format`);
3881
+ }
3882
+ axes.push({ name, values: vals.map(String) });
3331
3883
  }
3332
- const name = spec.slice(0, colonIdx);
3333
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3334
- if (name.length === 0 || values.length === 0) {
3335
- throw new Error(`Invalid axis spec "${spec}"`);
3884
+ } else {
3885
+ const axisSpecs = axesRaw.split(/\s+/);
3886
+ for (const spec of axisSpecs) {
3887
+ const colonIdx = spec.indexOf(":");
3888
+ if (colonIdx < 0) {
3889
+ throw new Error(
3890
+ `Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
3891
+ );
3892
+ }
3893
+ const name = spec.slice(0, colonIdx);
3894
+ const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3895
+ if (name.length === 0 || values.length === 0) {
3896
+ throw new Error(`Invalid axis spec "${spec}"`);
3897
+ }
3898
+ axes.push({ name, values });
3336
3899
  }
3337
- axes.push({ name, values });
3338
3900
  }
3339
3901
  }
3340
3902
  if (opts.contexts !== void 0) {
@@ -3448,16 +4010,31 @@ function registerRenderAll(renderCmd) {
3448
4010
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
3449
4011
  `);
3450
4012
  const results = [];
4013
+ const complianceStylesMap = {};
3451
4014
  let completed = 0;
3452
4015
  const renderOne = async (name) => {
3453
4016
  const descriptor = manifest.components[name];
3454
4017
  if (descriptor === void 0) return;
3455
4018
  const filePath = resolve(rootDir, descriptor.filePath);
3456
- const renderer = buildRenderer(filePath, name, 375, 812);
4019
+ const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
4020
+ const scopeData = await loadScopeFileForComponent(filePath);
4021
+ const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
4022
+ const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
4023
+ const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
4024
+ const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
4025
+ const renderer = buildRenderer(
4026
+ filePath,
4027
+ name,
4028
+ 375,
4029
+ 812,
4030
+ allCssFiles,
4031
+ process.cwd(),
4032
+ wrapperScript
4033
+ );
3457
4034
  const outcome = await safeRender(
3458
- () => renderer.renderCell({}, descriptor.complexityClass),
4035
+ () => renderer.renderCell(renderProps, descriptor.complexityClass),
3459
4036
  {
3460
- props: {},
4037
+ props: renderProps,
3461
4038
  sourceLocation: {
3462
4039
  file: descriptor.filePath,
3463
4040
  line: descriptor.loc.start,
@@ -3497,6 +4074,77 @@ function registerRenderAll(renderCmd) {
3497
4074
  writeFileSync(pngPath, result.screenshot);
3498
4075
  const jsonPath = resolve(outputDir, `${name}.json`);
3499
4076
  writeFileSync(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
4077
+ const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
4078
+ const compStyles = {
4079
+ colors: {},
4080
+ spacing: {},
4081
+ typography: {},
4082
+ borders: {},
4083
+ shadows: {}
4084
+ };
4085
+ for (const [prop, val] of Object.entries(rawStyles)) {
4086
+ if (!val || val === "none" || val === "") continue;
4087
+ const lower = prop.toLowerCase();
4088
+ if (lower.includes("color") || lower.includes("background")) {
4089
+ compStyles.colors[prop] = val;
4090
+ } else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
4091
+ compStyles.spacing[prop] = val;
4092
+ } else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
4093
+ compStyles.typography[prop] = val;
4094
+ } else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
4095
+ compStyles.borders[prop] = val;
4096
+ } else if (lower.includes("shadow")) {
4097
+ compStyles.shadows[prop] = val;
4098
+ }
4099
+ }
4100
+ complianceStylesMap[name] = compStyles;
4101
+ if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
4102
+ try {
4103
+ const scenarioEntries2 = Object.entries(scopeData.scenarios);
4104
+ const scenarioAxis = {
4105
+ name: "scenario",
4106
+ values: scenarioEntries2.map(([k]) => k)
4107
+ };
4108
+ const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
4109
+ const matrixRenderer = buildRenderer(
4110
+ filePath,
4111
+ name,
4112
+ 375,
4113
+ 812,
4114
+ allCssFiles,
4115
+ process.cwd(),
4116
+ wrapperScript
4117
+ );
4118
+ const wrappedRenderer = {
4119
+ _satori: matrixRenderer._satori,
4120
+ async renderCell(props, cc) {
4121
+ const scenarioName = props.scenario;
4122
+ const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
4123
+ return matrixRenderer.renderCell(realProps, cc ?? "simple");
4124
+ }
4125
+ };
4126
+ const matrix = new RenderMatrix(wrappedRenderer, [scenarioAxis], {
4127
+ concurrency: 2
4128
+ });
4129
+ const matrixResult = await matrix.render();
4130
+ const matrixCells = matrixResult.cells.map((cell) => ({
4131
+ axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
4132
+ screenshot: cell.result.screenshot.toString("base64"),
4133
+ width: cell.result.width,
4134
+ height: cell.result.height,
4135
+ renderTimeMs: cell.result.renderTimeMs
4136
+ }));
4137
+ const existingJson = JSON.parse(readFileSync(jsonPath, "utf-8"));
4138
+ existingJson.cells = matrixCells;
4139
+ existingJson.axisLabels = [scenarioAxis.values];
4140
+ writeFileSync(jsonPath, JSON.stringify(existingJson, null, 2));
4141
+ } catch (matrixErr) {
4142
+ process.stderr.write(
4143
+ ` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
4144
+ `
4145
+ );
4146
+ }
4147
+ }
3500
4148
  if (isTTY()) {
3501
4149
  process.stdout.write(
3502
4150
  `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -3520,6 +4168,14 @@ function registerRenderAll(renderCmd) {
3520
4168
  }
3521
4169
  await Promise.all(workers);
3522
4170
  await shutdownPool3();
4171
+ const compStylesPath = resolve(
4172
+ resolve(process.cwd(), opts.outputDir),
4173
+ "..",
4174
+ "compliance-styles.json"
4175
+ );
4176
+ writeFileSync(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
4177
+ process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
4178
+ `);
3523
4179
  process.stderr.write("\n");
3524
4180
  const summary = formatSummaryText(results, outputDir);
3525
4181
  process.stderr.write(`${summary}
@@ -3719,12 +4375,12 @@ async function runBaseline(options = {}) {
3719
4375
  mkdirSync(rendersDir, { recursive: true });
3720
4376
  let manifest;
3721
4377
  if (manifestPath !== void 0) {
3722
- const { readFileSync: readFileSync12 } = await import('fs');
4378
+ const { readFileSync: readFileSync14 } = await import('fs');
3723
4379
  const absPath = resolve(rootDir, manifestPath);
3724
4380
  if (!existsSync(absPath)) {
3725
4381
  throw new Error(`Manifest not found at ${absPath}.`);
3726
4382
  }
3727
- manifest = JSON.parse(readFileSync12(absPath, "utf-8"));
4383
+ manifest = JSON.parse(readFileSync14(absPath, "utf-8"));
3728
4384
  process.stderr.write(`Loaded manifest from ${manifestPath}
3729
4385
  `);
3730
4386
  } else {
@@ -5085,10 +5741,20 @@ function createTokensExportCommand() {
5085
5741
  ).action(
5086
5742
  (opts) => {
5087
5743
  if (!SUPPORTED_FORMATS.includes(opts.format)) {
5744
+ const FORMAT_ALIASES = {
5745
+ json: "flat-json",
5746
+ "json-flat": "flat-json",
5747
+ javascript: "ts",
5748
+ js: "ts",
5749
+ sass: "scss",
5750
+ tw: "tailwind"
5751
+ };
5752
+ const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
5088
5753
  process.stderr.write(
5089
5754
  `Error: unsupported format "${opts.format}".
5090
5755
  Supported formats: ${SUPPORTED_FORMATS.join(", ")}
5091
- `
5756
+ ` + (hint ? `Did you mean "${hint}"?
5757
+ ` : "")
5092
5758
  );
5093
5759
  process.exit(1);
5094
5760
  }
@@ -5902,6 +6568,7 @@ function createProgram(options = {}) {
5902
6568
  program.addCommand(createInstrumentCommand());
5903
6569
  program.addCommand(createInitCommand());
5904
6570
  program.addCommand(createCiCommand());
6571
+ program.addCommand(createDoctorCommand());
5905
6572
  const existingReportCmd = program.commands.find((c) => c.name() === "report");
5906
6573
  if (existingReportCmd !== void 0) {
5907
6574
  registerBaselineSubCommand(existingReportCmd);
@@ -5912,6 +6579,6 @@ function createProgram(options = {}) {
5912
6579
  return program;
5913
6580
  }
5914
6581
 
5915
- export { CI_EXIT, createCiCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
6582
+ export { CI_EXIT, createCiCommand, createDoctorCommand, createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, formatCiReport, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runCi, runInit };
5916
6583
  //# sourceMappingURL=index.js.map
5917
6584
  //# sourceMappingURL=index.js.map