@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync,
|
|
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((
|
|
1353
|
+
return new Promise((resolve19) => {
|
|
1121
1354
|
rl.question(question, (answer) => {
|
|
1122
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
3126
|
-
if (
|
|
3127
|
-
await page.addStyleTag({ content:
|
|
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 =
|
|
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
|
|
3145
|
-
const
|
|
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
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
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
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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
|
-
|
|
3333
|
-
const
|
|
3334
|
-
|
|
3335
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
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
|