@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/cli.js +926 -253
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +736 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +737 -70
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -118,6 +118,21 @@ import { createElement } from "react";
|
|
|
118
118
|
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
119
119
|
banner: {
|
|
120
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"
|
|
121
136
|
}
|
|
122
137
|
});
|
|
123
138
|
if (result.errors.length > 0) {
|
|
@@ -525,6 +540,57 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
525
540
|
if (deduped.length === 0) return null;
|
|
526
541
|
return build3(deduped);
|
|
527
542
|
}
|
|
543
|
+
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
544
|
+
const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import('fs');
|
|
545
|
+
const { createRequire: createRequire3 } = await import('module');
|
|
546
|
+
if (!existsSync16(cssFilePath)) return null;
|
|
547
|
+
const raw = readFileSync14(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;
|
|
593
|
+
}
|
|
528
594
|
|
|
529
595
|
// src/ci/commands.ts
|
|
530
596
|
var CI_EXIT = {
|
|
@@ -947,6 +1013,157 @@ function createCiCommand() {
|
|
|
947
1013
|
}
|
|
948
1014
|
);
|
|
949
1015
|
}
|
|
1016
|
+
function collectSourceFiles(dir) {
|
|
1017
|
+
if (!fs.existsSync(dir)) return [];
|
|
1018
|
+
const results = [];
|
|
1019
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1020
|
+
const full = path.join(dir, entry.name);
|
|
1021
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
|
|
1022
|
+
results.push(...collectSourceFiles(full));
|
|
1023
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
1024
|
+
results.push(full);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return results;
|
|
1028
|
+
}
|
|
1029
|
+
function checkConfig(cwd) {
|
|
1030
|
+
const configPath = path.resolve(cwd, "reactscope.config.json");
|
|
1031
|
+
if (!fs.existsSync(configPath)) {
|
|
1032
|
+
return {
|
|
1033
|
+
name: "config",
|
|
1034
|
+
status: "error",
|
|
1035
|
+
message: "reactscope.config.json not found \u2014 run `scope init`"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1040
|
+
return { name: "config", status: "ok", message: "reactscope.config.json valid" };
|
|
1041
|
+
} catch {
|
|
1042
|
+
return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function checkTokens(cwd) {
|
|
1046
|
+
const configPath = path.resolve(cwd, "reactscope.config.json");
|
|
1047
|
+
let tokensPath = path.resolve(cwd, "reactscope.tokens.json");
|
|
1048
|
+
if (fs.existsSync(configPath)) {
|
|
1049
|
+
try {
|
|
1050
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1051
|
+
if (cfg.tokens?.file) tokensPath = path.resolve(cwd, cfg.tokens.file);
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (!fs.existsSync(tokensPath)) {
|
|
1056
|
+
return {
|
|
1057
|
+
name: "tokens",
|
|
1058
|
+
status: "warn",
|
|
1059
|
+
message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
const raw = JSON.parse(fs.readFileSync(tokensPath, "utf-8"));
|
|
1064
|
+
if (!raw.version) {
|
|
1065
|
+
return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
|
|
1066
|
+
}
|
|
1067
|
+
return { name: "tokens", status: "ok", message: "Token file valid" };
|
|
1068
|
+
} catch {
|
|
1069
|
+
return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function checkGlobalCss(cwd) {
|
|
1073
|
+
const configPath = path.resolve(cwd, "reactscope.config.json");
|
|
1074
|
+
let globalCss = [];
|
|
1075
|
+
if (fs.existsSync(configPath)) {
|
|
1076
|
+
try {
|
|
1077
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1078
|
+
globalCss = cfg.components?.wrappers?.globalCSS ?? [];
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (globalCss.length === 0) {
|
|
1083
|
+
return {
|
|
1084
|
+
name: "globalCSS",
|
|
1085
|
+
status: "warn",
|
|
1086
|
+
message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const missing = globalCss.filter((f) => !fs.existsSync(path.resolve(cwd, f)));
|
|
1090
|
+
if (missing.length > 0) {
|
|
1091
|
+
return {
|
|
1092
|
+
name: "globalCSS",
|
|
1093
|
+
status: "error",
|
|
1094
|
+
message: `globalCSS file(s) not found: ${missing.join(", ")}`
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
name: "globalCSS",
|
|
1099
|
+
status: "ok",
|
|
1100
|
+
message: `${globalCss.length} globalCSS file(s) present`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function checkManifest(cwd) {
|
|
1104
|
+
const manifestPath = path.resolve(cwd, ".reactscope", "manifest.json");
|
|
1105
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1106
|
+
return {
|
|
1107
|
+
name: "manifest",
|
|
1108
|
+
status: "warn",
|
|
1109
|
+
message: "Manifest not found \u2014 run `scope manifest generate`"
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
const manifestMtime = fs.statSync(manifestPath).mtimeMs;
|
|
1113
|
+
const sourceDir = path.resolve(cwd, "src");
|
|
1114
|
+
const sourceFiles = collectSourceFiles(sourceDir);
|
|
1115
|
+
const stale = sourceFiles.filter((f) => fs.statSync(f).mtimeMs > manifestMtime);
|
|
1116
|
+
if (stale.length > 0) {
|
|
1117
|
+
return {
|
|
1118
|
+
name: "manifest",
|
|
1119
|
+
status: "warn",
|
|
1120
|
+
message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
|
|
1124
|
+
}
|
|
1125
|
+
var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
|
|
1126
|
+
function formatCheck(check) {
|
|
1127
|
+
return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
|
|
1128
|
+
}
|
|
1129
|
+
function createDoctorCommand() {
|
|
1130
|
+
return new commander.Command("doctor").description("Check the health of your Scope setup (config, tokens, CSS, manifest)").option("--json", "Emit structured JSON output", false).action((opts) => {
|
|
1131
|
+
const cwd = process.cwd();
|
|
1132
|
+
const checks = [
|
|
1133
|
+
checkConfig(cwd),
|
|
1134
|
+
checkTokens(cwd),
|
|
1135
|
+
checkGlobalCss(cwd),
|
|
1136
|
+
checkManifest(cwd)
|
|
1137
|
+
];
|
|
1138
|
+
const errors = checks.filter((c) => c.status === "error").length;
|
|
1139
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
1140
|
+
if (opts.json) {
|
|
1141
|
+
process.stdout.write(
|
|
1142
|
+
`${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
|
|
1143
|
+
`
|
|
1144
|
+
);
|
|
1145
|
+
if (errors > 0) process.exit(1);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
process.stdout.write("\nScope Doctor\n");
|
|
1149
|
+
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");
|
|
1150
|
+
for (const check of checks) process.stdout.write(`${formatCheck(check)}
|
|
1151
|
+
`);
|
|
1152
|
+
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");
|
|
1153
|
+
if (errors > 0) {
|
|
1154
|
+
process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
|
|
1155
|
+
|
|
1156
|
+
`);
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
} else if (warnings > 0) {
|
|
1159
|
+
process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
|
|
1160
|
+
|
|
1161
|
+
`);
|
|
1162
|
+
} else {
|
|
1163
|
+
process.stdout.write(" All checks passed!\n\n");
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
950
1167
|
function hasConfigFile(dir, stem) {
|
|
951
1168
|
if (!fs.existsSync(dir)) return false;
|
|
952
1169
|
try {
|
|
@@ -1028,6 +1245,20 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1028
1245
|
}
|
|
1029
1246
|
return unique;
|
|
1030
1247
|
}
|
|
1248
|
+
var GLOBAL_CSS_CANDIDATES = [
|
|
1249
|
+
"src/styles.css",
|
|
1250
|
+
"src/index.css",
|
|
1251
|
+
"src/global.css",
|
|
1252
|
+
"src/globals.css",
|
|
1253
|
+
"src/app.css",
|
|
1254
|
+
"src/main.css",
|
|
1255
|
+
"styles/globals.css",
|
|
1256
|
+
"styles/global.css",
|
|
1257
|
+
"styles/index.css"
|
|
1258
|
+
];
|
|
1259
|
+
function detectGlobalCSSFiles(rootDir) {
|
|
1260
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(rootDir, rel)));
|
|
1261
|
+
}
|
|
1031
1262
|
var TAILWIND_STEMS = ["tailwind.config"];
|
|
1032
1263
|
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
1033
1264
|
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
@@ -1095,13 +1326,15 @@ function detectProject(rootDir) {
|
|
|
1095
1326
|
const packageManager = detectPackageManager(rootDir);
|
|
1096
1327
|
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
1097
1328
|
const tokenSources = detectTokenSources(rootDir);
|
|
1329
|
+
const globalCSSFiles = detectGlobalCSSFiles(rootDir);
|
|
1098
1330
|
return {
|
|
1099
1331
|
framework,
|
|
1100
1332
|
typescript,
|
|
1101
1333
|
tsconfigPath,
|
|
1102
1334
|
componentPatterns,
|
|
1103
1335
|
tokenSources,
|
|
1104
|
-
packageManager
|
|
1336
|
+
packageManager,
|
|
1337
|
+
globalCSSFiles
|
|
1105
1338
|
};
|
|
1106
1339
|
}
|
|
1107
1340
|
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
@@ -1110,7 +1343,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
|
1110
1343
|
components: {
|
|
1111
1344
|
include,
|
|
1112
1345
|
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
1113
|
-
wrappers: { providers: [], globalCSS: [] }
|
|
1346
|
+
wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
|
|
1114
1347
|
},
|
|
1115
1348
|
render: {
|
|
1116
1349
|
viewport: { default: { width: 1280, height: 800 } },
|
|
@@ -1141,9 +1374,9 @@ function createRL() {
|
|
|
1141
1374
|
});
|
|
1142
1375
|
}
|
|
1143
1376
|
async function ask(rl, question) {
|
|
1144
|
-
return new Promise((
|
|
1377
|
+
return new Promise((resolve19) => {
|
|
1145
1378
|
rl.question(question, (answer) => {
|
|
1146
|
-
|
|
1379
|
+
resolve19(answer.trim());
|
|
1147
1380
|
});
|
|
1148
1381
|
});
|
|
1149
1382
|
}
|
|
@@ -1168,18 +1401,118 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1168
1401
|
`);
|
|
1169
1402
|
}
|
|
1170
1403
|
}
|
|
1404
|
+
function extractTailwindTokens(tokenSources) {
|
|
1405
|
+
const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
|
|
1406
|
+
if (!tailwindSource) return null;
|
|
1407
|
+
try {
|
|
1408
|
+
let parseBlock2 = function(block) {
|
|
1409
|
+
const result = {};
|
|
1410
|
+
const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
|
|
1411
|
+
for (const m of block.matchAll(lineRe)) {
|
|
1412
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
1413
|
+
result[m[1]] = m[2];
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return result;
|
|
1417
|
+
};
|
|
1418
|
+
var parseBlock = parseBlock2;
|
|
1419
|
+
const raw = fs.readFileSync(tailwindSource.path, "utf-8");
|
|
1420
|
+
const tokens = {};
|
|
1421
|
+
const colorsKeyIdx = raw.indexOf("colors:");
|
|
1422
|
+
if (colorsKeyIdx !== -1) {
|
|
1423
|
+
const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
|
|
1424
|
+
if (colorsBraceStart !== -1) {
|
|
1425
|
+
let colorDepth = 0;
|
|
1426
|
+
let colorsBraceEnd = -1;
|
|
1427
|
+
for (let ci = colorsBraceStart; ci < raw.length; ci++) {
|
|
1428
|
+
if (raw[ci] === "{") colorDepth++;
|
|
1429
|
+
else if (raw[ci] === "}") {
|
|
1430
|
+
colorDepth--;
|
|
1431
|
+
if (colorDepth === 0) {
|
|
1432
|
+
colorsBraceEnd = ci;
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (colorsBraceEnd > colorsBraceStart) {
|
|
1438
|
+
const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
|
|
1439
|
+
const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1440
|
+
const colorTokens = {};
|
|
1441
|
+
for (const sm of colorSection.matchAll(scaleRe)) {
|
|
1442
|
+
if (sm[1] === void 0 || sm[2] === void 0) continue;
|
|
1443
|
+
const scaleName = sm[1];
|
|
1444
|
+
const scaleValues = parseBlock2(sm[2]);
|
|
1445
|
+
if (Object.keys(scaleValues).length > 0) {
|
|
1446
|
+
const scaleTokens = {};
|
|
1447
|
+
for (const [step, hex] of Object.entries(scaleValues)) {
|
|
1448
|
+
scaleTokens[step] = { value: hex, type: "color" };
|
|
1449
|
+
}
|
|
1450
|
+
colorTokens[scaleName] = scaleTokens;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (Object.keys(colorTokens).length > 0) {
|
|
1454
|
+
tokens.color = colorTokens;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1460
|
+
if (spacingMatch?.[1] !== void 0) {
|
|
1461
|
+
const spacingValues = parseBlock2(spacingMatch[1]);
|
|
1462
|
+
if (Object.keys(spacingValues).length > 0) {
|
|
1463
|
+
const spacingTokens = {};
|
|
1464
|
+
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1465
|
+
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1466
|
+
}
|
|
1467
|
+
tokens.spacing = spacingTokens;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1471
|
+
if (fontFamilyMatch?.[1] !== void 0) {
|
|
1472
|
+
const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
|
|
1473
|
+
const fontTokens = {};
|
|
1474
|
+
for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
|
|
1475
|
+
if (fm[1] !== void 0 && fm[2] !== void 0) {
|
|
1476
|
+
fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (Object.keys(fontTokens).length > 0) {
|
|
1480
|
+
tokens.font = fontTokens;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1484
|
+
if (borderRadiusMatch?.[1] !== void 0) {
|
|
1485
|
+
const radiusValues = parseBlock2(borderRadiusMatch[1]);
|
|
1486
|
+
if (Object.keys(radiusValues).length > 0) {
|
|
1487
|
+
const radiusTokens = {};
|
|
1488
|
+
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1489
|
+
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1490
|
+
}
|
|
1491
|
+
tokens.radius = radiusTokens;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1171
1499
|
function scaffoldConfig(rootDir, config) {
|
|
1172
1500
|
const path$1 = path.join(rootDir, "reactscope.config.json");
|
|
1173
1501
|
fs.writeFileSync(path$1, `${JSON.stringify(config, null, 2)}
|
|
1174
1502
|
`);
|
|
1175
1503
|
return path$1;
|
|
1176
1504
|
}
|
|
1177
|
-
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
1505
|
+
function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
|
|
1178
1506
|
const path$1 = path.join(rootDir, tokenFile);
|
|
1179
1507
|
if (!fs.existsSync(path$1)) {
|
|
1180
1508
|
const stub = {
|
|
1181
1509
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
1182
|
-
|
|
1510
|
+
version: "1.0.0",
|
|
1511
|
+
meta: {
|
|
1512
|
+
name: "Design Tokens",
|
|
1513
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1514
|
+
},
|
|
1515
|
+
tokens: extractedTokens ?? {}
|
|
1183
1516
|
};
|
|
1184
1517
|
fs.writeFileSync(path$1, `${JSON.stringify(stub, null, 2)}
|
|
1185
1518
|
`);
|
|
@@ -1257,7 +1590,13 @@ async function runInit(options) {
|
|
|
1257
1590
|
}
|
|
1258
1591
|
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1259
1592
|
created.push(cfgPath);
|
|
1260
|
-
const
|
|
1593
|
+
const extractedTokens = extractTailwindTokens(detected.tokenSources);
|
|
1594
|
+
if (extractedTokens !== null) {
|
|
1595
|
+
const tokenGroupCount = Object.keys(extractedTokens).length;
|
|
1596
|
+
process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
|
|
1597
|
+
`);
|
|
1598
|
+
}
|
|
1599
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
|
|
1261
1600
|
created.push(tokPath);
|
|
1262
1601
|
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1263
1602
|
created.push(outDirPath);
|
|
@@ -1268,7 +1607,28 @@ async function runInit(options) {
|
|
|
1268
1607
|
process.stdout.write(` ${p}
|
|
1269
1608
|
`);
|
|
1270
1609
|
}
|
|
1271
|
-
process.stdout.write("\n
|
|
1610
|
+
process.stdout.write("\n Scanning components...\n");
|
|
1611
|
+
try {
|
|
1612
|
+
const manifestConfig = {
|
|
1613
|
+
include: config.components.include,
|
|
1614
|
+
rootDir
|
|
1615
|
+
};
|
|
1616
|
+
const manifest$1 = await manifest.generateManifest(manifestConfig);
|
|
1617
|
+
const manifestCount = Object.keys(manifest$1.components).length;
|
|
1618
|
+
const manifestOutPath = path.join(rootDir, config.output.dir, "manifest.json");
|
|
1619
|
+
fs.mkdirSync(path.join(rootDir, config.output.dir), { recursive: true });
|
|
1620
|
+
fs.writeFileSync(manifestOutPath, `${JSON.stringify(manifest$1, null, 2)}
|
|
1621
|
+
`);
|
|
1622
|
+
process.stdout.write(
|
|
1623
|
+
` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
|
|
1624
|
+
`
|
|
1625
|
+
);
|
|
1626
|
+
} catch {
|
|
1627
|
+
process.stdout.write(
|
|
1628
|
+
" (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
process.stdout.write("\n");
|
|
1272
1632
|
return {
|
|
1273
1633
|
success: true,
|
|
1274
1634
|
message: "Project initialised successfully.",
|
|
@@ -1357,7 +1717,10 @@ Available: ${available}${hint}`
|
|
|
1357
1717
|
});
|
|
1358
1718
|
}
|
|
1359
1719
|
function registerQuery(manifestCmd) {
|
|
1360
|
-
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(
|
|
1720
|
+
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(
|
|
1721
|
+
"--has-prop <spec>",
|
|
1722
|
+
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1723
|
+
).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(
|
|
1361
1724
|
(opts) => {
|
|
1362
1725
|
try {
|
|
1363
1726
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1368,9 +1731,11 @@ function registerQuery(manifestCmd) {
|
|
|
1368
1731
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
1369
1732
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
1370
1733
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1734
|
+
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1735
|
+
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1371
1736
|
if (queryParts.length === 0) {
|
|
1372
1737
|
process.stderr.write(
|
|
1373
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects,
|
|
1738
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
|
|
1374
1739
|
);
|
|
1375
1740
|
process.exit(1);
|
|
1376
1741
|
}
|
|
@@ -1397,6 +1762,27 @@ function registerQuery(manifestCmd) {
|
|
|
1397
1762
|
if (opts.hasFetch) {
|
|
1398
1763
|
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1399
1764
|
}
|
|
1765
|
+
if (opts.hasProp !== void 0) {
|
|
1766
|
+
const spec = opts.hasProp;
|
|
1767
|
+
const colonIdx = spec.indexOf(":");
|
|
1768
|
+
const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
|
|
1769
|
+
const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
|
|
1770
|
+
entries = entries.filter(([, d]) => {
|
|
1771
|
+
const props = d.props;
|
|
1772
|
+
if (!props || !(propName in props)) return false;
|
|
1773
|
+
if (propType !== void 0) {
|
|
1774
|
+
return props[propName]?.type === propType;
|
|
1775
|
+
}
|
|
1776
|
+
return true;
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (opts.composedBy !== void 0) {
|
|
1780
|
+
const targetName = opts.composedBy;
|
|
1781
|
+
entries = entries.filter(([, d]) => {
|
|
1782
|
+
const composedBy = d.composedBy;
|
|
1783
|
+
return composedBy !== void 0 && composedBy.includes(targetName);
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1400
1786
|
const rows = entries.map(([name, d]) => ({
|
|
1401
1787
|
name,
|
|
1402
1788
|
file: d.filePath,
|
|
@@ -3081,6 +3467,17 @@ ${msg}`);
|
|
|
3081
3467
|
}
|
|
3082
3468
|
|
|
3083
3469
|
// src/render-commands.ts
|
|
3470
|
+
function loadGlobalCssFilesFromConfig(cwd) {
|
|
3471
|
+
const configPath = path.resolve(cwd, "reactscope.config.json");
|
|
3472
|
+
if (!fs.existsSync(configPath)) return [];
|
|
3473
|
+
try {
|
|
3474
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
3475
|
+
const cfg = JSON.parse(raw);
|
|
3476
|
+
return cfg.components?.wrappers?.globalCSS ?? [];
|
|
3477
|
+
} catch {
|
|
3478
|
+
return [];
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3084
3481
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
3085
3482
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
3086
3483
|
var _pool3 = null;
|
|
@@ -3101,7 +3498,7 @@ async function shutdownPool3() {
|
|
|
3101
3498
|
_pool3 = null;
|
|
3102
3499
|
}
|
|
3103
3500
|
}
|
|
3104
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
|
|
3501
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
|
|
3105
3502
|
const satori = new render.SatoriRenderer({
|
|
3106
3503
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
3107
3504
|
});
|
|
@@ -3110,13 +3507,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3110
3507
|
async renderCell(props, _complexityClass) {
|
|
3111
3508
|
const startMs = performance.now();
|
|
3112
3509
|
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
3510
|
+
const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
|
|
3113
3511
|
const htmlHarness = await buildComponentHarness(
|
|
3114
3512
|
filePath,
|
|
3115
3513
|
componentName,
|
|
3116
3514
|
props,
|
|
3117
3515
|
viewportWidth,
|
|
3118
|
-
void 0,
|
|
3119
|
-
// projectCss (handled separately)
|
|
3516
|
+
projectCss ?? void 0,
|
|
3120
3517
|
wrapperScript
|
|
3121
3518
|
);
|
|
3122
3519
|
const slot = await pool.acquire();
|
|
@@ -3145,10 +3542,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3145
3542
|
}
|
|
3146
3543
|
});
|
|
3147
3544
|
return [...set];
|
|
3148
|
-
});
|
|
3149
|
-
const
|
|
3150
|
-
if (
|
|
3151
|
-
await page.addStyleTag({ content:
|
|
3545
|
+
}) ?? [];
|
|
3546
|
+
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3547
|
+
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3548
|
+
await page.addStyleTag({ content: projectCss2 });
|
|
3152
3549
|
}
|
|
3153
3550
|
const renderTimeMs = performance.now() - startMs;
|
|
3154
3551
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
@@ -3158,49 +3555,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3158
3555
|
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3159
3556
|
);
|
|
3160
3557
|
}
|
|
3161
|
-
const PAD =
|
|
3162
|
-
const MIN_W = 320;
|
|
3163
|
-
const MIN_H = 200;
|
|
3558
|
+
const PAD = 8;
|
|
3164
3559
|
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3165
3560
|
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3166
3561
|
const rawW = boundingBox.width + PAD * 2;
|
|
3167
3562
|
const rawH = boundingBox.height + PAD * 2;
|
|
3168
|
-
const
|
|
3169
|
-
const
|
|
3170
|
-
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3171
|
-
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3563
|
+
const safeW = Math.min(rawW, viewportWidth - clipX);
|
|
3564
|
+
const safeH = Math.min(rawH, viewportHeight - clipY);
|
|
3172
3565
|
const screenshot = await page.screenshot({
|
|
3173
3566
|
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3174
3567
|
type: "png"
|
|
3175
3568
|
});
|
|
3569
|
+
const STYLE_PROPS = [
|
|
3570
|
+
"display",
|
|
3571
|
+
"width",
|
|
3572
|
+
"height",
|
|
3573
|
+
"color",
|
|
3574
|
+
"backgroundColor",
|
|
3575
|
+
"fontSize",
|
|
3576
|
+
"fontFamily",
|
|
3577
|
+
"fontWeight",
|
|
3578
|
+
"lineHeight",
|
|
3579
|
+
"padding",
|
|
3580
|
+
"paddingTop",
|
|
3581
|
+
"paddingRight",
|
|
3582
|
+
"paddingBottom",
|
|
3583
|
+
"paddingLeft",
|
|
3584
|
+
"margin",
|
|
3585
|
+
"marginTop",
|
|
3586
|
+
"marginRight",
|
|
3587
|
+
"marginBottom",
|
|
3588
|
+
"marginLeft",
|
|
3589
|
+
"gap",
|
|
3590
|
+
"borderRadius",
|
|
3591
|
+
"borderWidth",
|
|
3592
|
+
"borderColor",
|
|
3593
|
+
"borderStyle",
|
|
3594
|
+
"boxShadow",
|
|
3595
|
+
"opacity",
|
|
3596
|
+
"position",
|
|
3597
|
+
"flexDirection",
|
|
3598
|
+
"alignItems",
|
|
3599
|
+
"justifyContent",
|
|
3600
|
+
"overflow"
|
|
3601
|
+
];
|
|
3602
|
+
const _domResult = await page.evaluate(
|
|
3603
|
+
(args) => {
|
|
3604
|
+
let count = 0;
|
|
3605
|
+
const styles = {};
|
|
3606
|
+
function captureStyles(el, id, propList) {
|
|
3607
|
+
const computed = window.getComputedStyle(el);
|
|
3608
|
+
const out = {};
|
|
3609
|
+
for (const prop of propList) {
|
|
3610
|
+
const val = computed[prop] ?? "";
|
|
3611
|
+
if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
|
|
3612
|
+
}
|
|
3613
|
+
styles[id] = out;
|
|
3614
|
+
}
|
|
3615
|
+
function walk(node) {
|
|
3616
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3617
|
+
return {
|
|
3618
|
+
tag: "#text",
|
|
3619
|
+
attrs: {},
|
|
3620
|
+
text: node.textContent?.trim() ?? "",
|
|
3621
|
+
children: []
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
const el = node;
|
|
3625
|
+
const id = count++;
|
|
3626
|
+
captureStyles(el, id, args.props);
|
|
3627
|
+
const attrs = {};
|
|
3628
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3629
|
+
attrs[attr.name] = attr.value;
|
|
3630
|
+
}
|
|
3631
|
+
const children = Array.from(el.childNodes).filter(
|
|
3632
|
+
(n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
|
|
3633
|
+
).map(walk);
|
|
3634
|
+
return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
|
|
3635
|
+
}
|
|
3636
|
+
const root = document.querySelector(args.sel);
|
|
3637
|
+
if (!root)
|
|
3638
|
+
return {
|
|
3639
|
+
tree: { tag: "div", attrs: {}, children: [] },
|
|
3640
|
+
elementCount: 0,
|
|
3641
|
+
nodeStyles: {}
|
|
3642
|
+
};
|
|
3643
|
+
return { tree: walk(root), elementCount: count, nodeStyles: styles };
|
|
3644
|
+
},
|
|
3645
|
+
{ sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
|
|
3646
|
+
);
|
|
3647
|
+
const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
|
|
3648
|
+
const elementCount = _domResult?.elementCount ?? 0;
|
|
3649
|
+
const nodeStyles = _domResult?.nodeStyles ?? {};
|
|
3176
3650
|
const computedStyles = {};
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
"fontFamily",
|
|
3190
|
-
"padding",
|
|
3191
|
-
"margin"
|
|
3192
|
-
]) {
|
|
3193
|
-
out[prop] = computed.getPropertyValue(prop);
|
|
3651
|
+
if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
|
|
3652
|
+
for (const [nodeId, styles] of Object.entries(nodeStyles)) {
|
|
3653
|
+
computedStyles[`#node-${nodeId}`] = styles;
|
|
3654
|
+
}
|
|
3655
|
+
const dom = {
|
|
3656
|
+
tree: domTree,
|
|
3657
|
+
elementCount,
|
|
3658
|
+
boundingBox: {
|
|
3659
|
+
x: boundingBox.x,
|
|
3660
|
+
y: boundingBox.y,
|
|
3661
|
+
width: boundingBox.width,
|
|
3662
|
+
height: boundingBox.height
|
|
3194
3663
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3664
|
+
};
|
|
3665
|
+
const a11yInfo = await page.evaluate((sel) => {
|
|
3666
|
+
const wrapper = document.querySelector(sel);
|
|
3667
|
+
const el = wrapper?.firstElementChild ?? wrapper;
|
|
3668
|
+
if (!el) return { role: "generic", name: "" };
|
|
3669
|
+
return {
|
|
3670
|
+
role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
|
|
3671
|
+
name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
|
|
3672
|
+
};
|
|
3673
|
+
}, "[data-reactscope-root]") ?? {
|
|
3674
|
+
role: "generic",
|
|
3675
|
+
name: ""
|
|
3676
|
+
};
|
|
3677
|
+
const imgViolations = await page.evaluate((sel) => {
|
|
3678
|
+
const container = document.querySelector(sel);
|
|
3679
|
+
if (!container) return [];
|
|
3680
|
+
const issues = [];
|
|
3681
|
+
container.querySelectorAll("img").forEach((img) => {
|
|
3682
|
+
if (!img.alt) issues.push("Image missing accessible name");
|
|
3683
|
+
});
|
|
3684
|
+
return issues;
|
|
3685
|
+
}, "[data-reactscope-root]") ?? [];
|
|
3686
|
+
const accessibility = {
|
|
3687
|
+
role: a11yInfo.role,
|
|
3688
|
+
name: a11yInfo.name,
|
|
3689
|
+
violations: imgViolations
|
|
3690
|
+
};
|
|
3198
3691
|
return {
|
|
3199
3692
|
screenshot,
|
|
3200
3693
|
width: Math.round(safeW),
|
|
3201
3694
|
height: Math.round(safeH),
|
|
3202
3695
|
renderTimeMs,
|
|
3203
|
-
computedStyles
|
|
3696
|
+
computedStyles,
|
|
3697
|
+
dom,
|
|
3698
|
+
accessibility
|
|
3204
3699
|
};
|
|
3205
3700
|
} finally {
|
|
3206
3701
|
pool.release(slot);
|
|
@@ -3250,26 +3745,64 @@ function registerRenderSingle(renderCmd) {
|
|
|
3250
3745
|
Available: ${available}`
|
|
3251
3746
|
);
|
|
3252
3747
|
}
|
|
3748
|
+
let props = {};
|
|
3749
|
+
if (opts.props !== void 0) {
|
|
3750
|
+
try {
|
|
3751
|
+
props = JSON.parse(opts.props);
|
|
3752
|
+
} catch {
|
|
3753
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
if (descriptor.props !== void 0) {
|
|
3757
|
+
const propDefs = descriptor.props;
|
|
3758
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
3759
|
+
if (propName in props) continue;
|
|
3760
|
+
if (!propDef.required && propDef.default !== void 0) continue;
|
|
3761
|
+
if (propDef.type === "node" || propDef.type === "string") {
|
|
3762
|
+
props[propName] = propName === "children" ? componentName : propName;
|
|
3763
|
+
} else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
|
|
3764
|
+
props[propName] = propDef.values[0];
|
|
3765
|
+
} else if (propDef.type === "boolean") {
|
|
3766
|
+
props[propName] = false;
|
|
3767
|
+
} else if (propDef.type === "number") {
|
|
3768
|
+
props[propName] = 0;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3253
3772
|
const { width, height } = parseViewport(opts.viewport);
|
|
3254
3773
|
const rootDir = process.cwd();
|
|
3255
3774
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3256
3775
|
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3257
3776
|
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3258
3777
|
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3259
|
-
const
|
|
3778
|
+
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3779
|
+
if (globalCssFiles.length === 0) {
|
|
3780
|
+
process.stderr.write(
|
|
3781
|
+
"warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
|
|
3782
|
+
);
|
|
3783
|
+
}
|
|
3784
|
+
const renderer = buildRenderer(
|
|
3785
|
+
filePath,
|
|
3786
|
+
componentName,
|
|
3787
|
+
width,
|
|
3788
|
+
height,
|
|
3789
|
+
globalCssFiles,
|
|
3790
|
+
rootDir,
|
|
3791
|
+
wrapperScript
|
|
3792
|
+
);
|
|
3260
3793
|
process.stderr.write(
|
|
3261
3794
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3262
3795
|
`
|
|
3263
3796
|
);
|
|
3264
3797
|
const fmt2 = resolveSingleFormat(opts.format);
|
|
3265
3798
|
let anyFailed = false;
|
|
3266
|
-
for (const [scenarioName,
|
|
3799
|
+
for (const [scenarioName, props2] of Object.entries(scenarios)) {
|
|
3267
3800
|
const isNamed = scenarioName !== "__default__";
|
|
3268
3801
|
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3269
3802
|
const outcome = await render.safeRender(
|
|
3270
|
-
() => renderer.renderCell(
|
|
3803
|
+
() => renderer.renderCell(props2, descriptor.complexityClass),
|
|
3271
3804
|
{
|
|
3272
|
-
props,
|
|
3805
|
+
props: props2,
|
|
3273
3806
|
sourceLocation: {
|
|
3274
3807
|
file: descriptor.filePath,
|
|
3275
3808
|
line: descriptor.loc.start,
|
|
@@ -3298,7 +3831,7 @@ Available: ${available}`
|
|
|
3298
3831
|
`
|
|
3299
3832
|
);
|
|
3300
3833
|
} else if (fmt2 === "json") {
|
|
3301
|
-
const json = formatRenderJson(label,
|
|
3834
|
+
const json = formatRenderJson(label, props2, result);
|
|
3302
3835
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3303
3836
|
`);
|
|
3304
3837
|
} else {
|
|
@@ -3325,7 +3858,10 @@ Available: ${available}`
|
|
|
3325
3858
|
);
|
|
3326
3859
|
}
|
|
3327
3860
|
function registerRenderMatrix(renderCmd) {
|
|
3328
|
-
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3861
|
+
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3862
|
+
"--axes <spec>",
|
|
3863
|
+
`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"]}'`
|
|
3864
|
+
).option(
|
|
3329
3865
|
"--contexts <ids>",
|
|
3330
3866
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
3331
3867
|
).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(
|
|
@@ -3344,21 +3880,47 @@ Available: ${available}`
|
|
|
3344
3880
|
const { width, height } = { width: 375, height: 812 };
|
|
3345
3881
|
const rootDir = process.cwd();
|
|
3346
3882
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3347
|
-
const
|
|
3883
|
+
const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3884
|
+
const renderer = buildRenderer(
|
|
3885
|
+
filePath,
|
|
3886
|
+
componentName,
|
|
3887
|
+
width,
|
|
3888
|
+
height,
|
|
3889
|
+
matrixCssFiles,
|
|
3890
|
+
rootDir
|
|
3891
|
+
);
|
|
3348
3892
|
const axes = [];
|
|
3349
3893
|
if (opts.axes !== void 0) {
|
|
3350
|
-
const
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3894
|
+
const axesRaw = opts.axes.trim();
|
|
3895
|
+
if (axesRaw.startsWith("{")) {
|
|
3896
|
+
let parsed;
|
|
3897
|
+
try {
|
|
3898
|
+
parsed = JSON.parse(axesRaw);
|
|
3899
|
+
} catch {
|
|
3900
|
+
throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
|
|
3901
|
+
}
|
|
3902
|
+
for (const [name, vals] of Object.entries(parsed)) {
|
|
3903
|
+
if (!Array.isArray(vals)) {
|
|
3904
|
+
throw new Error(`Axis "${name}" must be an array of values in JSON format`);
|
|
3905
|
+
}
|
|
3906
|
+
axes.push({ name, values: vals.map(String) });
|
|
3355
3907
|
}
|
|
3356
|
-
|
|
3357
|
-
const
|
|
3358
|
-
|
|
3359
|
-
|
|
3908
|
+
} else {
|
|
3909
|
+
const axisSpecs = axesRaw.split(/\s+/);
|
|
3910
|
+
for (const spec of axisSpecs) {
|
|
3911
|
+
const colonIdx = spec.indexOf(":");
|
|
3912
|
+
if (colonIdx < 0) {
|
|
3913
|
+
throw new Error(
|
|
3914
|
+
`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
const name = spec.slice(0, colonIdx);
|
|
3918
|
+
const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
|
|
3919
|
+
if (name.length === 0 || values.length === 0) {
|
|
3920
|
+
throw new Error(`Invalid axis spec "${spec}"`);
|
|
3921
|
+
}
|
|
3922
|
+
axes.push({ name, values });
|
|
3360
3923
|
}
|
|
3361
|
-
axes.push({ name, values });
|
|
3362
3924
|
}
|
|
3363
3925
|
}
|
|
3364
3926
|
if (opts.contexts !== void 0) {
|
|
@@ -3472,16 +4034,31 @@ function registerRenderAll(renderCmd) {
|
|
|
3472
4034
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
3473
4035
|
`);
|
|
3474
4036
|
const results = [];
|
|
4037
|
+
const complianceStylesMap = {};
|
|
3475
4038
|
let completed = 0;
|
|
3476
4039
|
const renderOne = async (name) => {
|
|
3477
4040
|
const descriptor = manifest.components[name];
|
|
3478
4041
|
if (descriptor === void 0) return;
|
|
3479
4042
|
const filePath = path.resolve(rootDir, descriptor.filePath);
|
|
3480
|
-
const
|
|
4043
|
+
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
4044
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
4045
|
+
const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
|
|
4046
|
+
const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
|
|
4047
|
+
const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
|
|
4048
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
4049
|
+
const renderer = buildRenderer(
|
|
4050
|
+
filePath,
|
|
4051
|
+
name,
|
|
4052
|
+
375,
|
|
4053
|
+
812,
|
|
4054
|
+
allCssFiles,
|
|
4055
|
+
process.cwd(),
|
|
4056
|
+
wrapperScript
|
|
4057
|
+
);
|
|
3481
4058
|
const outcome = await render.safeRender(
|
|
3482
|
-
() => renderer.renderCell(
|
|
4059
|
+
() => renderer.renderCell(renderProps, descriptor.complexityClass),
|
|
3483
4060
|
{
|
|
3484
|
-
props:
|
|
4061
|
+
props: renderProps,
|
|
3485
4062
|
sourceLocation: {
|
|
3486
4063
|
file: descriptor.filePath,
|
|
3487
4064
|
line: descriptor.loc.start,
|
|
@@ -3521,6 +4098,77 @@ function registerRenderAll(renderCmd) {
|
|
|
3521
4098
|
fs.writeFileSync(pngPath, result.screenshot);
|
|
3522
4099
|
const jsonPath = path.resolve(outputDir, `${name}.json`);
|
|
3523
4100
|
fs.writeFileSync(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
4101
|
+
const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
|
|
4102
|
+
const compStyles = {
|
|
4103
|
+
colors: {},
|
|
4104
|
+
spacing: {},
|
|
4105
|
+
typography: {},
|
|
4106
|
+
borders: {},
|
|
4107
|
+
shadows: {}
|
|
4108
|
+
};
|
|
4109
|
+
for (const [prop, val] of Object.entries(rawStyles)) {
|
|
4110
|
+
if (!val || val === "none" || val === "") continue;
|
|
4111
|
+
const lower = prop.toLowerCase();
|
|
4112
|
+
if (lower.includes("color") || lower.includes("background")) {
|
|
4113
|
+
compStyles.colors[prop] = val;
|
|
4114
|
+
} else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
|
|
4115
|
+
compStyles.spacing[prop] = val;
|
|
4116
|
+
} else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
|
|
4117
|
+
compStyles.typography[prop] = val;
|
|
4118
|
+
} else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
|
|
4119
|
+
compStyles.borders[prop] = val;
|
|
4120
|
+
} else if (lower.includes("shadow")) {
|
|
4121
|
+
compStyles.shadows[prop] = val;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
complianceStylesMap[name] = compStyles;
|
|
4125
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
|
|
4126
|
+
try {
|
|
4127
|
+
const scenarioEntries2 = Object.entries(scopeData.scenarios);
|
|
4128
|
+
const scenarioAxis = {
|
|
4129
|
+
name: "scenario",
|
|
4130
|
+
values: scenarioEntries2.map(([k]) => k)
|
|
4131
|
+
};
|
|
4132
|
+
const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
|
|
4133
|
+
const matrixRenderer = buildRenderer(
|
|
4134
|
+
filePath,
|
|
4135
|
+
name,
|
|
4136
|
+
375,
|
|
4137
|
+
812,
|
|
4138
|
+
allCssFiles,
|
|
4139
|
+
process.cwd(),
|
|
4140
|
+
wrapperScript
|
|
4141
|
+
);
|
|
4142
|
+
const wrappedRenderer = {
|
|
4143
|
+
_satori: matrixRenderer._satori,
|
|
4144
|
+
async renderCell(props, cc) {
|
|
4145
|
+
const scenarioName = props.scenario;
|
|
4146
|
+
const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
|
|
4147
|
+
return matrixRenderer.renderCell(realProps, cc ?? "simple");
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
const matrix = new render.RenderMatrix(wrappedRenderer, [scenarioAxis], {
|
|
4151
|
+
concurrency: 2
|
|
4152
|
+
});
|
|
4153
|
+
const matrixResult = await matrix.render();
|
|
4154
|
+
const matrixCells = matrixResult.cells.map((cell) => ({
|
|
4155
|
+
axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
|
|
4156
|
+
screenshot: cell.result.screenshot.toString("base64"),
|
|
4157
|
+
width: cell.result.width,
|
|
4158
|
+
height: cell.result.height,
|
|
4159
|
+
renderTimeMs: cell.result.renderTimeMs
|
|
4160
|
+
}));
|
|
4161
|
+
const existingJson = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
4162
|
+
existingJson.cells = matrixCells;
|
|
4163
|
+
existingJson.axisLabels = [scenarioAxis.values];
|
|
4164
|
+
fs.writeFileSync(jsonPath, JSON.stringify(existingJson, null, 2));
|
|
4165
|
+
} catch (matrixErr) {
|
|
4166
|
+
process.stderr.write(
|
|
4167
|
+
` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
|
|
4168
|
+
`
|
|
4169
|
+
);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
3524
4172
|
if (isTTY()) {
|
|
3525
4173
|
process.stdout.write(
|
|
3526
4174
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -3544,6 +4192,14 @@ function registerRenderAll(renderCmd) {
|
|
|
3544
4192
|
}
|
|
3545
4193
|
await Promise.all(workers);
|
|
3546
4194
|
await shutdownPool3();
|
|
4195
|
+
const compStylesPath = path.resolve(
|
|
4196
|
+
path.resolve(process.cwd(), opts.outputDir),
|
|
4197
|
+
"..",
|
|
4198
|
+
"compliance-styles.json"
|
|
4199
|
+
);
|
|
4200
|
+
fs.writeFileSync(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
|
|
4201
|
+
process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
|
|
4202
|
+
`);
|
|
3547
4203
|
process.stderr.write("\n");
|
|
3548
4204
|
const summary = formatSummaryText(results, outputDir);
|
|
3549
4205
|
process.stderr.write(`${summary}
|
|
@@ -3743,12 +4399,12 @@ async function runBaseline(options = {}) {
|
|
|
3743
4399
|
fs.mkdirSync(rendersDir, { recursive: true });
|
|
3744
4400
|
let manifest$1;
|
|
3745
4401
|
if (manifestPath !== void 0) {
|
|
3746
|
-
const { readFileSync:
|
|
4402
|
+
const { readFileSync: readFileSync14 } = await import('fs');
|
|
3747
4403
|
const absPath = path.resolve(rootDir, manifestPath);
|
|
3748
4404
|
if (!fs.existsSync(absPath)) {
|
|
3749
4405
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3750
4406
|
}
|
|
3751
|
-
manifest$1 = JSON.parse(
|
|
4407
|
+
manifest$1 = JSON.parse(readFileSync14(absPath, "utf-8"));
|
|
3752
4408
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3753
4409
|
`);
|
|
3754
4410
|
} else {
|
|
@@ -5109,10 +5765,20 @@ function createTokensExportCommand() {
|
|
|
5109
5765
|
).action(
|
|
5110
5766
|
(opts) => {
|
|
5111
5767
|
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
5768
|
+
const FORMAT_ALIASES = {
|
|
5769
|
+
json: "flat-json",
|
|
5770
|
+
"json-flat": "flat-json",
|
|
5771
|
+
javascript: "ts",
|
|
5772
|
+
js: "ts",
|
|
5773
|
+
sass: "scss",
|
|
5774
|
+
tw: "tailwind"
|
|
5775
|
+
};
|
|
5776
|
+
const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
|
|
5112
5777
|
process.stderr.write(
|
|
5113
5778
|
`Error: unsupported format "${opts.format}".
|
|
5114
5779
|
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
5115
|
-
`
|
|
5780
|
+
` + (hint ? `Did you mean "${hint}"?
|
|
5781
|
+
` : "")
|
|
5116
5782
|
);
|
|
5117
5783
|
process.exit(1);
|
|
5118
5784
|
}
|
|
@@ -5926,6 +6592,7 @@ function createProgram(options = {}) {
|
|
|
5926
6592
|
program.addCommand(createInstrumentCommand());
|
|
5927
6593
|
program.addCommand(createInitCommand());
|
|
5928
6594
|
program.addCommand(createCiCommand());
|
|
6595
|
+
program.addCommand(createDoctorCommand());
|
|
5929
6596
|
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
5930
6597
|
if (existingReportCmd !== void 0) {
|
|
5931
6598
|
registerBaselineSubCommand(existingReportCmd);
|
|
@@ -5938,6 +6605,7 @@ function createProgram(options = {}) {
|
|
|
5938
6605
|
|
|
5939
6606
|
exports.CI_EXIT = CI_EXIT;
|
|
5940
6607
|
exports.createCiCommand = createCiCommand;
|
|
6608
|
+
exports.createDoctorCommand = createDoctorCommand;
|
|
5941
6609
|
exports.createInitCommand = createInitCommand;
|
|
5942
6610
|
exports.createInstrumentCommand = createInstrumentCommand;
|
|
5943
6611
|
exports.createManifestCommand = createManifestCommand;
|