@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/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command11 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -141,6 +141,21 @@ import { createElement } from "react";
|
|
|
141
141
|
// Suppress "React must be in scope" warnings from old JSX (we use automatic)
|
|
142
142
|
banner: {
|
|
143
143
|
js: "/* @agent-scope/cli component harness */"
|
|
144
|
+
},
|
|
145
|
+
// CSS imports (e.g. `import './styles.css'`) are handled at the page level via
|
|
146
|
+
// globalCSS injection. Tell esbuild to treat CSS files as empty modules so
|
|
147
|
+
// components that import CSS directly (e.g. App.tsx) don't error during bundling.
|
|
148
|
+
loader: {
|
|
149
|
+
".css": "empty",
|
|
150
|
+
".svg": "dataurl",
|
|
151
|
+
".png": "dataurl",
|
|
152
|
+
".jpg": "dataurl",
|
|
153
|
+
".jpeg": "dataurl",
|
|
154
|
+
".gif": "dataurl",
|
|
155
|
+
".webp": "dataurl",
|
|
156
|
+
".ttf": "dataurl",
|
|
157
|
+
".woff": "dataurl",
|
|
158
|
+
".woff2": "dataurl"
|
|
144
159
|
}
|
|
145
160
|
});
|
|
146
161
|
if (result.errors.length > 0) {
|
|
@@ -553,6 +568,57 @@ async function getCompiledCssForClasses(cwd, classes) {
|
|
|
553
568
|
if (deduped.length === 0) return null;
|
|
554
569
|
return build3(deduped);
|
|
555
570
|
}
|
|
571
|
+
async function compileGlobalCssFile(cssFilePath, cwd) {
|
|
572
|
+
const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import("fs");
|
|
573
|
+
const { createRequire: createRequire3 } = await import("module");
|
|
574
|
+
if (!existsSync16(cssFilePath)) return null;
|
|
575
|
+
const raw = readFileSync14(cssFilePath, "utf-8");
|
|
576
|
+
const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
|
|
577
|
+
if (!needsCompile) {
|
|
578
|
+
return raw;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const require2 = createRequire3(resolve(cwd, "package.json"));
|
|
582
|
+
let postcss;
|
|
583
|
+
let twPlugin;
|
|
584
|
+
try {
|
|
585
|
+
postcss = require2("postcss");
|
|
586
|
+
twPlugin = require2("tailwindcss");
|
|
587
|
+
} catch {
|
|
588
|
+
return raw;
|
|
589
|
+
}
|
|
590
|
+
let autoprefixerPlugin;
|
|
591
|
+
try {
|
|
592
|
+
autoprefixerPlugin = require2("autoprefixer");
|
|
593
|
+
} catch {
|
|
594
|
+
autoprefixerPlugin = null;
|
|
595
|
+
}
|
|
596
|
+
const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
|
|
597
|
+
const result = await postcss(plugins).process(raw, {
|
|
598
|
+
from: cssFilePath,
|
|
599
|
+
to: cssFilePath
|
|
600
|
+
});
|
|
601
|
+
return result.css;
|
|
602
|
+
} catch (err) {
|
|
603
|
+
process.stderr.write(
|
|
604
|
+
`[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
|
|
605
|
+
`
|
|
606
|
+
);
|
|
607
|
+
return raw;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function loadGlobalCss(globalCssFiles, cwd) {
|
|
611
|
+
if (globalCssFiles.length === 0) return null;
|
|
612
|
+
const parts = [];
|
|
613
|
+
for (const relPath of globalCssFiles) {
|
|
614
|
+
const absPath = resolve(cwd, relPath);
|
|
615
|
+
const css = await compileGlobalCssFile(absPath, cwd);
|
|
616
|
+
if (css !== null && css.trim().length > 0) {
|
|
617
|
+
parts.push(css);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
621
|
+
}
|
|
556
622
|
|
|
557
623
|
// src/ci/commands.ts
|
|
558
624
|
var CI_EXIT = {
|
|
@@ -976,18 +1042,174 @@ function createCiCommand() {
|
|
|
976
1042
|
);
|
|
977
1043
|
}
|
|
978
1044
|
|
|
1045
|
+
// src/doctor-commands.ts
|
|
1046
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
1047
|
+
import { join, resolve as resolve3 } from "path";
|
|
1048
|
+
import { Command as Command2 } from "commander";
|
|
1049
|
+
function collectSourceFiles(dir) {
|
|
1050
|
+
if (!existsSync3(dir)) return [];
|
|
1051
|
+
const results = [];
|
|
1052
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1053
|
+
const full = join(dir, entry.name);
|
|
1054
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
|
|
1055
|
+
results.push(...collectSourceFiles(full));
|
|
1056
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
1057
|
+
results.push(full);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return results;
|
|
1061
|
+
}
|
|
1062
|
+
function checkConfig(cwd) {
|
|
1063
|
+
const configPath = resolve3(cwd, "reactscope.config.json");
|
|
1064
|
+
if (!existsSync3(configPath)) {
|
|
1065
|
+
return {
|
|
1066
|
+
name: "config",
|
|
1067
|
+
status: "error",
|
|
1068
|
+
message: "reactscope.config.json not found \u2014 run `scope init`"
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1073
|
+
return { name: "config", status: "ok", message: "reactscope.config.json valid" };
|
|
1074
|
+
} catch {
|
|
1075
|
+
return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function checkTokens(cwd) {
|
|
1079
|
+
const configPath = resolve3(cwd, "reactscope.config.json");
|
|
1080
|
+
let tokensPath = resolve3(cwd, "reactscope.tokens.json");
|
|
1081
|
+
if (existsSync3(configPath)) {
|
|
1082
|
+
try {
|
|
1083
|
+
const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1084
|
+
if (cfg.tokens?.file) tokensPath = resolve3(cwd, cfg.tokens.file);
|
|
1085
|
+
} catch {
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (!existsSync3(tokensPath)) {
|
|
1089
|
+
return {
|
|
1090
|
+
name: "tokens",
|
|
1091
|
+
status: "warn",
|
|
1092
|
+
message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
const raw = JSON.parse(readFileSync3(tokensPath, "utf-8"));
|
|
1097
|
+
if (!raw.version) {
|
|
1098
|
+
return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
|
|
1099
|
+
}
|
|
1100
|
+
return { name: "tokens", status: "ok", message: "Token file valid" };
|
|
1101
|
+
} catch {
|
|
1102
|
+
return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function checkGlobalCss(cwd) {
|
|
1106
|
+
const configPath = resolve3(cwd, "reactscope.config.json");
|
|
1107
|
+
let globalCss = [];
|
|
1108
|
+
if (existsSync3(configPath)) {
|
|
1109
|
+
try {
|
|
1110
|
+
const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1111
|
+
globalCss = cfg.components?.wrappers?.globalCSS ?? [];
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (globalCss.length === 0) {
|
|
1116
|
+
return {
|
|
1117
|
+
name: "globalCSS",
|
|
1118
|
+
status: "warn",
|
|
1119
|
+
message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
const missing = globalCss.filter((f) => !existsSync3(resolve3(cwd, f)));
|
|
1123
|
+
if (missing.length > 0) {
|
|
1124
|
+
return {
|
|
1125
|
+
name: "globalCSS",
|
|
1126
|
+
status: "error",
|
|
1127
|
+
message: `globalCSS file(s) not found: ${missing.join(", ")}`
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
name: "globalCSS",
|
|
1132
|
+
status: "ok",
|
|
1133
|
+
message: `${globalCss.length} globalCSS file(s) present`
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function checkManifest(cwd) {
|
|
1137
|
+
const manifestPath = resolve3(cwd, ".reactscope", "manifest.json");
|
|
1138
|
+
if (!existsSync3(manifestPath)) {
|
|
1139
|
+
return {
|
|
1140
|
+
name: "manifest",
|
|
1141
|
+
status: "warn",
|
|
1142
|
+
message: "Manifest not found \u2014 run `scope manifest generate`"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const manifestMtime = statSync(manifestPath).mtimeMs;
|
|
1146
|
+
const sourceDir = resolve3(cwd, "src");
|
|
1147
|
+
const sourceFiles = collectSourceFiles(sourceDir);
|
|
1148
|
+
const stale = sourceFiles.filter((f) => statSync(f).mtimeMs > manifestMtime);
|
|
1149
|
+
if (stale.length > 0) {
|
|
1150
|
+
return {
|
|
1151
|
+
name: "manifest",
|
|
1152
|
+
status: "warn",
|
|
1153
|
+
message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
|
|
1157
|
+
}
|
|
1158
|
+
var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
|
|
1159
|
+
function formatCheck(check) {
|
|
1160
|
+
return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
|
|
1161
|
+
}
|
|
1162
|
+
function createDoctorCommand() {
|
|
1163
|
+
return new Command2("doctor").description("Check the health of your Scope setup (config, tokens, CSS, manifest)").option("--json", "Emit structured JSON output", false).action((opts) => {
|
|
1164
|
+
const cwd = process.cwd();
|
|
1165
|
+
const checks = [
|
|
1166
|
+
checkConfig(cwd),
|
|
1167
|
+
checkTokens(cwd),
|
|
1168
|
+
checkGlobalCss(cwd),
|
|
1169
|
+
checkManifest(cwd)
|
|
1170
|
+
];
|
|
1171
|
+
const errors = checks.filter((c) => c.status === "error").length;
|
|
1172
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
1173
|
+
if (opts.json) {
|
|
1174
|
+
process.stdout.write(
|
|
1175
|
+
`${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
|
|
1176
|
+
`
|
|
1177
|
+
);
|
|
1178
|
+
if (errors > 0) process.exit(1);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
process.stdout.write("\nScope Doctor\n");
|
|
1182
|
+
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");
|
|
1183
|
+
for (const check of checks) process.stdout.write(`${formatCheck(check)}
|
|
1184
|
+
`);
|
|
1185
|
+
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");
|
|
1186
|
+
if (errors > 0) {
|
|
1187
|
+
process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
|
|
1188
|
+
|
|
1189
|
+
`);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
} else if (warnings > 0) {
|
|
1192
|
+
process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
|
|
1193
|
+
|
|
1194
|
+
`);
|
|
1195
|
+
} else {
|
|
1196
|
+
process.stdout.write(" All checks passed!\n\n");
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
979
1201
|
// src/init/index.ts
|
|
980
|
-
import { appendFileSync, existsSync as
|
|
981
|
-
import { join as
|
|
1202
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1203
|
+
import { join as join3 } from "path";
|
|
982
1204
|
import * as readline from "readline";
|
|
983
1205
|
|
|
984
1206
|
// src/init/detect.ts
|
|
985
|
-
import { existsSync as
|
|
986
|
-
import { join } from "path";
|
|
1207
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
1208
|
+
import { join as join2 } from "path";
|
|
987
1209
|
function hasConfigFile(dir, stem) {
|
|
988
|
-
if (!
|
|
1210
|
+
if (!existsSync4(dir)) return false;
|
|
989
1211
|
try {
|
|
990
|
-
const entries =
|
|
1212
|
+
const entries = readdirSync2(dir);
|
|
991
1213
|
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
992
1214
|
} catch {
|
|
993
1215
|
return false;
|
|
@@ -995,7 +1217,7 @@ function hasConfigFile(dir, stem) {
|
|
|
995
1217
|
}
|
|
996
1218
|
function readSafe(path) {
|
|
997
1219
|
try {
|
|
998
|
-
return
|
|
1220
|
+
return readFileSync4(path, "utf-8");
|
|
999
1221
|
} catch {
|
|
1000
1222
|
return null;
|
|
1001
1223
|
}
|
|
@@ -1008,15 +1230,15 @@ function detectFramework(rootDir, packageDeps) {
|
|
|
1008
1230
|
return "unknown";
|
|
1009
1231
|
}
|
|
1010
1232
|
function detectPackageManager(rootDir) {
|
|
1011
|
-
if (
|
|
1012
|
-
if (
|
|
1013
|
-
if (
|
|
1014
|
-
if (
|
|
1233
|
+
if (existsSync4(join2(rootDir, "bun.lock"))) return "bun";
|
|
1234
|
+
if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
1235
|
+
if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1236
|
+
if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
|
|
1015
1237
|
return "npm";
|
|
1016
1238
|
}
|
|
1017
1239
|
function detectTypeScript(rootDir) {
|
|
1018
|
-
const candidate =
|
|
1019
|
-
if (
|
|
1240
|
+
const candidate = join2(rootDir, "tsconfig.json");
|
|
1241
|
+
if (existsSync4(candidate)) {
|
|
1020
1242
|
return { typescript: true, tsconfigPath: candidate };
|
|
1021
1243
|
}
|
|
1022
1244
|
return { typescript: false, tsconfigPath: null };
|
|
@@ -1028,11 +1250,11 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1028
1250
|
const ext = typescript ? "tsx" : "jsx";
|
|
1029
1251
|
const altExt = typescript ? "jsx" : "jsx";
|
|
1030
1252
|
for (const dir of COMPONENT_DIRS) {
|
|
1031
|
-
const absDir =
|
|
1032
|
-
if (!
|
|
1253
|
+
const absDir = join2(rootDir, dir);
|
|
1254
|
+
if (!existsSync4(absDir)) continue;
|
|
1033
1255
|
let hasComponents = false;
|
|
1034
1256
|
try {
|
|
1035
|
-
const entries =
|
|
1257
|
+
const entries = readdirSync2(absDir, { withFileTypes: true });
|
|
1036
1258
|
hasComponents = entries.some(
|
|
1037
1259
|
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
1038
1260
|
);
|
|
@@ -1040,7 +1262,7 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1040
1262
|
hasComponents = entries.some(
|
|
1041
1263
|
(e) => e.isDirectory() && (() => {
|
|
1042
1264
|
try {
|
|
1043
|
-
return
|
|
1265
|
+
return readdirSync2(join2(absDir, e.name)).some(
|
|
1044
1266
|
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
1045
1267
|
);
|
|
1046
1268
|
} catch {
|
|
@@ -1065,6 +1287,20 @@ function detectComponentPatterns(rootDir, typescript) {
|
|
|
1065
1287
|
}
|
|
1066
1288
|
return unique;
|
|
1067
1289
|
}
|
|
1290
|
+
var GLOBAL_CSS_CANDIDATES = [
|
|
1291
|
+
"src/styles.css",
|
|
1292
|
+
"src/index.css",
|
|
1293
|
+
"src/global.css",
|
|
1294
|
+
"src/globals.css",
|
|
1295
|
+
"src/app.css",
|
|
1296
|
+
"src/main.css",
|
|
1297
|
+
"styles/globals.css",
|
|
1298
|
+
"styles/global.css",
|
|
1299
|
+
"styles/index.css"
|
|
1300
|
+
];
|
|
1301
|
+
function detectGlobalCSSFiles(rootDir) {
|
|
1302
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync4(join2(rootDir, rel)));
|
|
1303
|
+
}
|
|
1068
1304
|
var TAILWIND_STEMS = ["tailwind.config"];
|
|
1069
1305
|
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
1070
1306
|
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
@@ -1074,23 +1310,23 @@ function detectTokenSources(rootDir) {
|
|
|
1074
1310
|
for (const stem of TAILWIND_STEMS) {
|
|
1075
1311
|
if (hasConfigFile(rootDir, stem)) {
|
|
1076
1312
|
try {
|
|
1077
|
-
const entries =
|
|
1313
|
+
const entries = readdirSync2(rootDir);
|
|
1078
1314
|
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
1079
1315
|
if (match) {
|
|
1080
|
-
sources.push({ kind: "tailwind-config", path:
|
|
1316
|
+
sources.push({ kind: "tailwind-config", path: join2(rootDir, match) });
|
|
1081
1317
|
}
|
|
1082
1318
|
} catch {
|
|
1083
1319
|
}
|
|
1084
1320
|
}
|
|
1085
1321
|
}
|
|
1086
|
-
const srcDir =
|
|
1087
|
-
const dirsToScan =
|
|
1322
|
+
const srcDir = join2(rootDir, "src");
|
|
1323
|
+
const dirsToScan = existsSync4(srcDir) ? [srcDir] : [];
|
|
1088
1324
|
for (const scanDir of dirsToScan) {
|
|
1089
1325
|
try {
|
|
1090
|
-
const entries =
|
|
1326
|
+
const entries = readdirSync2(scanDir, { withFileTypes: true });
|
|
1091
1327
|
for (const entry of entries) {
|
|
1092
1328
|
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
1093
|
-
const filePath =
|
|
1329
|
+
const filePath = join2(scanDir, entry.name);
|
|
1094
1330
|
const content = readSafe(filePath);
|
|
1095
1331
|
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
1096
1332
|
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
@@ -1100,12 +1336,12 @@ function detectTokenSources(rootDir) {
|
|
|
1100
1336
|
} catch {
|
|
1101
1337
|
}
|
|
1102
1338
|
}
|
|
1103
|
-
if (
|
|
1339
|
+
if (existsSync4(srcDir)) {
|
|
1104
1340
|
try {
|
|
1105
|
-
const entries =
|
|
1341
|
+
const entries = readdirSync2(srcDir);
|
|
1106
1342
|
for (const entry of entries) {
|
|
1107
1343
|
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
1108
|
-
sources.push({ kind: "theme-file", path:
|
|
1344
|
+
sources.push({ kind: "theme-file", path: join2(srcDir, entry) });
|
|
1109
1345
|
}
|
|
1110
1346
|
}
|
|
1111
1347
|
} catch {
|
|
@@ -1114,7 +1350,7 @@ function detectTokenSources(rootDir) {
|
|
|
1114
1350
|
return sources;
|
|
1115
1351
|
}
|
|
1116
1352
|
function detectProject(rootDir) {
|
|
1117
|
-
const pkgPath =
|
|
1353
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
1118
1354
|
let packageDeps = {};
|
|
1119
1355
|
const pkgContent = readSafe(pkgPath);
|
|
1120
1356
|
if (pkgContent !== null) {
|
|
@@ -1132,25 +1368,28 @@ function detectProject(rootDir) {
|
|
|
1132
1368
|
const packageManager = detectPackageManager(rootDir);
|
|
1133
1369
|
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
1134
1370
|
const tokenSources = detectTokenSources(rootDir);
|
|
1371
|
+
const globalCSSFiles = detectGlobalCSSFiles(rootDir);
|
|
1135
1372
|
return {
|
|
1136
1373
|
framework,
|
|
1137
1374
|
typescript,
|
|
1138
1375
|
tsconfigPath,
|
|
1139
1376
|
componentPatterns,
|
|
1140
1377
|
tokenSources,
|
|
1141
|
-
packageManager
|
|
1378
|
+
packageManager,
|
|
1379
|
+
globalCSSFiles
|
|
1142
1380
|
};
|
|
1143
1381
|
}
|
|
1144
1382
|
|
|
1145
1383
|
// src/init/index.ts
|
|
1146
|
-
import {
|
|
1384
|
+
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
1385
|
+
import { Command as Command3 } from "commander";
|
|
1147
1386
|
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
1148
1387
|
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
1149
1388
|
return {
|
|
1150
1389
|
components: {
|
|
1151
1390
|
include,
|
|
1152
1391
|
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
1153
|
-
wrappers: { providers: [], globalCSS: [] }
|
|
1392
|
+
wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
|
|
1154
1393
|
},
|
|
1155
1394
|
render: {
|
|
1156
1395
|
viewport: { default: { width: 1280, height: 800 } },
|
|
@@ -1181,9 +1420,9 @@ function createRL() {
|
|
|
1181
1420
|
});
|
|
1182
1421
|
}
|
|
1183
1422
|
async function ask(rl, question) {
|
|
1184
|
-
return new Promise((
|
|
1423
|
+
return new Promise((resolve19) => {
|
|
1185
1424
|
rl.question(question, (answer) => {
|
|
1186
|
-
|
|
1425
|
+
resolve19(answer.trim());
|
|
1187
1426
|
});
|
|
1188
1427
|
});
|
|
1189
1428
|
}
|
|
@@ -1192,9 +1431,9 @@ async function askWithDefault(rl, label, defaultValue) {
|
|
|
1192
1431
|
return answer.length > 0 ? answer : defaultValue;
|
|
1193
1432
|
}
|
|
1194
1433
|
function ensureGitignoreEntry(rootDir, entry) {
|
|
1195
|
-
const gitignorePath =
|
|
1196
|
-
if (
|
|
1197
|
-
const content =
|
|
1434
|
+
const gitignorePath = join3(rootDir, ".gitignore");
|
|
1435
|
+
if (existsSync5(gitignorePath)) {
|
|
1436
|
+
const content = readFileSync5(gitignorePath, "utf-8");
|
|
1198
1437
|
const normalised = entry.replace(/\/$/, "");
|
|
1199
1438
|
const lines = content.split("\n").map((l) => l.trim());
|
|
1200
1439
|
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
@@ -1208,18 +1447,118 @@ function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1208
1447
|
`);
|
|
1209
1448
|
}
|
|
1210
1449
|
}
|
|
1450
|
+
function extractTailwindTokens(tokenSources) {
|
|
1451
|
+
const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
|
|
1452
|
+
if (!tailwindSource) return null;
|
|
1453
|
+
try {
|
|
1454
|
+
let parseBlock2 = function(block) {
|
|
1455
|
+
const result = {};
|
|
1456
|
+
const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
|
|
1457
|
+
for (const m of block.matchAll(lineRe)) {
|
|
1458
|
+
if (m[1] !== void 0 && m[2] !== void 0) {
|
|
1459
|
+
result[m[1]] = m[2];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return result;
|
|
1463
|
+
};
|
|
1464
|
+
var parseBlock = parseBlock2;
|
|
1465
|
+
const raw = readFileSync5(tailwindSource.path, "utf-8");
|
|
1466
|
+
const tokens = {};
|
|
1467
|
+
const colorsKeyIdx = raw.indexOf("colors:");
|
|
1468
|
+
if (colorsKeyIdx !== -1) {
|
|
1469
|
+
const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
|
|
1470
|
+
if (colorsBraceStart !== -1) {
|
|
1471
|
+
let colorDepth = 0;
|
|
1472
|
+
let colorsBraceEnd = -1;
|
|
1473
|
+
for (let ci = colorsBraceStart; ci < raw.length; ci++) {
|
|
1474
|
+
if (raw[ci] === "{") colorDepth++;
|
|
1475
|
+
else if (raw[ci] === "}") {
|
|
1476
|
+
colorDepth--;
|
|
1477
|
+
if (colorDepth === 0) {
|
|
1478
|
+
colorsBraceEnd = ci;
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (colorsBraceEnd > colorsBraceStart) {
|
|
1484
|
+
const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
|
|
1485
|
+
const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
|
|
1486
|
+
const colorTokens = {};
|
|
1487
|
+
for (const sm of colorSection.matchAll(scaleRe)) {
|
|
1488
|
+
if (sm[1] === void 0 || sm[2] === void 0) continue;
|
|
1489
|
+
const scaleName = sm[1];
|
|
1490
|
+
const scaleValues = parseBlock2(sm[2]);
|
|
1491
|
+
if (Object.keys(scaleValues).length > 0) {
|
|
1492
|
+
const scaleTokens = {};
|
|
1493
|
+
for (const [step, hex] of Object.entries(scaleValues)) {
|
|
1494
|
+
scaleTokens[step] = { value: hex, type: "color" };
|
|
1495
|
+
}
|
|
1496
|
+
colorTokens[scaleName] = scaleTokens;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (Object.keys(colorTokens).length > 0) {
|
|
1500
|
+
tokens.color = colorTokens;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1506
|
+
if (spacingMatch?.[1] !== void 0) {
|
|
1507
|
+
const spacingValues = parseBlock2(spacingMatch[1]);
|
|
1508
|
+
if (Object.keys(spacingValues).length > 0) {
|
|
1509
|
+
const spacingTokens = {};
|
|
1510
|
+
for (const [key, val] of Object.entries(spacingValues)) {
|
|
1511
|
+
spacingTokens[key] = { value: val, type: "dimension" };
|
|
1512
|
+
}
|
|
1513
|
+
tokens.spacing = spacingTokens;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1517
|
+
if (fontFamilyMatch?.[1] !== void 0) {
|
|
1518
|
+
const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
|
|
1519
|
+
const fontTokens = {};
|
|
1520
|
+
for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
|
|
1521
|
+
if (fm[1] !== void 0 && fm[2] !== void 0) {
|
|
1522
|
+
fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (Object.keys(fontTokens).length > 0) {
|
|
1526
|
+
tokens.font = fontTokens;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
|
|
1530
|
+
if (borderRadiusMatch?.[1] !== void 0) {
|
|
1531
|
+
const radiusValues = parseBlock2(borderRadiusMatch[1]);
|
|
1532
|
+
if (Object.keys(radiusValues).length > 0) {
|
|
1533
|
+
const radiusTokens = {};
|
|
1534
|
+
for (const [key, val] of Object.entries(radiusValues)) {
|
|
1535
|
+
radiusTokens[key] = { value: val, type: "dimension" };
|
|
1536
|
+
}
|
|
1537
|
+
tokens.radius = radiusTokens;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
1541
|
+
} catch {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1211
1545
|
function scaffoldConfig(rootDir, config) {
|
|
1212
|
-
const path =
|
|
1546
|
+
const path = join3(rootDir, "reactscope.config.json");
|
|
1213
1547
|
writeFileSync3(path, `${JSON.stringify(config, null, 2)}
|
|
1214
1548
|
`);
|
|
1215
1549
|
return path;
|
|
1216
1550
|
}
|
|
1217
|
-
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
1218
|
-
const path =
|
|
1219
|
-
if (!
|
|
1551
|
+
function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
|
|
1552
|
+
const path = join3(rootDir, tokenFile);
|
|
1553
|
+
if (!existsSync5(path)) {
|
|
1220
1554
|
const stub = {
|
|
1221
1555
|
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
1222
|
-
|
|
1556
|
+
version: "1.0.0",
|
|
1557
|
+
meta: {
|
|
1558
|
+
name: "Design Tokens",
|
|
1559
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1560
|
+
},
|
|
1561
|
+
tokens: extractedTokens ?? {}
|
|
1223
1562
|
};
|
|
1224
1563
|
writeFileSync3(path, `${JSON.stringify(stub, null, 2)}
|
|
1225
1564
|
`);
|
|
@@ -1227,19 +1566,19 @@ function scaffoldTokenFile(rootDir, tokenFile) {
|
|
|
1227
1566
|
return path;
|
|
1228
1567
|
}
|
|
1229
1568
|
function scaffoldOutputDir(rootDir, outputDir) {
|
|
1230
|
-
const dirPath =
|
|
1569
|
+
const dirPath = join3(rootDir, outputDir);
|
|
1231
1570
|
mkdirSync(dirPath, { recursive: true });
|
|
1232
|
-
const keepPath =
|
|
1233
|
-
if (!
|
|
1571
|
+
const keepPath = join3(dirPath, ".gitkeep");
|
|
1572
|
+
if (!existsSync5(keepPath)) {
|
|
1234
1573
|
writeFileSync3(keepPath, "");
|
|
1235
1574
|
}
|
|
1236
1575
|
return dirPath;
|
|
1237
1576
|
}
|
|
1238
1577
|
async function runInit(options) {
|
|
1239
1578
|
const rootDir = options.cwd ?? process.cwd();
|
|
1240
|
-
const configPath =
|
|
1579
|
+
const configPath = join3(rootDir, "reactscope.config.json");
|
|
1241
1580
|
const created = [];
|
|
1242
|
-
if (
|
|
1581
|
+
if (existsSync5(configPath) && !options.force) {
|
|
1243
1582
|
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
1244
1583
|
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
1245
1584
|
`);
|
|
@@ -1297,7 +1636,13 @@ async function runInit(options) {
|
|
|
1297
1636
|
}
|
|
1298
1637
|
const cfgPath = scaffoldConfig(rootDir, config);
|
|
1299
1638
|
created.push(cfgPath);
|
|
1300
|
-
const
|
|
1639
|
+
const extractedTokens = extractTailwindTokens(detected.tokenSources);
|
|
1640
|
+
if (extractedTokens !== null) {
|
|
1641
|
+
const tokenGroupCount = Object.keys(extractedTokens).length;
|
|
1642
|
+
process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
|
|
1643
|
+
`);
|
|
1644
|
+
}
|
|
1645
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
|
|
1301
1646
|
created.push(tokPath);
|
|
1302
1647
|
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
1303
1648
|
created.push(outDirPath);
|
|
@@ -1308,7 +1653,28 @@ async function runInit(options) {
|
|
|
1308
1653
|
process.stdout.write(` ${p}
|
|
1309
1654
|
`);
|
|
1310
1655
|
}
|
|
1311
|
-
process.stdout.write("\n
|
|
1656
|
+
process.stdout.write("\n Scanning components...\n");
|
|
1657
|
+
try {
|
|
1658
|
+
const manifestConfig = {
|
|
1659
|
+
include: config.components.include,
|
|
1660
|
+
rootDir
|
|
1661
|
+
};
|
|
1662
|
+
const manifest = await generateManifest2(manifestConfig);
|
|
1663
|
+
const manifestCount = Object.keys(manifest.components).length;
|
|
1664
|
+
const manifestOutPath = join3(rootDir, config.output.dir, "manifest.json");
|
|
1665
|
+
mkdirSync(join3(rootDir, config.output.dir), { recursive: true });
|
|
1666
|
+
writeFileSync3(manifestOutPath, `${JSON.stringify(manifest, null, 2)}
|
|
1667
|
+
`);
|
|
1668
|
+
process.stdout.write(
|
|
1669
|
+
` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
|
|
1670
|
+
`
|
|
1671
|
+
);
|
|
1672
|
+
} catch {
|
|
1673
|
+
process.stdout.write(
|
|
1674
|
+
" (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
process.stdout.write("\n");
|
|
1312
1678
|
return {
|
|
1313
1679
|
success: true,
|
|
1314
1680
|
message: "Project initialised successfully.",
|
|
@@ -1317,7 +1683,7 @@ async function runInit(options) {
|
|
|
1317
1683
|
};
|
|
1318
1684
|
}
|
|
1319
1685
|
function createInitCommand() {
|
|
1320
|
-
return new
|
|
1686
|
+
return new Command3("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
|
|
1321
1687
|
try {
|
|
1322
1688
|
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
1323
1689
|
if (!result.success && !result.skipped) {
|
|
@@ -1332,24 +1698,24 @@ function createInitCommand() {
|
|
|
1332
1698
|
}
|
|
1333
1699
|
|
|
1334
1700
|
// src/instrument/renders.ts
|
|
1335
|
-
import { resolve as
|
|
1701
|
+
import { resolve as resolve8 } from "path";
|
|
1336
1702
|
import { getBrowserEntryScript as getBrowserEntryScript5 } from "@agent-scope/playwright";
|
|
1337
1703
|
import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
|
|
1338
|
-
import { Command as
|
|
1704
|
+
import { Command as Command6 } from "commander";
|
|
1339
1705
|
|
|
1340
1706
|
// src/manifest-commands.ts
|
|
1341
|
-
import { existsSync as
|
|
1342
|
-
import { resolve as
|
|
1343
|
-
import { generateManifest as
|
|
1344
|
-
import { Command as
|
|
1707
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1708
|
+
import { resolve as resolve4 } from "path";
|
|
1709
|
+
import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
|
|
1710
|
+
import { Command as Command4 } from "commander";
|
|
1345
1711
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
1346
1712
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
1347
|
-
const absPath =
|
|
1348
|
-
if (!
|
|
1713
|
+
const absPath = resolve4(process.cwd(), manifestPath);
|
|
1714
|
+
if (!existsSync6(absPath)) {
|
|
1349
1715
|
throw new Error(`Manifest not found at ${absPath}.
|
|
1350
1716
|
Run \`scope manifest generate\` first.`);
|
|
1351
1717
|
}
|
|
1352
|
-
const raw =
|
|
1718
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
1353
1719
|
return JSON.parse(raw);
|
|
1354
1720
|
}
|
|
1355
1721
|
function resolveFormat(formatFlag) {
|
|
@@ -1409,7 +1775,10 @@ Available: ${available}${hint}`
|
|
|
1409
1775
|
});
|
|
1410
1776
|
}
|
|
1411
1777
|
function registerQuery(manifestCmd) {
|
|
1412
|
-
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(
|
|
1778
|
+
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(
|
|
1779
|
+
"--has-prop <spec>",
|
|
1780
|
+
"Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
|
|
1781
|
+
).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(
|
|
1413
1782
|
(opts) => {
|
|
1414
1783
|
try {
|
|
1415
1784
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -1420,9 +1789,11 @@ function registerQuery(manifestCmd) {
|
|
|
1420
1789
|
if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
|
|
1421
1790
|
if (opts.sideEffects) queryParts.push("side-effects");
|
|
1422
1791
|
if (opts.hasFetch) queryParts.push("has-fetch");
|
|
1792
|
+
if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
|
|
1793
|
+
if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
|
|
1423
1794
|
if (queryParts.length === 0) {
|
|
1424
1795
|
process.stderr.write(
|
|
1425
|
-
"No query flags specified. Use --context, --hook, --complexity, --side-effects,
|
|
1796
|
+
"No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
|
|
1426
1797
|
);
|
|
1427
1798
|
process.exit(1);
|
|
1428
1799
|
}
|
|
@@ -1449,6 +1820,27 @@ function registerQuery(manifestCmd) {
|
|
|
1449
1820
|
if (opts.hasFetch) {
|
|
1450
1821
|
entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
|
|
1451
1822
|
}
|
|
1823
|
+
if (opts.hasProp !== void 0) {
|
|
1824
|
+
const spec = opts.hasProp;
|
|
1825
|
+
const colonIdx = spec.indexOf(":");
|
|
1826
|
+
const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
|
|
1827
|
+
const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
|
|
1828
|
+
entries = entries.filter(([, d]) => {
|
|
1829
|
+
const props = d.props;
|
|
1830
|
+
if (!props || !(propName in props)) return false;
|
|
1831
|
+
if (propType !== void 0) {
|
|
1832
|
+
return props[propName]?.type === propType;
|
|
1833
|
+
}
|
|
1834
|
+
return true;
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
if (opts.composedBy !== void 0) {
|
|
1838
|
+
const targetName = opts.composedBy;
|
|
1839
|
+
entries = entries.filter(([, d]) => {
|
|
1840
|
+
const composedBy = d.composedBy;
|
|
1841
|
+
return composedBy !== void 0 && composedBy.includes(targetName);
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1452
1844
|
const rows = entries.map(([name, d]) => ({
|
|
1453
1845
|
name,
|
|
1454
1846
|
file: d.filePath,
|
|
@@ -1472,13 +1864,13 @@ function registerGenerate(manifestCmd) {
|
|
|
1472
1864
|
"Generate the component manifest from source and write to .reactscope/manifest.json"
|
|
1473
1865
|
).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
|
|
1474
1866
|
try {
|
|
1475
|
-
const rootDir =
|
|
1476
|
-
const outputPath =
|
|
1867
|
+
const rootDir = resolve4(process.cwd(), opts.root ?? ".");
|
|
1868
|
+
const outputPath = resolve4(process.cwd(), opts.output);
|
|
1477
1869
|
const include = opts.include?.split(",").map((s) => s.trim());
|
|
1478
1870
|
const exclude = opts.exclude?.split(",").map((s) => s.trim());
|
|
1479
1871
|
process.stderr.write(`Scanning ${rootDir} for React components...
|
|
1480
1872
|
`);
|
|
1481
|
-
const manifest = await
|
|
1873
|
+
const manifest = await generateManifest3({
|
|
1482
1874
|
rootDir,
|
|
1483
1875
|
...include !== void 0 && { include },
|
|
1484
1876
|
...exclude !== void 0 && { exclude }
|
|
@@ -1487,7 +1879,7 @@ function registerGenerate(manifestCmd) {
|
|
|
1487
1879
|
process.stderr.write(`Found ${componentCount} components.
|
|
1488
1880
|
`);
|
|
1489
1881
|
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
1490
|
-
if (!
|
|
1882
|
+
if (!existsSync6(outputDir)) {
|
|
1491
1883
|
mkdirSync2(outputDir, { recursive: true });
|
|
1492
1884
|
}
|
|
1493
1885
|
writeFileSync4(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
@@ -1503,7 +1895,7 @@ function registerGenerate(manifestCmd) {
|
|
|
1503
1895
|
});
|
|
1504
1896
|
}
|
|
1505
1897
|
function createManifestCommand() {
|
|
1506
|
-
const manifestCmd = new
|
|
1898
|
+
const manifestCmd = new Command4("manifest").description(
|
|
1507
1899
|
"Query and explore the component manifest"
|
|
1508
1900
|
);
|
|
1509
1901
|
registerList(manifestCmd);
|
|
@@ -1514,7 +1906,7 @@ function createManifestCommand() {
|
|
|
1514
1906
|
}
|
|
1515
1907
|
|
|
1516
1908
|
// src/instrument/hooks.ts
|
|
1517
|
-
import { resolve as
|
|
1909
|
+
import { resolve as resolve5 } from "path";
|
|
1518
1910
|
import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
|
|
1519
1911
|
import { Command as Cmd } from "commander";
|
|
1520
1912
|
import { chromium as chromium2 } from "playwright";
|
|
@@ -1846,7 +2238,7 @@ Available: ${available}`
|
|
|
1846
2238
|
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
1847
2239
|
}
|
|
1848
2240
|
const rootDir = process.cwd();
|
|
1849
|
-
const filePath =
|
|
2241
|
+
const filePath = resolve5(rootDir, descriptor.filePath);
|
|
1850
2242
|
process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
|
|
1851
2243
|
`);
|
|
1852
2244
|
const result = await runHooksProfiling(componentName, filePath, props);
|
|
@@ -1874,7 +2266,7 @@ Available: ${available}`
|
|
|
1874
2266
|
}
|
|
1875
2267
|
|
|
1876
2268
|
// src/instrument/profile.ts
|
|
1877
|
-
import { resolve as
|
|
2269
|
+
import { resolve as resolve6 } from "path";
|
|
1878
2270
|
import { getBrowserEntryScript as getBrowserEntryScript3 } from "@agent-scope/playwright";
|
|
1879
2271
|
import { Command as Cmd2 } from "commander";
|
|
1880
2272
|
import { chromium as chromium3 } from "playwright";
|
|
@@ -2149,7 +2541,7 @@ Available: ${available}`
|
|
|
2149
2541
|
throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
|
|
2150
2542
|
}
|
|
2151
2543
|
const rootDir = process.cwd();
|
|
2152
|
-
const filePath =
|
|
2544
|
+
const filePath = resolve6(rootDir, descriptor.filePath);
|
|
2153
2545
|
process.stderr.write(`Profiling interaction for ${componentName}\u2026
|
|
2154
2546
|
`);
|
|
2155
2547
|
const result = await runInteractionProfile(componentName, filePath, props, interaction);
|
|
@@ -2177,9 +2569,9 @@ Available: ${available}`
|
|
|
2177
2569
|
}
|
|
2178
2570
|
|
|
2179
2571
|
// src/instrument/tree.ts
|
|
2180
|
-
import { resolve as
|
|
2572
|
+
import { resolve as resolve7 } from "path";
|
|
2181
2573
|
import { getBrowserEntryScript as getBrowserEntryScript4 } from "@agent-scope/playwright";
|
|
2182
|
-
import { Command as
|
|
2574
|
+
import { Command as Command5 } from "commander";
|
|
2183
2575
|
import { chromium as chromium4 } from "playwright";
|
|
2184
2576
|
var MANIFEST_PATH4 = ".reactscope/manifest.json";
|
|
2185
2577
|
var DEFAULT_VIEWPORT_WIDTH = 375;
|
|
@@ -2460,7 +2852,7 @@ async function runInstrumentTree(options) {
|
|
|
2460
2852
|
}
|
|
2461
2853
|
}
|
|
2462
2854
|
function createInstrumentTreeCommand() {
|
|
2463
|
-
return new
|
|
2855
|
+
return new Command5("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
|
|
2464
2856
|
"--wasted-renders",
|
|
2465
2857
|
"Filter to components with wasted renders (no prop/state/context changes, not memoized)",
|
|
2466
2858
|
false
|
|
@@ -2484,7 +2876,7 @@ Available: ${available}`
|
|
|
2484
2876
|
}
|
|
2485
2877
|
}
|
|
2486
2878
|
const rootDir = process.cwd();
|
|
2487
|
-
const filePath =
|
|
2879
|
+
const filePath = resolve7(rootDir, descriptor.filePath);
|
|
2488
2880
|
process.stderr.write(`Instrumenting ${componentName}\u2026
|
|
2489
2881
|
`);
|
|
2490
2882
|
const instrumentRoot = await runInstrumentTree({
|
|
@@ -2855,7 +3247,7 @@ Available: ${available}`
|
|
|
2855
3247
|
);
|
|
2856
3248
|
}
|
|
2857
3249
|
const rootDir = process.cwd();
|
|
2858
|
-
const filePath =
|
|
3250
|
+
const filePath = resolve8(rootDir, descriptor.filePath);
|
|
2859
3251
|
const preScript = getBrowserEntryScript5() + "\n" + buildInstrumentationScript();
|
|
2860
3252
|
const htmlHarness = await buildComponentHarness(
|
|
2861
3253
|
filePath,
|
|
@@ -2945,7 +3337,7 @@ function formatRendersTable(result) {
|
|
|
2945
3337
|
return lines.join("\n");
|
|
2946
3338
|
}
|
|
2947
3339
|
function createInstrumentRendersCommand() {
|
|
2948
|
-
return new
|
|
3340
|
+
return new Command6("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
|
|
2949
3341
|
"--interaction <json>",
|
|
2950
3342
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
2951
3343
|
"[]"
|
|
@@ -2990,7 +3382,7 @@ function createInstrumentRendersCommand() {
|
|
|
2990
3382
|
);
|
|
2991
3383
|
}
|
|
2992
3384
|
function createInstrumentCommand() {
|
|
2993
|
-
const instrumentCmd = new
|
|
3385
|
+
const instrumentCmd = new Command6("instrument").description(
|
|
2994
3386
|
"Structured instrumentation commands for React component analysis"
|
|
2995
3387
|
);
|
|
2996
3388
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
@@ -3001,8 +3393,8 @@ function createInstrumentCommand() {
|
|
|
3001
3393
|
}
|
|
3002
3394
|
|
|
3003
3395
|
// src/render-commands.ts
|
|
3004
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
3005
|
-
import { resolve as
|
|
3396
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3397
|
+
import { resolve as resolve10 } from "path";
|
|
3006
3398
|
import {
|
|
3007
3399
|
ALL_CONTEXT_IDS,
|
|
3008
3400
|
ALL_STRESS_IDS,
|
|
@@ -3013,13 +3405,13 @@ import {
|
|
|
3013
3405
|
safeRender as safeRender2,
|
|
3014
3406
|
stressAxis
|
|
3015
3407
|
} from "@agent-scope/render";
|
|
3016
|
-
import { Command as
|
|
3408
|
+
import { Command as Command7 } from "commander";
|
|
3017
3409
|
|
|
3018
3410
|
// src/scope-file.ts
|
|
3019
|
-
import { existsSync as
|
|
3411
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync } from "fs";
|
|
3020
3412
|
import { createRequire as createRequire2 } from "module";
|
|
3021
3413
|
import { tmpdir } from "os";
|
|
3022
|
-
import { dirname as dirname2, join as
|
|
3414
|
+
import { dirname as dirname2, join as join4, resolve as resolve9 } from "path";
|
|
3023
3415
|
import * as esbuild2 from "esbuild";
|
|
3024
3416
|
var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
|
|
3025
3417
|
function findScopeFile(componentFilePath) {
|
|
@@ -3027,15 +3419,15 @@ function findScopeFile(componentFilePath) {
|
|
|
3027
3419
|
const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
3028
3420
|
const baseName = stem.slice(dir.length + 1);
|
|
3029
3421
|
for (const ext of SCOPE_EXTENSIONS) {
|
|
3030
|
-
const candidate =
|
|
3031
|
-
if (
|
|
3422
|
+
const candidate = join4(dir, `${baseName}${ext}`);
|
|
3423
|
+
if (existsSync7(candidate)) return candidate;
|
|
3032
3424
|
}
|
|
3033
3425
|
return null;
|
|
3034
3426
|
}
|
|
3035
3427
|
async function loadScopeFile(scopeFilePath) {
|
|
3036
|
-
const tmpDir =
|
|
3428
|
+
const tmpDir = join4(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3037
3429
|
mkdirSync3(tmpDir, { recursive: true });
|
|
3038
|
-
const outFile =
|
|
3430
|
+
const outFile = join4(tmpDir, "scope-file.cjs");
|
|
3039
3431
|
try {
|
|
3040
3432
|
const result = await esbuild2.build({
|
|
3041
3433
|
entryPoints: [scopeFilePath],
|
|
@@ -3060,7 +3452,7 @@ async function loadScopeFile(scopeFilePath) {
|
|
|
3060
3452
|
${msg}`);
|
|
3061
3453
|
}
|
|
3062
3454
|
const req = createRequire2(import.meta.url);
|
|
3063
|
-
delete req.cache[
|
|
3455
|
+
delete req.cache[resolve9(outFile)];
|
|
3064
3456
|
const mod = req(outFile);
|
|
3065
3457
|
const scenarios = extractScenarios(mod, scopeFilePath);
|
|
3066
3458
|
const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
|
|
@@ -3137,6 +3529,17 @@ ${msg}`);
|
|
|
3137
3529
|
}
|
|
3138
3530
|
|
|
3139
3531
|
// src/render-commands.ts
|
|
3532
|
+
function loadGlobalCssFilesFromConfig(cwd) {
|
|
3533
|
+
const configPath = resolve10(cwd, "reactscope.config.json");
|
|
3534
|
+
if (!existsSync8(configPath)) return [];
|
|
3535
|
+
try {
|
|
3536
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
3537
|
+
const cfg = JSON.parse(raw);
|
|
3538
|
+
return cfg.components?.wrappers?.globalCSS ?? [];
|
|
3539
|
+
} catch {
|
|
3540
|
+
return [];
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3140
3543
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
3141
3544
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
3142
3545
|
var _pool3 = null;
|
|
@@ -3157,7 +3560,7 @@ async function shutdownPool3() {
|
|
|
3157
3560
|
_pool3 = null;
|
|
3158
3561
|
}
|
|
3159
3562
|
}
|
|
3160
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
|
|
3563
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
|
|
3161
3564
|
const satori = new SatoriRenderer({
|
|
3162
3565
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
3163
3566
|
});
|
|
@@ -3166,13 +3569,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3166
3569
|
async renderCell(props, _complexityClass) {
|
|
3167
3570
|
const startMs = performance.now();
|
|
3168
3571
|
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
3572
|
+
const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
|
|
3169
3573
|
const htmlHarness = await buildComponentHarness(
|
|
3170
3574
|
filePath,
|
|
3171
3575
|
componentName,
|
|
3172
3576
|
props,
|
|
3173
3577
|
viewportWidth,
|
|
3174
|
-
void 0,
|
|
3175
|
-
// projectCss (handled separately)
|
|
3578
|
+
projectCss ?? void 0,
|
|
3176
3579
|
wrapperScript
|
|
3177
3580
|
);
|
|
3178
3581
|
const slot = await pool.acquire();
|
|
@@ -3201,10 +3604,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3201
3604
|
}
|
|
3202
3605
|
});
|
|
3203
3606
|
return [...set];
|
|
3204
|
-
});
|
|
3205
|
-
const
|
|
3206
|
-
if (
|
|
3207
|
-
await page.addStyleTag({ content:
|
|
3607
|
+
}) ?? [];
|
|
3608
|
+
const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
|
|
3609
|
+
if (projectCss2 != null && projectCss2.length > 0) {
|
|
3610
|
+
await page.addStyleTag({ content: projectCss2 });
|
|
3208
3611
|
}
|
|
3209
3612
|
const renderTimeMs = performance.now() - startMs;
|
|
3210
3613
|
const rootLocator = page.locator("[data-reactscope-root]");
|
|
@@ -3214,49 +3617,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
|
|
|
3214
3617
|
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
3215
3618
|
);
|
|
3216
3619
|
}
|
|
3217
|
-
const PAD =
|
|
3218
|
-
const MIN_W = 320;
|
|
3219
|
-
const MIN_H = 200;
|
|
3620
|
+
const PAD = 8;
|
|
3220
3621
|
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
3221
3622
|
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
3222
3623
|
const rawW = boundingBox.width + PAD * 2;
|
|
3223
3624
|
const rawH = boundingBox.height + PAD * 2;
|
|
3224
|
-
const
|
|
3225
|
-
const
|
|
3226
|
-
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
3227
|
-
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
3625
|
+
const safeW = Math.min(rawW, viewportWidth - clipX);
|
|
3626
|
+
const safeH = Math.min(rawH, viewportHeight - clipY);
|
|
3228
3627
|
const screenshot = await page.screenshot({
|
|
3229
3628
|
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
3230
3629
|
type: "png"
|
|
3231
3630
|
});
|
|
3631
|
+
const STYLE_PROPS = [
|
|
3632
|
+
"display",
|
|
3633
|
+
"width",
|
|
3634
|
+
"height",
|
|
3635
|
+
"color",
|
|
3636
|
+
"backgroundColor",
|
|
3637
|
+
"fontSize",
|
|
3638
|
+
"fontFamily",
|
|
3639
|
+
"fontWeight",
|
|
3640
|
+
"lineHeight",
|
|
3641
|
+
"padding",
|
|
3642
|
+
"paddingTop",
|
|
3643
|
+
"paddingRight",
|
|
3644
|
+
"paddingBottom",
|
|
3645
|
+
"paddingLeft",
|
|
3646
|
+
"margin",
|
|
3647
|
+
"marginTop",
|
|
3648
|
+
"marginRight",
|
|
3649
|
+
"marginBottom",
|
|
3650
|
+
"marginLeft",
|
|
3651
|
+
"gap",
|
|
3652
|
+
"borderRadius",
|
|
3653
|
+
"borderWidth",
|
|
3654
|
+
"borderColor",
|
|
3655
|
+
"borderStyle",
|
|
3656
|
+
"boxShadow",
|
|
3657
|
+
"opacity",
|
|
3658
|
+
"position",
|
|
3659
|
+
"flexDirection",
|
|
3660
|
+
"alignItems",
|
|
3661
|
+
"justifyContent",
|
|
3662
|
+
"overflow"
|
|
3663
|
+
];
|
|
3664
|
+
const _domResult = await page.evaluate(
|
|
3665
|
+
(args) => {
|
|
3666
|
+
let count = 0;
|
|
3667
|
+
const styles = {};
|
|
3668
|
+
function captureStyles(el, id, propList) {
|
|
3669
|
+
const computed = window.getComputedStyle(el);
|
|
3670
|
+
const out = {};
|
|
3671
|
+
for (const prop of propList) {
|
|
3672
|
+
const val = computed[prop] ?? "";
|
|
3673
|
+
if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
|
|
3674
|
+
}
|
|
3675
|
+
styles[id] = out;
|
|
3676
|
+
}
|
|
3677
|
+
function walk(node) {
|
|
3678
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
3679
|
+
return {
|
|
3680
|
+
tag: "#text",
|
|
3681
|
+
attrs: {},
|
|
3682
|
+
text: node.textContent?.trim() ?? "",
|
|
3683
|
+
children: []
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
const el = node;
|
|
3687
|
+
const id = count++;
|
|
3688
|
+
captureStyles(el, id, args.props);
|
|
3689
|
+
const attrs = {};
|
|
3690
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3691
|
+
attrs[attr.name] = attr.value;
|
|
3692
|
+
}
|
|
3693
|
+
const children = Array.from(el.childNodes).filter(
|
|
3694
|
+
(n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
|
|
3695
|
+
).map(walk);
|
|
3696
|
+
return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
|
|
3697
|
+
}
|
|
3698
|
+
const root = document.querySelector(args.sel);
|
|
3699
|
+
if (!root)
|
|
3700
|
+
return {
|
|
3701
|
+
tree: { tag: "div", attrs: {}, children: [] },
|
|
3702
|
+
elementCount: 0,
|
|
3703
|
+
nodeStyles: {}
|
|
3704
|
+
};
|
|
3705
|
+
return { tree: walk(root), elementCount: count, nodeStyles: styles };
|
|
3706
|
+
},
|
|
3707
|
+
{ sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
|
|
3708
|
+
);
|
|
3709
|
+
const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
|
|
3710
|
+
const elementCount = _domResult?.elementCount ?? 0;
|
|
3711
|
+
const nodeStyles = _domResult?.nodeStyles ?? {};
|
|
3232
3712
|
const computedStyles = {};
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
"fontFamily",
|
|
3246
|
-
"padding",
|
|
3247
|
-
"margin"
|
|
3248
|
-
]) {
|
|
3249
|
-
out[prop] = computed.getPropertyValue(prop);
|
|
3713
|
+
if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
|
|
3714
|
+
for (const [nodeId, styles] of Object.entries(nodeStyles)) {
|
|
3715
|
+
computedStyles[`#node-${nodeId}`] = styles;
|
|
3716
|
+
}
|
|
3717
|
+
const dom = {
|
|
3718
|
+
tree: domTree,
|
|
3719
|
+
elementCount,
|
|
3720
|
+
boundingBox: {
|
|
3721
|
+
x: boundingBox.x,
|
|
3722
|
+
y: boundingBox.y,
|
|
3723
|
+
width: boundingBox.width,
|
|
3724
|
+
height: boundingBox.height
|
|
3250
3725
|
}
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3726
|
+
};
|
|
3727
|
+
const a11yInfo = await page.evaluate((sel) => {
|
|
3728
|
+
const wrapper = document.querySelector(sel);
|
|
3729
|
+
const el = wrapper?.firstElementChild ?? wrapper;
|
|
3730
|
+
if (!el) return { role: "generic", name: "" };
|
|
3731
|
+
return {
|
|
3732
|
+
role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
|
|
3733
|
+
name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
|
|
3734
|
+
};
|
|
3735
|
+
}, "[data-reactscope-root]") ?? {
|
|
3736
|
+
role: "generic",
|
|
3737
|
+
name: ""
|
|
3738
|
+
};
|
|
3739
|
+
const imgViolations = await page.evaluate((sel) => {
|
|
3740
|
+
const container = document.querySelector(sel);
|
|
3741
|
+
if (!container) return [];
|
|
3742
|
+
const issues = [];
|
|
3743
|
+
container.querySelectorAll("img").forEach((img) => {
|
|
3744
|
+
if (!img.alt) issues.push("Image missing accessible name");
|
|
3745
|
+
});
|
|
3746
|
+
return issues;
|
|
3747
|
+
}, "[data-reactscope-root]") ?? [];
|
|
3748
|
+
const accessibility = {
|
|
3749
|
+
role: a11yInfo.role,
|
|
3750
|
+
name: a11yInfo.name,
|
|
3751
|
+
violations: imgViolations
|
|
3752
|
+
};
|
|
3254
3753
|
return {
|
|
3255
3754
|
screenshot,
|
|
3256
3755
|
width: Math.round(safeW),
|
|
3257
3756
|
height: Math.round(safeH),
|
|
3258
3757
|
renderTimeMs,
|
|
3259
|
-
computedStyles
|
|
3758
|
+
computedStyles,
|
|
3759
|
+
dom,
|
|
3760
|
+
accessibility
|
|
3260
3761
|
};
|
|
3261
3762
|
} finally {
|
|
3262
3763
|
pool.release(slot);
|
|
@@ -3306,26 +3807,64 @@ function registerRenderSingle(renderCmd) {
|
|
|
3306
3807
|
Available: ${available}`
|
|
3307
3808
|
);
|
|
3308
3809
|
}
|
|
3810
|
+
let props = {};
|
|
3811
|
+
if (opts.props !== void 0) {
|
|
3812
|
+
try {
|
|
3813
|
+
props = JSON.parse(opts.props);
|
|
3814
|
+
} catch {
|
|
3815
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
if (descriptor.props !== void 0) {
|
|
3819
|
+
const propDefs = descriptor.props;
|
|
3820
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
3821
|
+
if (propName in props) continue;
|
|
3822
|
+
if (!propDef.required && propDef.default !== void 0) continue;
|
|
3823
|
+
if (propDef.type === "node" || propDef.type === "string") {
|
|
3824
|
+
props[propName] = propName === "children" ? componentName : propName;
|
|
3825
|
+
} else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
|
|
3826
|
+
props[propName] = propDef.values[0];
|
|
3827
|
+
} else if (propDef.type === "boolean") {
|
|
3828
|
+
props[propName] = false;
|
|
3829
|
+
} else if (propDef.type === "number") {
|
|
3830
|
+
props[propName] = 0;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3309
3834
|
const { width, height } = parseViewport(opts.viewport);
|
|
3310
3835
|
const rootDir = process.cwd();
|
|
3311
|
-
const filePath =
|
|
3836
|
+
const filePath = resolve10(rootDir, descriptor.filePath);
|
|
3312
3837
|
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3313
3838
|
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3314
3839
|
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3315
|
-
const
|
|
3840
|
+
const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3841
|
+
if (globalCssFiles.length === 0) {
|
|
3842
|
+
process.stderr.write(
|
|
3843
|
+
"warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
const renderer = buildRenderer(
|
|
3847
|
+
filePath,
|
|
3848
|
+
componentName,
|
|
3849
|
+
width,
|
|
3850
|
+
height,
|
|
3851
|
+
globalCssFiles,
|
|
3852
|
+
rootDir,
|
|
3853
|
+
wrapperScript
|
|
3854
|
+
);
|
|
3316
3855
|
process.stderr.write(
|
|
3317
3856
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3318
3857
|
`
|
|
3319
3858
|
);
|
|
3320
3859
|
const fmt2 = resolveSingleFormat(opts.format);
|
|
3321
3860
|
let anyFailed = false;
|
|
3322
|
-
for (const [scenarioName,
|
|
3861
|
+
for (const [scenarioName, props2] of Object.entries(scenarios)) {
|
|
3323
3862
|
const isNamed = scenarioName !== "__default__";
|
|
3324
3863
|
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3325
3864
|
const outcome = await safeRender2(
|
|
3326
|
-
() => renderer.renderCell(
|
|
3865
|
+
() => renderer.renderCell(props2, descriptor.complexityClass),
|
|
3327
3866
|
{
|
|
3328
|
-
props,
|
|
3867
|
+
props: props2,
|
|
3329
3868
|
sourceLocation: {
|
|
3330
3869
|
file: descriptor.filePath,
|
|
3331
3870
|
line: descriptor.loc.start,
|
|
@@ -3347,20 +3886,20 @@ Available: ${available}`
|
|
|
3347
3886
|
const result = outcome.result;
|
|
3348
3887
|
const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
|
|
3349
3888
|
if (opts.output !== void 0 && !isNamed) {
|
|
3350
|
-
const outPath =
|
|
3889
|
+
const outPath = resolve10(process.cwd(), opts.output);
|
|
3351
3890
|
writeFileSync5(outPath, result.screenshot);
|
|
3352
3891
|
process.stdout.write(
|
|
3353
3892
|
`\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3354
3893
|
`
|
|
3355
3894
|
);
|
|
3356
3895
|
} else if (fmt2 === "json") {
|
|
3357
|
-
const json = formatRenderJson(label,
|
|
3896
|
+
const json = formatRenderJson(label, props2, result);
|
|
3358
3897
|
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3359
3898
|
`);
|
|
3360
3899
|
} else {
|
|
3361
|
-
const dir =
|
|
3900
|
+
const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3362
3901
|
mkdirSync4(dir, { recursive: true });
|
|
3363
|
-
const outPath =
|
|
3902
|
+
const outPath = resolve10(dir, outFileName);
|
|
3364
3903
|
writeFileSync5(outPath, result.screenshot);
|
|
3365
3904
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
|
|
3366
3905
|
process.stdout.write(
|
|
@@ -3381,7 +3920,10 @@ Available: ${available}`
|
|
|
3381
3920
|
);
|
|
3382
3921
|
}
|
|
3383
3922
|
function registerRenderMatrix(renderCmd) {
|
|
3384
|
-
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3923
|
+
renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
|
|
3924
|
+
"--axes <spec>",
|
|
3925
|
+
`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"]}'`
|
|
3926
|
+
).option(
|
|
3385
3927
|
"--contexts <ids>",
|
|
3386
3928
|
"Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
|
|
3387
3929
|
).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(
|
|
@@ -3399,22 +3941,48 @@ Available: ${available}`
|
|
|
3399
3941
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
|
|
3400
3942
|
const { width, height } = { width: 375, height: 812 };
|
|
3401
3943
|
const rootDir = process.cwd();
|
|
3402
|
-
const filePath =
|
|
3403
|
-
const
|
|
3944
|
+
const filePath = resolve10(rootDir, descriptor.filePath);
|
|
3945
|
+
const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
|
|
3946
|
+
const renderer = buildRenderer(
|
|
3947
|
+
filePath,
|
|
3948
|
+
componentName,
|
|
3949
|
+
width,
|
|
3950
|
+
height,
|
|
3951
|
+
matrixCssFiles,
|
|
3952
|
+
rootDir
|
|
3953
|
+
);
|
|
3404
3954
|
const axes = [];
|
|
3405
3955
|
if (opts.axes !== void 0) {
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3956
|
+
const axesRaw = opts.axes.trim();
|
|
3957
|
+
if (axesRaw.startsWith("{")) {
|
|
3958
|
+
let parsed;
|
|
3959
|
+
try {
|
|
3960
|
+
parsed = JSON.parse(axesRaw);
|
|
3961
|
+
} catch {
|
|
3962
|
+
throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
|
|
3411
3963
|
}
|
|
3412
|
-
const name
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3964
|
+
for (const [name, vals] of Object.entries(parsed)) {
|
|
3965
|
+
if (!Array.isArray(vals)) {
|
|
3966
|
+
throw new Error(`Axis "${name}" must be an array of values in JSON format`);
|
|
3967
|
+
}
|
|
3968
|
+
axes.push({ name, values: vals.map(String) });
|
|
3969
|
+
}
|
|
3970
|
+
} else {
|
|
3971
|
+
const axisSpecs = axesRaw.split(/\s+/);
|
|
3972
|
+
for (const spec of axisSpecs) {
|
|
3973
|
+
const colonIdx = spec.indexOf(":");
|
|
3974
|
+
if (colonIdx < 0) {
|
|
3975
|
+
throw new Error(
|
|
3976
|
+
`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
const name = spec.slice(0, colonIdx);
|
|
3980
|
+
const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
|
|
3981
|
+
if (name.length === 0 || values.length === 0) {
|
|
3982
|
+
throw new Error(`Invalid axis spec "${spec}"`);
|
|
3983
|
+
}
|
|
3984
|
+
axes.push({ name, values });
|
|
3416
3985
|
}
|
|
3417
|
-
axes.push({ name, values });
|
|
3418
3986
|
}
|
|
3419
3987
|
}
|
|
3420
3988
|
if (opts.contexts !== void 0) {
|
|
@@ -3466,7 +4034,7 @@ Available: ${available}`
|
|
|
3466
4034
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3467
4035
|
const gen = new SpriteSheetGenerator2();
|
|
3468
4036
|
const sheet = await gen.generate(result);
|
|
3469
|
-
const spritePath =
|
|
4037
|
+
const spritePath = resolve10(process.cwd(), opts.sprite);
|
|
3470
4038
|
writeFileSync5(spritePath, sheet.png);
|
|
3471
4039
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
3472
4040
|
`);
|
|
@@ -3476,9 +4044,9 @@ Available: ${available}`
|
|
|
3476
4044
|
const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
|
|
3477
4045
|
const gen = new SpriteSheetGenerator2();
|
|
3478
4046
|
const sheet = await gen.generate(result);
|
|
3479
|
-
const dir =
|
|
4047
|
+
const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3480
4048
|
mkdirSync4(dir, { recursive: true });
|
|
3481
|
-
const outPath =
|
|
4049
|
+
const outPath = resolve10(dir, `${componentName}-matrix.png`);
|
|
3482
4050
|
writeFileSync5(outPath, sheet.png);
|
|
3483
4051
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
3484
4052
|
process.stdout.write(
|
|
@@ -3522,22 +4090,37 @@ function registerRenderAll(renderCmd) {
|
|
|
3522
4090
|
return;
|
|
3523
4091
|
}
|
|
3524
4092
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
3525
|
-
const outputDir =
|
|
4093
|
+
const outputDir = resolve10(process.cwd(), opts.outputDir);
|
|
3526
4094
|
mkdirSync4(outputDir, { recursive: true });
|
|
3527
4095
|
const rootDir = process.cwd();
|
|
3528
4096
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
3529
4097
|
`);
|
|
3530
4098
|
const results = [];
|
|
4099
|
+
const complianceStylesMap = {};
|
|
3531
4100
|
let completed = 0;
|
|
3532
4101
|
const renderOne = async (name) => {
|
|
3533
4102
|
const descriptor = manifest.components[name];
|
|
3534
4103
|
if (descriptor === void 0) return;
|
|
3535
|
-
const filePath =
|
|
3536
|
-
const
|
|
4104
|
+
const filePath = resolve10(rootDir, descriptor.filePath);
|
|
4105
|
+
const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
|
|
4106
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
4107
|
+
const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
|
|
4108
|
+
const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
|
|
4109
|
+
const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
|
|
4110
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
4111
|
+
const renderer = buildRenderer(
|
|
4112
|
+
filePath,
|
|
4113
|
+
name,
|
|
4114
|
+
375,
|
|
4115
|
+
812,
|
|
4116
|
+
allCssFiles,
|
|
4117
|
+
process.cwd(),
|
|
4118
|
+
wrapperScript
|
|
4119
|
+
);
|
|
3537
4120
|
const outcome = await safeRender2(
|
|
3538
|
-
() => renderer.renderCell(
|
|
4121
|
+
() => renderer.renderCell(renderProps, descriptor.complexityClass),
|
|
3539
4122
|
{
|
|
3540
|
-
props:
|
|
4123
|
+
props: renderProps,
|
|
3541
4124
|
sourceLocation: {
|
|
3542
4125
|
file: descriptor.filePath,
|
|
3543
4126
|
line: descriptor.loc.start,
|
|
@@ -3555,7 +4138,7 @@ function registerRenderAll(renderCmd) {
|
|
|
3555
4138
|
success: false,
|
|
3556
4139
|
errorMessage: outcome.error.message
|
|
3557
4140
|
});
|
|
3558
|
-
const errPath =
|
|
4141
|
+
const errPath = resolve10(outputDir, `${name}.error.json`);
|
|
3559
4142
|
writeFileSync5(
|
|
3560
4143
|
errPath,
|
|
3561
4144
|
JSON.stringify(
|
|
@@ -3573,10 +4156,81 @@ function registerRenderAll(renderCmd) {
|
|
|
3573
4156
|
}
|
|
3574
4157
|
const result = outcome.result;
|
|
3575
4158
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
3576
|
-
const pngPath =
|
|
4159
|
+
const pngPath = resolve10(outputDir, `${name}.png`);
|
|
3577
4160
|
writeFileSync5(pngPath, result.screenshot);
|
|
3578
|
-
const jsonPath =
|
|
4161
|
+
const jsonPath = resolve10(outputDir, `${name}.json`);
|
|
3579
4162
|
writeFileSync5(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
4163
|
+
const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
|
|
4164
|
+
const compStyles = {
|
|
4165
|
+
colors: {},
|
|
4166
|
+
spacing: {},
|
|
4167
|
+
typography: {},
|
|
4168
|
+
borders: {},
|
|
4169
|
+
shadows: {}
|
|
4170
|
+
};
|
|
4171
|
+
for (const [prop, val] of Object.entries(rawStyles)) {
|
|
4172
|
+
if (!val || val === "none" || val === "") continue;
|
|
4173
|
+
const lower = prop.toLowerCase();
|
|
4174
|
+
if (lower.includes("color") || lower.includes("background")) {
|
|
4175
|
+
compStyles.colors[prop] = val;
|
|
4176
|
+
} else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
|
|
4177
|
+
compStyles.spacing[prop] = val;
|
|
4178
|
+
} else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
|
|
4179
|
+
compStyles.typography[prop] = val;
|
|
4180
|
+
} else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
|
|
4181
|
+
compStyles.borders[prop] = val;
|
|
4182
|
+
} else if (lower.includes("shadow")) {
|
|
4183
|
+
compStyles.shadows[prop] = val;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
complianceStylesMap[name] = compStyles;
|
|
4187
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
|
|
4188
|
+
try {
|
|
4189
|
+
const scenarioEntries2 = Object.entries(scopeData.scenarios);
|
|
4190
|
+
const scenarioAxis = {
|
|
4191
|
+
name: "scenario",
|
|
4192
|
+
values: scenarioEntries2.map(([k]) => k)
|
|
4193
|
+
};
|
|
4194
|
+
const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
|
|
4195
|
+
const matrixRenderer = buildRenderer(
|
|
4196
|
+
filePath,
|
|
4197
|
+
name,
|
|
4198
|
+
375,
|
|
4199
|
+
812,
|
|
4200
|
+
allCssFiles,
|
|
4201
|
+
process.cwd(),
|
|
4202
|
+
wrapperScript
|
|
4203
|
+
);
|
|
4204
|
+
const wrappedRenderer = {
|
|
4205
|
+
_satori: matrixRenderer._satori,
|
|
4206
|
+
async renderCell(props, cc) {
|
|
4207
|
+
const scenarioName = props.scenario;
|
|
4208
|
+
const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
|
|
4209
|
+
return matrixRenderer.renderCell(realProps, cc ?? "simple");
|
|
4210
|
+
}
|
|
4211
|
+
};
|
|
4212
|
+
const matrix = new RenderMatrix(wrappedRenderer, [scenarioAxis], {
|
|
4213
|
+
concurrency: 2
|
|
4214
|
+
});
|
|
4215
|
+
const matrixResult = await matrix.render();
|
|
4216
|
+
const matrixCells = matrixResult.cells.map((cell) => ({
|
|
4217
|
+
axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
|
|
4218
|
+
screenshot: cell.result.screenshot.toString("base64"),
|
|
4219
|
+
width: cell.result.width,
|
|
4220
|
+
height: cell.result.height,
|
|
4221
|
+
renderTimeMs: cell.result.renderTimeMs
|
|
4222
|
+
}));
|
|
4223
|
+
const existingJson = JSON.parse(readFileSync7(jsonPath, "utf-8"));
|
|
4224
|
+
existingJson.cells = matrixCells;
|
|
4225
|
+
existingJson.axisLabels = [scenarioAxis.values];
|
|
4226
|
+
writeFileSync5(jsonPath, JSON.stringify(existingJson, null, 2));
|
|
4227
|
+
} catch (matrixErr) {
|
|
4228
|
+
process.stderr.write(
|
|
4229
|
+
` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
|
|
4230
|
+
`
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
3580
4234
|
if (isTTY()) {
|
|
3581
4235
|
process.stdout.write(
|
|
3582
4236
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -3600,6 +4254,14 @@ function registerRenderAll(renderCmd) {
|
|
|
3600
4254
|
}
|
|
3601
4255
|
await Promise.all(workers);
|
|
3602
4256
|
await shutdownPool3();
|
|
4257
|
+
const compStylesPath = resolve10(
|
|
4258
|
+
resolve10(process.cwd(), opts.outputDir),
|
|
4259
|
+
"..",
|
|
4260
|
+
"compliance-styles.json"
|
|
4261
|
+
);
|
|
4262
|
+
writeFileSync5(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
|
|
4263
|
+
process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
|
|
4264
|
+
`);
|
|
3603
4265
|
process.stderr.write("\n");
|
|
3604
4266
|
const summary = formatSummaryText(results, outputDir);
|
|
3605
4267
|
process.stderr.write(`${summary}
|
|
@@ -3638,7 +4300,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
3638
4300
|
return "json";
|
|
3639
4301
|
}
|
|
3640
4302
|
function createRenderCommand() {
|
|
3641
|
-
const renderCmd = new
|
|
4303
|
+
const renderCmd = new Command7("render").description(
|
|
3642
4304
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
3643
4305
|
);
|
|
3644
4306
|
registerRenderSingle(renderCmd);
|
|
@@ -3648,9 +4310,9 @@ function createRenderCommand() {
|
|
|
3648
4310
|
}
|
|
3649
4311
|
|
|
3650
4312
|
// src/report/baseline.ts
|
|
3651
|
-
import { existsSync as
|
|
3652
|
-
import { resolve as
|
|
3653
|
-
import { generateManifest as
|
|
4313
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
4314
|
+
import { resolve as resolve11 } from "path";
|
|
4315
|
+
import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
|
|
3654
4316
|
import { BrowserPool as BrowserPool4, safeRender as safeRender3 } from "@agent-scope/render";
|
|
3655
4317
|
import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
|
|
3656
4318
|
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
@@ -3798,30 +4460,30 @@ async function runBaseline(options = {}) {
|
|
|
3798
4460
|
} = options;
|
|
3799
4461
|
const startTime = performance.now();
|
|
3800
4462
|
const rootDir = process.cwd();
|
|
3801
|
-
const baselineDir =
|
|
3802
|
-
const rendersDir =
|
|
3803
|
-
if (
|
|
4463
|
+
const baselineDir = resolve11(rootDir, outputDir);
|
|
4464
|
+
const rendersDir = resolve11(baselineDir, "renders");
|
|
4465
|
+
if (existsSync9(baselineDir)) {
|
|
3804
4466
|
rmSync2(baselineDir, { recursive: true, force: true });
|
|
3805
4467
|
}
|
|
3806
4468
|
mkdirSync5(rendersDir, { recursive: true });
|
|
3807
4469
|
let manifest;
|
|
3808
4470
|
if (manifestPath !== void 0) {
|
|
3809
|
-
const { readFileSync:
|
|
3810
|
-
const absPath =
|
|
3811
|
-
if (!
|
|
4471
|
+
const { readFileSync: readFileSync14 } = await import("fs");
|
|
4472
|
+
const absPath = resolve11(rootDir, manifestPath);
|
|
4473
|
+
if (!existsSync9(absPath)) {
|
|
3812
4474
|
throw new Error(`Manifest not found at ${absPath}.`);
|
|
3813
4475
|
}
|
|
3814
|
-
manifest = JSON.parse(
|
|
4476
|
+
manifest = JSON.parse(readFileSync14(absPath, "utf-8"));
|
|
3815
4477
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
3816
4478
|
`);
|
|
3817
4479
|
} else {
|
|
3818
4480
|
process.stderr.write("Scanning for React components\u2026\n");
|
|
3819
|
-
manifest = await
|
|
4481
|
+
manifest = await generateManifest4({ rootDir });
|
|
3820
4482
|
const count = Object.keys(manifest.components).length;
|
|
3821
4483
|
process.stderr.write(`Found ${count} components.
|
|
3822
4484
|
`);
|
|
3823
4485
|
}
|
|
3824
|
-
writeFileSync6(
|
|
4486
|
+
writeFileSync6(resolve11(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
3825
4487
|
let componentNames = Object.keys(manifest.components);
|
|
3826
4488
|
if (componentsGlob !== void 0) {
|
|
3827
4489
|
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
@@ -3842,7 +4504,7 @@ async function runBaseline(options = {}) {
|
|
|
3842
4504
|
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3843
4505
|
};
|
|
3844
4506
|
writeFileSync6(
|
|
3845
|
-
|
|
4507
|
+
resolve11(baselineDir, "compliance.json"),
|
|
3846
4508
|
JSON.stringify(emptyReport, null, 2),
|
|
3847
4509
|
"utf-8"
|
|
3848
4510
|
);
|
|
@@ -3863,7 +4525,7 @@ async function runBaseline(options = {}) {
|
|
|
3863
4525
|
const renderOne = async (name) => {
|
|
3864
4526
|
const descriptor = manifest.components[name];
|
|
3865
4527
|
if (descriptor === void 0) return;
|
|
3866
|
-
const filePath =
|
|
4528
|
+
const filePath = resolve11(rootDir, descriptor.filePath);
|
|
3867
4529
|
const outcome = await safeRender3(
|
|
3868
4530
|
() => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
|
|
3869
4531
|
{
|
|
@@ -3882,7 +4544,7 @@ async function runBaseline(options = {}) {
|
|
|
3882
4544
|
}
|
|
3883
4545
|
if (outcome.crashed) {
|
|
3884
4546
|
failureCount++;
|
|
3885
|
-
const errPath =
|
|
4547
|
+
const errPath = resolve11(rendersDir, `${name}.error.json`);
|
|
3886
4548
|
writeFileSync6(
|
|
3887
4549
|
errPath,
|
|
3888
4550
|
JSON.stringify(
|
|
@@ -3900,10 +4562,10 @@ async function runBaseline(options = {}) {
|
|
|
3900
4562
|
return;
|
|
3901
4563
|
}
|
|
3902
4564
|
const result = outcome.result;
|
|
3903
|
-
writeFileSync6(
|
|
4565
|
+
writeFileSync6(resolve11(rendersDir, `${name}.png`), result.screenshot);
|
|
3904
4566
|
const jsonOutput = formatRenderJson(name, {}, result);
|
|
3905
4567
|
writeFileSync6(
|
|
3906
|
-
|
|
4568
|
+
resolve11(rendersDir, `${name}.json`),
|
|
3907
4569
|
JSON.stringify(jsonOutput, null, 2),
|
|
3908
4570
|
"utf-8"
|
|
3909
4571
|
);
|
|
@@ -3931,7 +4593,7 @@ async function runBaseline(options = {}) {
|
|
|
3931
4593
|
const engine = new ComplianceEngine2(resolver);
|
|
3932
4594
|
const batchReport = engine.auditBatch(computedStylesMap);
|
|
3933
4595
|
writeFileSync6(
|
|
3934
|
-
|
|
4596
|
+
resolve11(baselineDir, "compliance.json"),
|
|
3935
4597
|
JSON.stringify(batchReport, null, 2),
|
|
3936
4598
|
"utf-8"
|
|
3937
4599
|
);
|
|
@@ -3974,22 +4636,22 @@ function registerBaselineSubCommand(reportCmd) {
|
|
|
3974
4636
|
}
|
|
3975
4637
|
|
|
3976
4638
|
// src/report/diff.ts
|
|
3977
|
-
import { existsSync as
|
|
3978
|
-
import { resolve as
|
|
3979
|
-
import { generateManifest as
|
|
4639
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4640
|
+
import { resolve as resolve12 } from "path";
|
|
4641
|
+
import { generateManifest as generateManifest5 } from "@agent-scope/manifest";
|
|
3980
4642
|
import { BrowserPool as BrowserPool5, safeRender as safeRender4 } from "@agent-scope/render";
|
|
3981
4643
|
import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3 } from "@agent-scope/tokens";
|
|
3982
4644
|
var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
|
|
3983
4645
|
function loadBaselineCompliance(baselineDir) {
|
|
3984
|
-
const compliancePath =
|
|
3985
|
-
if (!
|
|
3986
|
-
const raw = JSON.parse(
|
|
4646
|
+
const compliancePath = resolve12(baselineDir, "compliance.json");
|
|
4647
|
+
if (!existsSync10(compliancePath)) return null;
|
|
4648
|
+
const raw = JSON.parse(readFileSync8(compliancePath, "utf-8"));
|
|
3987
4649
|
return raw;
|
|
3988
4650
|
}
|
|
3989
4651
|
function loadBaselineRenderJson2(baselineDir, componentName) {
|
|
3990
|
-
const jsonPath =
|
|
3991
|
-
if (!
|
|
3992
|
-
return JSON.parse(
|
|
4652
|
+
const jsonPath = resolve12(baselineDir, "renders", `${componentName}.json`);
|
|
4653
|
+
if (!existsSync10(jsonPath)) return null;
|
|
4654
|
+
return JSON.parse(readFileSync8(jsonPath, "utf-8"));
|
|
3993
4655
|
}
|
|
3994
4656
|
var _pool5 = null;
|
|
3995
4657
|
async function getPool5(viewportWidth, viewportHeight) {
|
|
@@ -4156,19 +4818,19 @@ async function runDiff(options = {}) {
|
|
|
4156
4818
|
} = options;
|
|
4157
4819
|
const startTime = performance.now();
|
|
4158
4820
|
const rootDir = process.cwd();
|
|
4159
|
-
const baselineDir =
|
|
4160
|
-
if (!
|
|
4821
|
+
const baselineDir = resolve12(rootDir, baselineDirRaw);
|
|
4822
|
+
if (!existsSync10(baselineDir)) {
|
|
4161
4823
|
throw new Error(
|
|
4162
4824
|
`Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
|
|
4163
4825
|
);
|
|
4164
4826
|
}
|
|
4165
|
-
const baselineManifestPath =
|
|
4166
|
-
if (!
|
|
4827
|
+
const baselineManifestPath = resolve12(baselineDir, "manifest.json");
|
|
4828
|
+
if (!existsSync10(baselineManifestPath)) {
|
|
4167
4829
|
throw new Error(
|
|
4168
4830
|
`Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
|
|
4169
4831
|
);
|
|
4170
4832
|
}
|
|
4171
|
-
const baselineManifest = JSON.parse(
|
|
4833
|
+
const baselineManifest = JSON.parse(readFileSync8(baselineManifestPath, "utf-8"));
|
|
4172
4834
|
const baselineCompliance = loadBaselineCompliance(baselineDir);
|
|
4173
4835
|
const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
|
|
4174
4836
|
process.stderr.write(
|
|
@@ -4177,16 +4839,16 @@ async function runDiff(options = {}) {
|
|
|
4177
4839
|
);
|
|
4178
4840
|
let currentManifest;
|
|
4179
4841
|
if (manifestPath !== void 0) {
|
|
4180
|
-
const absPath =
|
|
4181
|
-
if (!
|
|
4842
|
+
const absPath = resolve12(rootDir, manifestPath);
|
|
4843
|
+
if (!existsSync10(absPath)) {
|
|
4182
4844
|
throw new Error(`Manifest not found at "${absPath}".`);
|
|
4183
4845
|
}
|
|
4184
|
-
currentManifest = JSON.parse(
|
|
4846
|
+
currentManifest = JSON.parse(readFileSync8(absPath, "utf-8"));
|
|
4185
4847
|
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
4186
4848
|
`);
|
|
4187
4849
|
} else {
|
|
4188
4850
|
process.stderr.write("Scanning for React components\u2026\n");
|
|
4189
|
-
currentManifest = await
|
|
4851
|
+
currentManifest = await generateManifest5({ rootDir });
|
|
4190
4852
|
const count = Object.keys(currentManifest.components).length;
|
|
4191
4853
|
process.stderr.write(`Found ${count} components.
|
|
4192
4854
|
`);
|
|
@@ -4214,7 +4876,7 @@ async function runDiff(options = {}) {
|
|
|
4214
4876
|
const renderOne = async (name) => {
|
|
4215
4877
|
const descriptor = currentManifest.components[name];
|
|
4216
4878
|
if (descriptor === void 0) return;
|
|
4217
|
-
const filePath =
|
|
4879
|
+
const filePath = resolve12(rootDir, descriptor.filePath);
|
|
4218
4880
|
const outcome = await safeRender4(
|
|
4219
4881
|
() => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
|
|
4220
4882
|
{
|
|
@@ -4454,8 +5116,8 @@ function registerDiffSubCommand(reportCmd) {
|
|
|
4454
5116
|
}
|
|
4455
5117
|
|
|
4456
5118
|
// src/report/pr-comment.ts
|
|
4457
|
-
import { existsSync as
|
|
4458
|
-
import { resolve as
|
|
5119
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
5120
|
+
import { resolve as resolve13 } from "path";
|
|
4459
5121
|
var STATUS_BADGE = {
|
|
4460
5122
|
added: "\u2705 added",
|
|
4461
5123
|
removed: "\u{1F5D1}\uFE0F removed",
|
|
@@ -4538,13 +5200,13 @@ function formatPrComment(diff) {
|
|
|
4538
5200
|
return lines.join("\n");
|
|
4539
5201
|
}
|
|
4540
5202
|
function loadDiffResult(filePath) {
|
|
4541
|
-
const abs =
|
|
4542
|
-
if (!
|
|
5203
|
+
const abs = resolve13(filePath);
|
|
5204
|
+
if (!existsSync11(abs)) {
|
|
4543
5205
|
throw new Error(`DiffResult file not found: ${abs}`);
|
|
4544
5206
|
}
|
|
4545
5207
|
let raw;
|
|
4546
5208
|
try {
|
|
4547
|
-
raw =
|
|
5209
|
+
raw = readFileSync9(abs, "utf-8");
|
|
4548
5210
|
} catch (err) {
|
|
4549
5211
|
throw new Error(
|
|
4550
5212
|
`Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4571,7 +5233,7 @@ function registerPrCommentSubCommand(reportCmd) {
|
|
|
4571
5233
|
const diff = loadDiffResult(opts.input);
|
|
4572
5234
|
const comment = formatPrComment(diff);
|
|
4573
5235
|
if (opts.output !== void 0) {
|
|
4574
|
-
writeFileSync8(
|
|
5236
|
+
writeFileSync8(resolve13(opts.output), comment, "utf-8");
|
|
4575
5237
|
process.stderr.write(`PR comment written to ${opts.output}
|
|
4576
5238
|
`);
|
|
4577
5239
|
} else {
|
|
@@ -4866,11 +5528,11 @@ function buildStructuredReport(report) {
|
|
|
4866
5528
|
}
|
|
4867
5529
|
|
|
4868
5530
|
// src/site-commands.ts
|
|
4869
|
-
import { createReadStream, existsSync as
|
|
5531
|
+
import { createReadStream, existsSync as existsSync12, statSync as statSync2 } from "fs";
|
|
4870
5532
|
import { createServer } from "http";
|
|
4871
|
-
import { extname, join as
|
|
5533
|
+
import { extname, join as join5, resolve as resolve14 } from "path";
|
|
4872
5534
|
import { buildSite } from "@agent-scope/site";
|
|
4873
|
-
import { Command as
|
|
5535
|
+
import { Command as Command8 } from "commander";
|
|
4874
5536
|
var MIME_TYPES = {
|
|
4875
5537
|
".html": "text/html; charset=utf-8",
|
|
4876
5538
|
".css": "text/css; charset=utf-8",
|
|
@@ -4886,16 +5548,16 @@ function registerBuild(siteCmd) {
|
|
|
4886
5548
|
siteCmd.command("build").description("Build a static HTML gallery from .reactscope/ output").option("-i, --input <path>", "Path to .reactscope input directory", ".reactscope").option("-o, --output <path>", "Output directory for generated site", ".reactscope/site").option("--base-path <path>", "Base URL path prefix for subdirectory deployment", "/").option("--compliance <path>", "Path to compliance batch report JSON").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
|
|
4887
5549
|
async (opts) => {
|
|
4888
5550
|
try {
|
|
4889
|
-
const inputDir =
|
|
4890
|
-
const outputDir =
|
|
4891
|
-
if (!
|
|
5551
|
+
const inputDir = resolve14(process.cwd(), opts.input);
|
|
5552
|
+
const outputDir = resolve14(process.cwd(), opts.output);
|
|
5553
|
+
if (!existsSync12(inputDir)) {
|
|
4892
5554
|
throw new Error(
|
|
4893
5555
|
`Input directory not found: ${inputDir}
|
|
4894
5556
|
Run \`scope manifest generate\` and \`scope render\` first.`
|
|
4895
5557
|
);
|
|
4896
5558
|
}
|
|
4897
|
-
const manifestPath =
|
|
4898
|
-
if (!
|
|
5559
|
+
const manifestPath = join5(inputDir, "manifest.json");
|
|
5560
|
+
if (!existsSync12(manifestPath)) {
|
|
4899
5561
|
throw new Error(
|
|
4900
5562
|
`Manifest not found at ${manifestPath}
|
|
4901
5563
|
Run \`scope manifest generate\` first.`
|
|
@@ -4908,7 +5570,7 @@ Run \`scope manifest generate\` first.`
|
|
|
4908
5570
|
outputDir,
|
|
4909
5571
|
basePath: opts.basePath,
|
|
4910
5572
|
...opts.compliance !== void 0 && {
|
|
4911
|
-
compliancePath:
|
|
5573
|
+
compliancePath: resolve14(process.cwd(), opts.compliance)
|
|
4912
5574
|
},
|
|
4913
5575
|
title: opts.title
|
|
4914
5576
|
});
|
|
@@ -4931,8 +5593,8 @@ function registerServe(siteCmd) {
|
|
|
4931
5593
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
4932
5594
|
throw new Error(`Invalid port: ${opts.port}`);
|
|
4933
5595
|
}
|
|
4934
|
-
const serveDir =
|
|
4935
|
-
if (!
|
|
5596
|
+
const serveDir = resolve14(process.cwd(), opts.dir);
|
|
5597
|
+
if (!existsSync12(serveDir)) {
|
|
4936
5598
|
throw new Error(
|
|
4937
5599
|
`Serve directory not found: ${serveDir}
|
|
4938
5600
|
Run \`scope site build\` first.`
|
|
@@ -4941,13 +5603,13 @@ Run \`scope site build\` first.`
|
|
|
4941
5603
|
const server = createServer((req, res) => {
|
|
4942
5604
|
const rawUrl = req.url ?? "/";
|
|
4943
5605
|
const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
|
|
4944
|
-
const filePath =
|
|
5606
|
+
const filePath = join5(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
|
|
4945
5607
|
if (!filePath.startsWith(serveDir)) {
|
|
4946
5608
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
4947
5609
|
res.end("Forbidden");
|
|
4948
5610
|
return;
|
|
4949
5611
|
}
|
|
4950
|
-
if (
|
|
5612
|
+
if (existsSync12(filePath) && statSync2(filePath).isFile()) {
|
|
4951
5613
|
const ext = extname(filePath).toLowerCase();
|
|
4952
5614
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4953
5615
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -4955,7 +5617,7 @@ Run \`scope site build\` first.`
|
|
|
4955
5617
|
return;
|
|
4956
5618
|
}
|
|
4957
5619
|
const htmlPath = `${filePath}.html`;
|
|
4958
|
-
if (
|
|
5620
|
+
if (existsSync12(htmlPath) && statSync2(htmlPath).isFile()) {
|
|
4959
5621
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4960
5622
|
createReadStream(htmlPath).pipe(res);
|
|
4961
5623
|
return;
|
|
@@ -4988,7 +5650,7 @@ Run \`scope site build\` first.`
|
|
|
4988
5650
|
});
|
|
4989
5651
|
}
|
|
4990
5652
|
function createSiteCommand() {
|
|
4991
|
-
const siteCmd = new
|
|
5653
|
+
const siteCmd = new Command8("site").description(
|
|
4992
5654
|
"Build and serve the static component gallery site"
|
|
4993
5655
|
);
|
|
4994
5656
|
registerBuild(siteCmd);
|
|
@@ -4997,8 +5659,8 @@ function createSiteCommand() {
|
|
|
4997
5659
|
}
|
|
4998
5660
|
|
|
4999
5661
|
// src/tokens/commands.ts
|
|
5000
|
-
import { existsSync as
|
|
5001
|
-
import { resolve as
|
|
5662
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
|
|
5663
|
+
import { resolve as resolve18 } from "path";
|
|
5002
5664
|
import {
|
|
5003
5665
|
parseTokenFileSync as parseTokenFileSync2,
|
|
5004
5666
|
TokenParseError,
|
|
@@ -5006,26 +5668,26 @@ import {
|
|
|
5006
5668
|
TokenValidationError,
|
|
5007
5669
|
validateTokenFile
|
|
5008
5670
|
} from "@agent-scope/tokens";
|
|
5009
|
-
import { Command as
|
|
5671
|
+
import { Command as Command10 } from "commander";
|
|
5010
5672
|
|
|
5011
5673
|
// src/tokens/compliance.ts
|
|
5012
|
-
import { existsSync as
|
|
5013
|
-
import { resolve as
|
|
5674
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
5675
|
+
import { resolve as resolve15 } from "path";
|
|
5014
5676
|
import {
|
|
5015
5677
|
ComplianceEngine as ComplianceEngine4,
|
|
5016
5678
|
TokenResolver as TokenResolver4
|
|
5017
5679
|
} from "@agent-scope/tokens";
|
|
5018
5680
|
var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
|
|
5019
5681
|
function loadStylesFile(stylesPath) {
|
|
5020
|
-
const absPath =
|
|
5021
|
-
if (!
|
|
5682
|
+
const absPath = resolve15(process.cwd(), stylesPath);
|
|
5683
|
+
if (!existsSync13(absPath)) {
|
|
5022
5684
|
throw new Error(
|
|
5023
5685
|
`Compliance styles file not found at ${absPath}.
|
|
5024
5686
|
Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
|
|
5025
5687
|
Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
|
|
5026
5688
|
);
|
|
5027
5689
|
}
|
|
5028
|
-
const raw =
|
|
5690
|
+
const raw = readFileSync10(absPath, "utf-8");
|
|
5029
5691
|
let parsed;
|
|
5030
5692
|
try {
|
|
5031
5693
|
parsed = JSON.parse(raw);
|
|
@@ -5185,60 +5847,70 @@ function registerCompliance(tokensCmd) {
|
|
|
5185
5847
|
}
|
|
5186
5848
|
|
|
5187
5849
|
// src/tokens/export.ts
|
|
5188
|
-
import { existsSync as
|
|
5189
|
-
import { resolve as
|
|
5850
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
5851
|
+
import { resolve as resolve16 } from "path";
|
|
5190
5852
|
import {
|
|
5191
5853
|
exportTokens,
|
|
5192
5854
|
parseTokenFileSync,
|
|
5193
5855
|
ThemeResolver,
|
|
5194
5856
|
TokenResolver as TokenResolver5
|
|
5195
5857
|
} from "@agent-scope/tokens";
|
|
5196
|
-
import { Command as
|
|
5858
|
+
import { Command as Command9 } from "commander";
|
|
5197
5859
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
5198
5860
|
var CONFIG_FILE = "reactscope.config.json";
|
|
5199
5861
|
var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
|
|
5200
5862
|
function resolveTokenFilePath2(fileFlag) {
|
|
5201
5863
|
if (fileFlag !== void 0) {
|
|
5202
|
-
return
|
|
5864
|
+
return resolve16(process.cwd(), fileFlag);
|
|
5203
5865
|
}
|
|
5204
|
-
const configPath =
|
|
5205
|
-
if (
|
|
5866
|
+
const configPath = resolve16(process.cwd(), CONFIG_FILE);
|
|
5867
|
+
if (existsSync14(configPath)) {
|
|
5206
5868
|
try {
|
|
5207
|
-
const raw =
|
|
5869
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
5208
5870
|
const config = JSON.parse(raw);
|
|
5209
5871
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
5210
5872
|
const file = config.tokens.file;
|
|
5211
|
-
return
|
|
5873
|
+
return resolve16(process.cwd(), file);
|
|
5212
5874
|
}
|
|
5213
5875
|
} catch {
|
|
5214
5876
|
}
|
|
5215
5877
|
}
|
|
5216
|
-
return
|
|
5878
|
+
return resolve16(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
5217
5879
|
}
|
|
5218
5880
|
function createTokensExportCommand() {
|
|
5219
|
-
return new
|
|
5881
|
+
return new Command9("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
|
|
5220
5882
|
"--theme <name>",
|
|
5221
5883
|
"Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
|
|
5222
5884
|
).action(
|
|
5223
5885
|
(opts) => {
|
|
5224
5886
|
if (!SUPPORTED_FORMATS.includes(opts.format)) {
|
|
5887
|
+
const FORMAT_ALIASES = {
|
|
5888
|
+
json: "flat-json",
|
|
5889
|
+
"json-flat": "flat-json",
|
|
5890
|
+
javascript: "ts",
|
|
5891
|
+
js: "ts",
|
|
5892
|
+
sass: "scss",
|
|
5893
|
+
tw: "tailwind"
|
|
5894
|
+
};
|
|
5895
|
+
const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
|
|
5225
5896
|
process.stderr.write(
|
|
5226
5897
|
`Error: unsupported format "${opts.format}".
|
|
5227
5898
|
Supported formats: ${SUPPORTED_FORMATS.join(", ")}
|
|
5228
|
-
`
|
|
5899
|
+
` + (hint ? `Did you mean "${hint}"?
|
|
5900
|
+
` : "")
|
|
5229
5901
|
);
|
|
5230
5902
|
process.exit(1);
|
|
5231
5903
|
}
|
|
5232
5904
|
const format = opts.format;
|
|
5233
5905
|
try {
|
|
5234
5906
|
const filePath = resolveTokenFilePath2(opts.file);
|
|
5235
|
-
if (!
|
|
5907
|
+
if (!existsSync14(filePath)) {
|
|
5236
5908
|
throw new Error(
|
|
5237
5909
|
`Token file not found at ${filePath}.
|
|
5238
5910
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5239
5911
|
);
|
|
5240
5912
|
}
|
|
5241
|
-
const raw =
|
|
5913
|
+
const raw = readFileSync11(filePath, "utf-8");
|
|
5242
5914
|
const { tokens, rawFile } = parseTokenFileSync(raw);
|
|
5243
5915
|
let themesMap;
|
|
5244
5916
|
if (opts.theme !== void 0) {
|
|
@@ -5277,7 +5949,7 @@ Available themes: ${themeNames.join(", ")}`
|
|
|
5277
5949
|
themes: themesMap
|
|
5278
5950
|
});
|
|
5279
5951
|
if (opts.out !== void 0) {
|
|
5280
|
-
const outPath =
|
|
5952
|
+
const outPath = resolve16(process.cwd(), opts.out);
|
|
5281
5953
|
writeFileSync9(outPath, output, "utf-8");
|
|
5282
5954
|
process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
|
|
5283
5955
|
`);
|
|
@@ -5393,7 +6065,7 @@ ${formatImpactSummary(report)}
|
|
|
5393
6065
|
|
|
5394
6066
|
// src/tokens/preview.ts
|
|
5395
6067
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
5396
|
-
import { resolve as
|
|
6068
|
+
import { resolve as resolve17 } from "path";
|
|
5397
6069
|
import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
|
|
5398
6070
|
import { ComplianceEngine as ComplianceEngine6, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver7 } from "@agent-scope/tokens";
|
|
5399
6071
|
var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
|
|
@@ -5574,8 +6246,8 @@ function registerPreview(tokensCmd) {
|
|
|
5574
6246
|
});
|
|
5575
6247
|
const spriteResult = await generator.generate(matrixResult);
|
|
5576
6248
|
const tokenLabel = tokenPath.replace(/\./g, "-");
|
|
5577
|
-
const outputPath = opts.output ??
|
|
5578
|
-
const outputDir =
|
|
6249
|
+
const outputPath = opts.output ?? resolve17(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
|
|
6250
|
+
const outputDir = resolve17(outputPath, "..");
|
|
5579
6251
|
mkdirSync6(outputDir, { recursive: true });
|
|
5580
6252
|
writeFileSync10(outputPath, spriteResult.png);
|
|
5581
6253
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
|
|
@@ -5636,30 +6308,30 @@ function buildTable2(headers, rows) {
|
|
|
5636
6308
|
}
|
|
5637
6309
|
function resolveTokenFilePath(fileFlag) {
|
|
5638
6310
|
if (fileFlag !== void 0) {
|
|
5639
|
-
return
|
|
6311
|
+
return resolve18(process.cwd(), fileFlag);
|
|
5640
6312
|
}
|
|
5641
|
-
const configPath =
|
|
5642
|
-
if (
|
|
6313
|
+
const configPath = resolve18(process.cwd(), CONFIG_FILE2);
|
|
6314
|
+
if (existsSync15(configPath)) {
|
|
5643
6315
|
try {
|
|
5644
|
-
const raw =
|
|
6316
|
+
const raw = readFileSync12(configPath, "utf-8");
|
|
5645
6317
|
const config = JSON.parse(raw);
|
|
5646
6318
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
5647
6319
|
const file = config.tokens.file;
|
|
5648
|
-
return
|
|
6320
|
+
return resolve18(process.cwd(), file);
|
|
5649
6321
|
}
|
|
5650
6322
|
} catch {
|
|
5651
6323
|
}
|
|
5652
6324
|
}
|
|
5653
|
-
return
|
|
6325
|
+
return resolve18(process.cwd(), DEFAULT_TOKEN_FILE2);
|
|
5654
6326
|
}
|
|
5655
6327
|
function loadTokens(absPath) {
|
|
5656
|
-
if (!
|
|
6328
|
+
if (!existsSync15(absPath)) {
|
|
5657
6329
|
throw new Error(
|
|
5658
6330
|
`Token file not found at ${absPath}.
|
|
5659
6331
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5660
6332
|
);
|
|
5661
6333
|
}
|
|
5662
|
-
const raw =
|
|
6334
|
+
const raw = readFileSync12(absPath, "utf-8");
|
|
5663
6335
|
return parseTokenFileSync2(raw);
|
|
5664
6336
|
}
|
|
5665
6337
|
function getRawValue(node, segments) {
|
|
@@ -5873,13 +6545,13 @@ function registerValidate(tokensCmd) {
|
|
|
5873
6545
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
5874
6546
|
try {
|
|
5875
6547
|
const filePath = resolveTokenFilePath(opts.file);
|
|
5876
|
-
if (!
|
|
6548
|
+
if (!existsSync15(filePath)) {
|
|
5877
6549
|
throw new Error(
|
|
5878
6550
|
`Token file not found at ${filePath}.
|
|
5879
6551
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
5880
6552
|
);
|
|
5881
6553
|
}
|
|
5882
|
-
const raw =
|
|
6554
|
+
const raw = readFileSync12(filePath, "utf-8");
|
|
5883
6555
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
5884
6556
|
const errors = [];
|
|
5885
6557
|
let parsed;
|
|
@@ -5947,7 +6619,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
5947
6619
|
}
|
|
5948
6620
|
}
|
|
5949
6621
|
function createTokensCommand() {
|
|
5950
|
-
const tokensCmd = new
|
|
6622
|
+
const tokensCmd = new Command10("tokens").description(
|
|
5951
6623
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
5952
6624
|
);
|
|
5953
6625
|
registerGet2(tokensCmd);
|
|
@@ -5964,7 +6636,7 @@ function createTokensCommand() {
|
|
|
5964
6636
|
|
|
5965
6637
|
// src/program.ts
|
|
5966
6638
|
function createProgram(options = {}) {
|
|
5967
|
-
const program2 = new
|
|
6639
|
+
const program2 = new Command11("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
5968
6640
|
program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
|
|
5969
6641
|
async (url, opts) => {
|
|
5970
6642
|
try {
|
|
@@ -6037,7 +6709,7 @@ function createProgram(options = {}) {
|
|
|
6037
6709
|
}
|
|
6038
6710
|
);
|
|
6039
6711
|
program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
|
|
6040
|
-
const raw =
|
|
6712
|
+
const raw = readFileSync13(tracePath, "utf-8");
|
|
6041
6713
|
const trace = loadTrace(raw);
|
|
6042
6714
|
const source = generateTest(trace, {
|
|
6043
6715
|
description: opts.description,
|
|
@@ -6052,6 +6724,7 @@ function createProgram(options = {}) {
|
|
|
6052
6724
|
program2.addCommand(createInstrumentCommand());
|
|
6053
6725
|
program2.addCommand(createInitCommand());
|
|
6054
6726
|
program2.addCommand(createCiCommand());
|
|
6727
|
+
program2.addCommand(createDoctorCommand());
|
|
6055
6728
|
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
6056
6729
|
if (existingReportCmd !== void 0) {
|
|
6057
6730
|
registerBaselineSubCommand(existingReportCmd);
|