@agent-scope/cli 1.8.0 → 1.10.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 +749 -63
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +673 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +674 -6
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5
5
|
import { generateTest, loadTrace } from "@agent-scope/playwright";
|
|
6
|
-
import { Command as
|
|
6
|
+
import { Command as Command6 } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/browser.ts
|
|
9
9
|
import { writeFileSync } from "fs";
|
|
@@ -50,10 +50,365 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
50
50
|
writeFileSync(outputPath, json, "utf-8");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// src/init/index.ts
|
|
54
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
55
|
+
import { join as join2 } from "path";
|
|
56
|
+
import * as readline from "readline";
|
|
57
|
+
|
|
58
|
+
// src/init/detect.ts
|
|
59
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
60
|
+
import { join } from "path";
|
|
61
|
+
function hasConfigFile(dir, stem) {
|
|
62
|
+
if (!existsSync(dir)) return false;
|
|
63
|
+
try {
|
|
64
|
+
const entries = readdirSync(dir);
|
|
65
|
+
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function readSafe(path) {
|
|
71
|
+
try {
|
|
72
|
+
return readFileSync(path, "utf-8");
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function detectFramework(rootDir, packageDeps) {
|
|
78
|
+
if (hasConfigFile(rootDir, "next.config")) return "next";
|
|
79
|
+
if (hasConfigFile(rootDir, "vite.config")) return "vite";
|
|
80
|
+
if (hasConfigFile(rootDir, "remix.config")) return "remix";
|
|
81
|
+
if ("react-scripts" in packageDeps) return "cra";
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
function detectPackageManager(rootDir) {
|
|
85
|
+
if (existsSync(join(rootDir, "bun.lock"))) return "bun";
|
|
86
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
87
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
88
|
+
if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
|
|
89
|
+
return "npm";
|
|
90
|
+
}
|
|
91
|
+
function detectTypeScript(rootDir) {
|
|
92
|
+
const candidate = join(rootDir, "tsconfig.json");
|
|
93
|
+
if (existsSync(candidate)) {
|
|
94
|
+
return { typescript: true, tsconfigPath: candidate };
|
|
95
|
+
}
|
|
96
|
+
return { typescript: false, tsconfigPath: null };
|
|
97
|
+
}
|
|
98
|
+
var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
|
|
99
|
+
var COMPONENT_EXTS = [".tsx", ".jsx"];
|
|
100
|
+
function detectComponentPatterns(rootDir, typescript) {
|
|
101
|
+
const patterns = [];
|
|
102
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
103
|
+
const altExt = typescript ? "jsx" : "jsx";
|
|
104
|
+
for (const dir of COMPONENT_DIRS) {
|
|
105
|
+
const absDir = join(rootDir, dir);
|
|
106
|
+
if (!existsSync(absDir)) continue;
|
|
107
|
+
let hasComponents = false;
|
|
108
|
+
try {
|
|
109
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
110
|
+
hasComponents = entries.some(
|
|
111
|
+
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
112
|
+
);
|
|
113
|
+
if (!hasComponents) {
|
|
114
|
+
hasComponents = entries.some(
|
|
115
|
+
(e) => e.isDirectory() && (() => {
|
|
116
|
+
try {
|
|
117
|
+
return readdirSync(join(absDir, e.name)).some(
|
|
118
|
+
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
})()
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (hasComponents) {
|
|
130
|
+
patterns.push(`${dir}/**/*.${ext}`);
|
|
131
|
+
if (altExt !== ext) {
|
|
132
|
+
patterns.push(`${dir}/**/*.${altExt}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const unique = [...new Set(patterns)];
|
|
137
|
+
if (unique.length === 0) {
|
|
138
|
+
return [`**/*.${ext}`];
|
|
139
|
+
}
|
|
140
|
+
return unique;
|
|
141
|
+
}
|
|
142
|
+
var TAILWIND_STEMS = ["tailwind.config"];
|
|
143
|
+
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
144
|
+
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
145
|
+
var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
|
|
146
|
+
function detectTokenSources(rootDir) {
|
|
147
|
+
const sources = [];
|
|
148
|
+
for (const stem of TAILWIND_STEMS) {
|
|
149
|
+
if (hasConfigFile(rootDir, stem)) {
|
|
150
|
+
try {
|
|
151
|
+
const entries = readdirSync(rootDir);
|
|
152
|
+
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
153
|
+
if (match) {
|
|
154
|
+
sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const srcDir = join(rootDir, "src");
|
|
161
|
+
const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
|
|
162
|
+
for (const scanDir of dirsToScan) {
|
|
163
|
+
try {
|
|
164
|
+
const entries = readdirSync(scanDir, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
167
|
+
const filePath = join(scanDir, entry.name);
|
|
168
|
+
const content = readSafe(filePath);
|
|
169
|
+
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
170
|
+
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (existsSync(srcDir)) {
|
|
178
|
+
try {
|
|
179
|
+
const entries = readdirSync(srcDir);
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
182
|
+
sources.push({ kind: "theme-file", path: join(srcDir, entry) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return sources;
|
|
189
|
+
}
|
|
190
|
+
function detectProject(rootDir) {
|
|
191
|
+
const pkgPath = join(rootDir, "package.json");
|
|
192
|
+
let packageDeps = {};
|
|
193
|
+
const pkgContent = readSafe(pkgPath);
|
|
194
|
+
if (pkgContent !== null) {
|
|
195
|
+
try {
|
|
196
|
+
const pkg = JSON.parse(pkgContent);
|
|
197
|
+
packageDeps = {
|
|
198
|
+
...pkg.dependencies,
|
|
199
|
+
...pkg.devDependencies
|
|
200
|
+
};
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const framework = detectFramework(rootDir, packageDeps);
|
|
205
|
+
const { typescript, tsconfigPath } = detectTypeScript(rootDir);
|
|
206
|
+
const packageManager = detectPackageManager(rootDir);
|
|
207
|
+
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
208
|
+
const tokenSources = detectTokenSources(rootDir);
|
|
209
|
+
return {
|
|
210
|
+
framework,
|
|
211
|
+
typescript,
|
|
212
|
+
tsconfigPath,
|
|
213
|
+
componentPatterns,
|
|
214
|
+
tokenSources,
|
|
215
|
+
packageManager
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/init/index.ts
|
|
220
|
+
import { Command } from "commander";
|
|
221
|
+
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
222
|
+
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
223
|
+
return {
|
|
224
|
+
components: {
|
|
225
|
+
include,
|
|
226
|
+
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
227
|
+
wrappers: { providers: [], globalCSS: [] }
|
|
228
|
+
},
|
|
229
|
+
render: {
|
|
230
|
+
viewport: { default: { width: 1280, height: 800 } },
|
|
231
|
+
theme: "light",
|
|
232
|
+
warmBrowser: true
|
|
233
|
+
},
|
|
234
|
+
tokens: {
|
|
235
|
+
file: tokenFile,
|
|
236
|
+
compliance: { threshold: 90 }
|
|
237
|
+
},
|
|
238
|
+
output: {
|
|
239
|
+
dir: outputDir,
|
|
240
|
+
sprites: { format: "png", cellPadding: 8, labelAxes: true },
|
|
241
|
+
json: { pretty: true }
|
|
242
|
+
},
|
|
243
|
+
ci: {
|
|
244
|
+
complianceThreshold: 90,
|
|
245
|
+
failOnA11yViolations: true,
|
|
246
|
+
failOnConsoleErrors: false,
|
|
247
|
+
baselinePath: `${outputDir}baseline/`
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function createRL() {
|
|
252
|
+
return readline.createInterface({
|
|
253
|
+
input: process.stdin,
|
|
254
|
+
output: process.stdout
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async function ask(rl, question) {
|
|
258
|
+
return new Promise((resolve7) => {
|
|
259
|
+
rl.question(question, (answer) => {
|
|
260
|
+
resolve7(answer.trim());
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async function askWithDefault(rl, label, defaultValue) {
|
|
265
|
+
const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
|
|
266
|
+
return answer.length > 0 ? answer : defaultValue;
|
|
267
|
+
}
|
|
268
|
+
function ensureGitignoreEntry(rootDir, entry) {
|
|
269
|
+
const gitignorePath = join2(rootDir, ".gitignore");
|
|
270
|
+
if (existsSync2(gitignorePath)) {
|
|
271
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
272
|
+
const normalised = entry.replace(/\/$/, "");
|
|
273
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
274
|
+
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
278
|
+
appendFileSync(gitignorePath, `${suffix}${entry}
|
|
279
|
+
`);
|
|
280
|
+
} else {
|
|
281
|
+
writeFileSync2(gitignorePath, `${entry}
|
|
282
|
+
`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function scaffoldConfig(rootDir, config) {
|
|
286
|
+
const path = join2(rootDir, "reactscope.config.json");
|
|
287
|
+
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
288
|
+
`);
|
|
289
|
+
return path;
|
|
290
|
+
}
|
|
291
|
+
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
292
|
+
const path = join2(rootDir, tokenFile);
|
|
293
|
+
if (!existsSync2(path)) {
|
|
294
|
+
const stub = {
|
|
295
|
+
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
296
|
+
tokens: {}
|
|
297
|
+
};
|
|
298
|
+
writeFileSync2(path, `${JSON.stringify(stub, null, 2)}
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
return path;
|
|
302
|
+
}
|
|
303
|
+
function scaffoldOutputDir(rootDir, outputDir) {
|
|
304
|
+
const dirPath = join2(rootDir, outputDir);
|
|
305
|
+
mkdirSync(dirPath, { recursive: true });
|
|
306
|
+
const keepPath = join2(dirPath, ".gitkeep");
|
|
307
|
+
if (!existsSync2(keepPath)) {
|
|
308
|
+
writeFileSync2(keepPath, "");
|
|
309
|
+
}
|
|
310
|
+
return dirPath;
|
|
311
|
+
}
|
|
312
|
+
async function runInit(options) {
|
|
313
|
+
const rootDir = options.cwd ?? process.cwd();
|
|
314
|
+
const configPath = join2(rootDir, "reactscope.config.json");
|
|
315
|
+
const created = [];
|
|
316
|
+
if (existsSync2(configPath) && !options.force) {
|
|
317
|
+
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
318
|
+
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
319
|
+
`);
|
|
320
|
+
return { success: false, message: msg, created: [], skipped: true };
|
|
321
|
+
}
|
|
322
|
+
const detected = detectProject(rootDir);
|
|
323
|
+
const defaultTokenFile = "reactscope.tokens.json";
|
|
324
|
+
const defaultOutputDir = ".reactscope/";
|
|
325
|
+
let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
|
|
326
|
+
if (options.yes) {
|
|
327
|
+
process.stdout.write("\n\u{1F50D} Detected project settings:\n");
|
|
328
|
+
process.stdout.write(` Framework : ${detected.framework}
|
|
329
|
+
`);
|
|
330
|
+
process.stdout.write(` TypeScript : ${detected.typescript}
|
|
331
|
+
`);
|
|
332
|
+
process.stdout.write(` Include globs : ${config.components.include.join(", ")}
|
|
333
|
+
`);
|
|
334
|
+
process.stdout.write(` Token file : ${config.tokens.file}
|
|
335
|
+
`);
|
|
336
|
+
process.stdout.write(` Output dir : ${config.output.dir}
|
|
337
|
+
|
|
338
|
+
`);
|
|
339
|
+
} else {
|
|
340
|
+
const rl = createRL();
|
|
341
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
342
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
343
|
+
try {
|
|
344
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
345
|
+
`);
|
|
346
|
+
const includeRaw = await askWithDefault(
|
|
347
|
+
rl,
|
|
348
|
+
"Component include patterns (comma-separated)",
|
|
349
|
+
config.components.include.join(", ")
|
|
350
|
+
);
|
|
351
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
352
|
+
const excludeRaw = await askWithDefault(
|
|
353
|
+
rl,
|
|
354
|
+
"Component exclude patterns (comma-separated)",
|
|
355
|
+
config.components.exclude.join(", ")
|
|
356
|
+
);
|
|
357
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
358
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
359
|
+
config.tokens.file = tokenFile;
|
|
360
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
361
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
362
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
363
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
364
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
365
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
366
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
367
|
+
} finally {
|
|
368
|
+
rl.close();
|
|
369
|
+
}
|
|
370
|
+
process.stdout.write("\n");
|
|
371
|
+
}
|
|
372
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
373
|
+
created.push(cfgPath);
|
|
374
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
375
|
+
created.push(tokPath);
|
|
376
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
377
|
+
created.push(outDirPath);
|
|
378
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
379
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
380
|
+
process.stdout.write(" Created files:\n");
|
|
381
|
+
for (const p of created) {
|
|
382
|
+
process.stdout.write(` ${p}
|
|
383
|
+
`);
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
message: "Project initialised successfully.",
|
|
389
|
+
created,
|
|
390
|
+
skipped: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function createInitCommand() {
|
|
394
|
+
return new Command("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) => {
|
|
395
|
+
try {
|
|
396
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
397
|
+
if (!result.success && !result.skipped) {
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
402
|
+
`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
53
408
|
// src/instrument/renders.ts
|
|
54
409
|
import { resolve as resolve2 } from "path";
|
|
55
410
|
import { BrowserPool } from "@agent-scope/render";
|
|
56
|
-
import { Command as
|
|
411
|
+
import { Command as Command3 } from "commander";
|
|
57
412
|
|
|
58
413
|
// src/component-bundler.ts
|
|
59
414
|
import { dirname } from "path";
|
|
@@ -170,10 +525,10 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
|
170
525
|
}
|
|
171
526
|
|
|
172
527
|
// src/manifest-commands.ts
|
|
173
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync as
|
|
528
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
174
529
|
import { resolve } from "path";
|
|
175
530
|
import { generateManifest } from "@agent-scope/manifest";
|
|
176
|
-
import { Command } from "commander";
|
|
531
|
+
import { Command as Command2 } from "commander";
|
|
177
532
|
|
|
178
533
|
// src/manifest-formatter.ts
|
|
179
534
|
function isTTY() {
|
|
@@ -274,11 +629,11 @@ function matchGlob(pattern, value) {
|
|
|
274
629
|
var MANIFEST_PATH = ".reactscope/manifest.json";
|
|
275
630
|
function loadManifest(manifestPath = MANIFEST_PATH) {
|
|
276
631
|
const absPath = resolve(process.cwd(), manifestPath);
|
|
277
|
-
if (!
|
|
632
|
+
if (!existsSync3(absPath)) {
|
|
278
633
|
throw new Error(`Manifest not found at ${absPath}.
|
|
279
634
|
Run \`scope manifest generate\` first.`);
|
|
280
635
|
}
|
|
281
|
-
const raw =
|
|
636
|
+
const raw = readFileSync3(absPath, "utf-8");
|
|
282
637
|
return JSON.parse(raw);
|
|
283
638
|
}
|
|
284
639
|
function resolveFormat(formatFlag) {
|
|
@@ -416,10 +771,10 @@ function registerGenerate(manifestCmd) {
|
|
|
416
771
|
process.stderr.write(`Found ${componentCount} components.
|
|
417
772
|
`);
|
|
418
773
|
const outputDir = outputPath.replace(/\/[^/]+$/, "");
|
|
419
|
-
if (!
|
|
420
|
-
|
|
774
|
+
if (!existsSync3(outputDir)) {
|
|
775
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
421
776
|
}
|
|
422
|
-
|
|
777
|
+
writeFileSync3(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
423
778
|
process.stderr.write(`Manifest written to ${outputPath}
|
|
424
779
|
`);
|
|
425
780
|
process.stdout.write(`${outputPath}
|
|
@@ -432,7 +787,7 @@ function registerGenerate(manifestCmd) {
|
|
|
432
787
|
});
|
|
433
788
|
}
|
|
434
789
|
function createManifestCommand() {
|
|
435
|
-
const manifestCmd = new
|
|
790
|
+
const manifestCmd = new Command2("manifest").description(
|
|
436
791
|
"Query and explore the component manifest"
|
|
437
792
|
);
|
|
438
793
|
registerList(manifestCmd);
|
|
@@ -1017,7 +1372,7 @@ function formatRendersTable(result) {
|
|
|
1017
1372
|
return lines.join("\n");
|
|
1018
1373
|
}
|
|
1019
1374
|
function createInstrumentRendersCommand() {
|
|
1020
|
-
return new
|
|
1375
|
+
return new Command3("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(
|
|
1021
1376
|
"--interaction <json>",
|
|
1022
1377
|
`Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
|
|
1023
1378
|
"[]"
|
|
@@ -1062,7 +1417,7 @@ function createInstrumentRendersCommand() {
|
|
|
1062
1417
|
);
|
|
1063
1418
|
}
|
|
1064
1419
|
function createInstrumentCommand() {
|
|
1065
|
-
const instrumentCmd = new
|
|
1420
|
+
const instrumentCmd = new Command3("instrument").description(
|
|
1066
1421
|
"Structured instrumentation commands for React component analysis"
|
|
1067
1422
|
);
|
|
1068
1423
|
instrumentCmd.addCommand(createInstrumentRendersCommand());
|
|
@@ -1070,7 +1425,7 @@ function createInstrumentCommand() {
|
|
|
1070
1425
|
}
|
|
1071
1426
|
|
|
1072
1427
|
// src/render-commands.ts
|
|
1073
|
-
import { mkdirSync as
|
|
1428
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1074
1429
|
import { resolve as resolve4 } from "path";
|
|
1075
1430
|
import {
|
|
1076
1431
|
ALL_CONTEXT_IDS,
|
|
@@ -1082,10 +1437,10 @@ import {
|
|
|
1082
1437
|
safeRender,
|
|
1083
1438
|
stressAxis
|
|
1084
1439
|
} from "@agent-scope/render";
|
|
1085
|
-
import { Command as
|
|
1440
|
+
import { Command as Command4 } from "commander";
|
|
1086
1441
|
|
|
1087
1442
|
// src/tailwind-css.ts
|
|
1088
|
-
import { existsSync as
|
|
1443
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1089
1444
|
import { createRequire } from "module";
|
|
1090
1445
|
import { resolve as resolve3 } from "path";
|
|
1091
1446
|
var CONFIG_FILENAMES = [
|
|
@@ -1112,39 +1467,39 @@ function getCachedBuild(cwd) {
|
|
|
1112
1467
|
function findStylesEntry(cwd) {
|
|
1113
1468
|
for (const name of CONFIG_FILENAMES) {
|
|
1114
1469
|
const p = resolve3(cwd, name);
|
|
1115
|
-
if (!
|
|
1470
|
+
if (!existsSync4(p)) continue;
|
|
1116
1471
|
try {
|
|
1117
1472
|
if (name.endsWith(".json")) {
|
|
1118
|
-
const raw =
|
|
1473
|
+
const raw = readFileSync4(p, "utf-8");
|
|
1119
1474
|
const data = JSON.parse(raw);
|
|
1120
1475
|
const scope = data.scope;
|
|
1121
1476
|
const entry = scope?.stylesEntry ?? data.stylesEntry;
|
|
1122
1477
|
if (typeof entry === "string") {
|
|
1123
1478
|
const full = resolve3(cwd, entry);
|
|
1124
|
-
if (
|
|
1479
|
+
if (existsSync4(full)) return full;
|
|
1125
1480
|
}
|
|
1126
1481
|
}
|
|
1127
1482
|
} catch {
|
|
1128
1483
|
}
|
|
1129
1484
|
}
|
|
1130
1485
|
const pkgPath = resolve3(cwd, "package.json");
|
|
1131
|
-
if (
|
|
1486
|
+
if (existsSync4(pkgPath)) {
|
|
1132
1487
|
try {
|
|
1133
|
-
const raw =
|
|
1488
|
+
const raw = readFileSync4(pkgPath, "utf-8");
|
|
1134
1489
|
const pkg = JSON.parse(raw);
|
|
1135
1490
|
const entry = pkg.scope?.stylesEntry;
|
|
1136
1491
|
if (typeof entry === "string") {
|
|
1137
1492
|
const full = resolve3(cwd, entry);
|
|
1138
|
-
if (
|
|
1493
|
+
if (existsSync4(full)) return full;
|
|
1139
1494
|
}
|
|
1140
1495
|
} catch {
|
|
1141
1496
|
}
|
|
1142
1497
|
}
|
|
1143
1498
|
for (const candidate of STYLE_ENTRY_CANDIDATES) {
|
|
1144
1499
|
const full = resolve3(cwd, candidate);
|
|
1145
|
-
if (
|
|
1500
|
+
if (existsSync4(full)) {
|
|
1146
1501
|
try {
|
|
1147
|
-
const content =
|
|
1502
|
+
const content = readFileSync4(full, "utf-8");
|
|
1148
1503
|
if (TAILWIND_IMPORT.test(content)) return full;
|
|
1149
1504
|
} catch {
|
|
1150
1505
|
}
|
|
@@ -1167,22 +1522,22 @@ async function getTailwindCompiler(cwd) {
|
|
|
1167
1522
|
} catch {
|
|
1168
1523
|
return null;
|
|
1169
1524
|
}
|
|
1170
|
-
const entryContent =
|
|
1525
|
+
const entryContent = readFileSync4(entryPath, "utf-8");
|
|
1171
1526
|
const loadStylesheet = async (id, base) => {
|
|
1172
1527
|
if (id === "tailwindcss") {
|
|
1173
1528
|
const nodeModules = resolve3(cwd, "node_modules");
|
|
1174
1529
|
const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
|
|
1175
|
-
if (!
|
|
1530
|
+
if (!existsSync4(tailwindCssPath)) {
|
|
1176
1531
|
throw new Error(
|
|
1177
1532
|
`Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
|
|
1178
1533
|
);
|
|
1179
1534
|
}
|
|
1180
|
-
const content =
|
|
1535
|
+
const content = readFileSync4(tailwindCssPath, "utf-8");
|
|
1181
1536
|
return { path: "virtual:tailwindcss/index.css", base, content };
|
|
1182
1537
|
}
|
|
1183
1538
|
const full = resolve3(base, id);
|
|
1184
|
-
if (
|
|
1185
|
-
const content =
|
|
1539
|
+
if (existsSync4(full)) {
|
|
1540
|
+
const content = readFileSync4(full, "utf-8");
|
|
1186
1541
|
return { path: full, base: resolve3(full, ".."), content };
|
|
1187
1542
|
}
|
|
1188
1543
|
throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
|
|
@@ -1383,7 +1738,7 @@ Available: ${available}`
|
|
|
1383
1738
|
const result = outcome.result;
|
|
1384
1739
|
if (opts.output !== void 0) {
|
|
1385
1740
|
const outPath = resolve4(process.cwd(), opts.output);
|
|
1386
|
-
|
|
1741
|
+
writeFileSync4(outPath, result.screenshot);
|
|
1387
1742
|
process.stdout.write(
|
|
1388
1743
|
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
1389
1744
|
`
|
|
@@ -1397,9 +1752,9 @@ Available: ${available}`
|
|
|
1397
1752
|
`);
|
|
1398
1753
|
} else if (fmt === "file") {
|
|
1399
1754
|
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1400
|
-
|
|
1755
|
+
mkdirSync3(dir, { recursive: true });
|
|
1401
1756
|
const outPath = resolve4(dir, `${componentName}.png`);
|
|
1402
|
-
|
|
1757
|
+
writeFileSync4(outPath, result.screenshot);
|
|
1403
1758
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1404
1759
|
process.stdout.write(
|
|
1405
1760
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1407,9 +1762,9 @@ Available: ${available}`
|
|
|
1407
1762
|
);
|
|
1408
1763
|
} else {
|
|
1409
1764
|
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1410
|
-
|
|
1765
|
+
mkdirSync3(dir, { recursive: true });
|
|
1411
1766
|
const outPath = resolve4(dir, `${componentName}.png`);
|
|
1412
|
-
|
|
1767
|
+
writeFileSync4(outPath, result.screenshot);
|
|
1413
1768
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
1414
1769
|
process.stdout.write(
|
|
1415
1770
|
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1512,7 +1867,7 @@ Available: ${available}`
|
|
|
1512
1867
|
const gen = new SpriteSheetGenerator();
|
|
1513
1868
|
const sheet = await gen.generate(result);
|
|
1514
1869
|
const spritePath = resolve4(process.cwd(), opts.sprite);
|
|
1515
|
-
|
|
1870
|
+
writeFileSync4(spritePath, sheet.png);
|
|
1516
1871
|
process.stderr.write(`Sprite sheet saved to ${spritePath}
|
|
1517
1872
|
`);
|
|
1518
1873
|
}
|
|
@@ -1522,9 +1877,9 @@ Available: ${available}`
|
|
|
1522
1877
|
const gen = new SpriteSheetGenerator();
|
|
1523
1878
|
const sheet = await gen.generate(result);
|
|
1524
1879
|
const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
1525
|
-
|
|
1880
|
+
mkdirSync3(dir, { recursive: true });
|
|
1526
1881
|
const outPath = resolve4(dir, `${componentName}-matrix.png`);
|
|
1527
|
-
|
|
1882
|
+
writeFileSync4(outPath, sheet.png);
|
|
1528
1883
|
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
|
|
1529
1884
|
process.stdout.write(
|
|
1530
1885
|
`\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
|
|
@@ -1568,7 +1923,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1568
1923
|
}
|
|
1569
1924
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
|
|
1570
1925
|
const outputDir = resolve4(process.cwd(), opts.outputDir);
|
|
1571
|
-
|
|
1926
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
1572
1927
|
const rootDir = process.cwd();
|
|
1573
1928
|
process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
|
|
1574
1929
|
`);
|
|
@@ -1601,7 +1956,7 @@ function registerRenderAll(renderCmd) {
|
|
|
1601
1956
|
errorMessage: outcome.error.message
|
|
1602
1957
|
});
|
|
1603
1958
|
const errPath = resolve4(outputDir, `${name}.error.json`);
|
|
1604
|
-
|
|
1959
|
+
writeFileSync4(
|
|
1605
1960
|
errPath,
|
|
1606
1961
|
JSON.stringify(
|
|
1607
1962
|
{
|
|
@@ -1619,9 +1974,9 @@ function registerRenderAll(renderCmd) {
|
|
|
1619
1974
|
const result = outcome.result;
|
|
1620
1975
|
results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
|
|
1621
1976
|
const pngPath = resolve4(outputDir, `${name}.png`);
|
|
1622
|
-
|
|
1977
|
+
writeFileSync4(pngPath, result.screenshot);
|
|
1623
1978
|
const jsonPath = resolve4(outputDir, `${name}.json`);
|
|
1624
|
-
|
|
1979
|
+
writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
|
|
1625
1980
|
if (isTTY()) {
|
|
1626
1981
|
process.stdout.write(
|
|
1627
1982
|
`\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
@@ -1683,7 +2038,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
|
|
|
1683
2038
|
return "json";
|
|
1684
2039
|
}
|
|
1685
2040
|
function createRenderCommand() {
|
|
1686
|
-
const renderCmd = new
|
|
2041
|
+
const renderCmd = new Command4("render").description(
|
|
1687
2042
|
"Render components to PNG or JSON via esbuild + BrowserPool"
|
|
1688
2043
|
);
|
|
1689
2044
|
registerRenderSingle(renderCmd);
|
|
@@ -1692,6 +2047,332 @@ function createRenderCommand() {
|
|
|
1692
2047
|
return renderCmd;
|
|
1693
2048
|
}
|
|
1694
2049
|
|
|
2050
|
+
// src/report/baseline.ts
|
|
2051
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2052
|
+
import { resolve as resolve5 } from "path";
|
|
2053
|
+
import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
|
|
2054
|
+
import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
|
|
2055
|
+
import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
|
|
2056
|
+
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2057
|
+
var _pool3 = null;
|
|
2058
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2059
|
+
if (_pool3 === null) {
|
|
2060
|
+
_pool3 = new BrowserPool3({
|
|
2061
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2062
|
+
viewportWidth,
|
|
2063
|
+
viewportHeight
|
|
2064
|
+
});
|
|
2065
|
+
await _pool3.init();
|
|
2066
|
+
}
|
|
2067
|
+
return _pool3;
|
|
2068
|
+
}
|
|
2069
|
+
async function shutdownPool3() {
|
|
2070
|
+
if (_pool3 !== null) {
|
|
2071
|
+
await _pool3.close();
|
|
2072
|
+
_pool3 = null;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2076
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2077
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2078
|
+
const slot = await pool.acquire();
|
|
2079
|
+
const { page } = slot;
|
|
2080
|
+
try {
|
|
2081
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
2082
|
+
await page.waitForFunction(
|
|
2083
|
+
() => {
|
|
2084
|
+
const w = window;
|
|
2085
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
2086
|
+
},
|
|
2087
|
+
{ timeout: 15e3 }
|
|
2088
|
+
);
|
|
2089
|
+
const renderError = await page.evaluate(() => {
|
|
2090
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
2091
|
+
});
|
|
2092
|
+
if (renderError !== null) {
|
|
2093
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
2094
|
+
}
|
|
2095
|
+
const rootDir = process.cwd();
|
|
2096
|
+
const classes = await page.evaluate(() => {
|
|
2097
|
+
const set = /* @__PURE__ */ new Set();
|
|
2098
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
2099
|
+
for (const c of el.className.split(/\s+/)) {
|
|
2100
|
+
if (c) set.add(c);
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
return [...set];
|
|
2104
|
+
});
|
|
2105
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
2106
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
2107
|
+
await page.addStyleTag({ content: projectCss });
|
|
2108
|
+
}
|
|
2109
|
+
const startMs = performance.now();
|
|
2110
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
2111
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
2112
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
2113
|
+
throw new Error(
|
|
2114
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
const PAD = 24;
|
|
2118
|
+
const MIN_W = 320;
|
|
2119
|
+
const MIN_H = 200;
|
|
2120
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
2121
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
2122
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
2123
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
2124
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
2125
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
2126
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
2127
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
2128
|
+
const screenshot = await page.screenshot({
|
|
2129
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
2130
|
+
type: "png"
|
|
2131
|
+
});
|
|
2132
|
+
const computedStylesRaw = {};
|
|
2133
|
+
const styles = await page.evaluate((sel) => {
|
|
2134
|
+
const el = document.querySelector(sel);
|
|
2135
|
+
if (el === null) return {};
|
|
2136
|
+
const computed = window.getComputedStyle(el);
|
|
2137
|
+
const out = {};
|
|
2138
|
+
for (const prop of [
|
|
2139
|
+
"display",
|
|
2140
|
+
"width",
|
|
2141
|
+
"height",
|
|
2142
|
+
"color",
|
|
2143
|
+
"backgroundColor",
|
|
2144
|
+
"fontSize",
|
|
2145
|
+
"fontFamily",
|
|
2146
|
+
"padding",
|
|
2147
|
+
"margin"
|
|
2148
|
+
]) {
|
|
2149
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
2150
|
+
}
|
|
2151
|
+
return out;
|
|
2152
|
+
}, "[data-reactscope-root] > *");
|
|
2153
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
2154
|
+
const renderTimeMs = performance.now() - startMs;
|
|
2155
|
+
return {
|
|
2156
|
+
screenshot,
|
|
2157
|
+
width: Math.round(safeW),
|
|
2158
|
+
height: Math.round(safeH),
|
|
2159
|
+
renderTimeMs,
|
|
2160
|
+
computedStyles: computedStylesRaw
|
|
2161
|
+
};
|
|
2162
|
+
} finally {
|
|
2163
|
+
pool.release(slot);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
2167
|
+
const flat = {};
|
|
2168
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
2169
|
+
Object.assign(flat, styles);
|
|
2170
|
+
}
|
|
2171
|
+
const colors = {};
|
|
2172
|
+
const spacing = {};
|
|
2173
|
+
const typography = {};
|
|
2174
|
+
const borders = {};
|
|
2175
|
+
const shadows = {};
|
|
2176
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
2177
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
2178
|
+
colors[prop] = value;
|
|
2179
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
2180
|
+
spacing[prop] = value;
|
|
2181
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
2182
|
+
typography[prop] = value;
|
|
2183
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
2184
|
+
borders[prop] = value;
|
|
2185
|
+
} else if (prop === "boxShadow") {
|
|
2186
|
+
shadows[prop] = value;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return { colors, spacing, typography, borders, shadows };
|
|
2190
|
+
}
|
|
2191
|
+
async function runBaseline(options = {}) {
|
|
2192
|
+
const {
|
|
2193
|
+
outputDir = DEFAULT_BASELINE_DIR,
|
|
2194
|
+
componentsGlob,
|
|
2195
|
+
manifestPath,
|
|
2196
|
+
viewportWidth = 375,
|
|
2197
|
+
viewportHeight = 812
|
|
2198
|
+
} = options;
|
|
2199
|
+
const startTime = performance.now();
|
|
2200
|
+
const rootDir = process.cwd();
|
|
2201
|
+
const baselineDir = resolve5(rootDir, outputDir);
|
|
2202
|
+
const rendersDir = resolve5(baselineDir, "renders");
|
|
2203
|
+
if (existsSync5(baselineDir)) {
|
|
2204
|
+
rmSync(baselineDir, { recursive: true, force: true });
|
|
2205
|
+
}
|
|
2206
|
+
mkdirSync4(rendersDir, { recursive: true });
|
|
2207
|
+
let manifest;
|
|
2208
|
+
if (manifestPath !== void 0) {
|
|
2209
|
+
const { readFileSync: readFileSync7 } = await import("fs");
|
|
2210
|
+
const absPath = resolve5(rootDir, manifestPath);
|
|
2211
|
+
if (!existsSync5(absPath)) {
|
|
2212
|
+
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2213
|
+
}
|
|
2214
|
+
manifest = JSON.parse(readFileSync7(absPath, "utf-8"));
|
|
2215
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2216
|
+
`);
|
|
2217
|
+
} else {
|
|
2218
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
2219
|
+
manifest = await generateManifest2({ rootDir });
|
|
2220
|
+
const count = Object.keys(manifest.components).length;
|
|
2221
|
+
process.stderr.write(`Found ${count} components.
|
|
2222
|
+
`);
|
|
2223
|
+
}
|
|
2224
|
+
writeFileSync5(resolve5(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2225
|
+
let componentNames = Object.keys(manifest.components);
|
|
2226
|
+
if (componentsGlob !== void 0) {
|
|
2227
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
2228
|
+
process.stderr.write(
|
|
2229
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
2230
|
+
`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
const total = componentNames.length;
|
|
2234
|
+
if (total === 0) {
|
|
2235
|
+
process.stderr.write("No components to baseline.\n");
|
|
2236
|
+
const emptyReport = {
|
|
2237
|
+
components: {},
|
|
2238
|
+
totalProperties: 0,
|
|
2239
|
+
totalOnSystem: 0,
|
|
2240
|
+
totalOffSystem: 0,
|
|
2241
|
+
aggregateCompliance: 1,
|
|
2242
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2243
|
+
};
|
|
2244
|
+
writeFileSync5(
|
|
2245
|
+
resolve5(baselineDir, "compliance.json"),
|
|
2246
|
+
JSON.stringify(emptyReport, null, 2),
|
|
2247
|
+
"utf-8"
|
|
2248
|
+
);
|
|
2249
|
+
return {
|
|
2250
|
+
baselineDir,
|
|
2251
|
+
componentCount: 0,
|
|
2252
|
+
failureCount: 0,
|
|
2253
|
+
wallClockMs: performance.now() - startTime
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
2257
|
+
`);
|
|
2258
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
2259
|
+
let completed = 0;
|
|
2260
|
+
let failureCount = 0;
|
|
2261
|
+
const CONCURRENCY = 4;
|
|
2262
|
+
let nextIdx = 0;
|
|
2263
|
+
const renderOne = async (name) => {
|
|
2264
|
+
const descriptor = manifest.components[name];
|
|
2265
|
+
if (descriptor === void 0) return;
|
|
2266
|
+
const filePath = resolve5(rootDir, descriptor.filePath);
|
|
2267
|
+
const outcome = await safeRender2(
|
|
2268
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2269
|
+
{
|
|
2270
|
+
props: {},
|
|
2271
|
+
sourceLocation: {
|
|
2272
|
+
file: descriptor.filePath,
|
|
2273
|
+
line: descriptor.loc.start,
|
|
2274
|
+
column: 0
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
);
|
|
2278
|
+
completed++;
|
|
2279
|
+
const pct = Math.round(completed / total * 100);
|
|
2280
|
+
if (isTTY()) {
|
|
2281
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
2282
|
+
}
|
|
2283
|
+
if (outcome.crashed) {
|
|
2284
|
+
failureCount++;
|
|
2285
|
+
const errPath = resolve5(rendersDir, `${name}.error.json`);
|
|
2286
|
+
writeFileSync5(
|
|
2287
|
+
errPath,
|
|
2288
|
+
JSON.stringify(
|
|
2289
|
+
{
|
|
2290
|
+
component: name,
|
|
2291
|
+
errorMessage: outcome.error.message,
|
|
2292
|
+
heuristicFlags: outcome.error.heuristicFlags,
|
|
2293
|
+
propsAtCrash: outcome.error.propsAtCrash
|
|
2294
|
+
},
|
|
2295
|
+
null,
|
|
2296
|
+
2
|
|
2297
|
+
),
|
|
2298
|
+
"utf-8"
|
|
2299
|
+
);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const result = outcome.result;
|
|
2303
|
+
writeFileSync5(resolve5(rendersDir, `${name}.png`), result.screenshot);
|
|
2304
|
+
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2305
|
+
writeFileSync5(
|
|
2306
|
+
resolve5(rendersDir, `${name}.json`),
|
|
2307
|
+
JSON.stringify(jsonOutput, null, 2),
|
|
2308
|
+
"utf-8"
|
|
2309
|
+
);
|
|
2310
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
2311
|
+
};
|
|
2312
|
+
const worker = async () => {
|
|
2313
|
+
while (nextIdx < componentNames.length) {
|
|
2314
|
+
const i = nextIdx++;
|
|
2315
|
+
const name = componentNames[i];
|
|
2316
|
+
if (name !== void 0) {
|
|
2317
|
+
await renderOne(name);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
const workers = [];
|
|
2322
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
2323
|
+
workers.push(worker());
|
|
2324
|
+
}
|
|
2325
|
+
await Promise.all(workers);
|
|
2326
|
+
await shutdownPool3();
|
|
2327
|
+
if (isTTY()) {
|
|
2328
|
+
process.stderr.write("\n");
|
|
2329
|
+
}
|
|
2330
|
+
const resolver = new TokenResolver([]);
|
|
2331
|
+
const engine = new ComplianceEngine(resolver);
|
|
2332
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2333
|
+
writeFileSync5(
|
|
2334
|
+
resolve5(baselineDir, "compliance.json"),
|
|
2335
|
+
JSON.stringify(batchReport, null, 2),
|
|
2336
|
+
"utf-8"
|
|
2337
|
+
);
|
|
2338
|
+
const wallClockMs = performance.now() - startTime;
|
|
2339
|
+
const successCount = total - failureCount;
|
|
2340
|
+
process.stderr.write(
|
|
2341
|
+
`
|
|
2342
|
+
Baseline complete: ${successCount}/${total} components rendered` + (failureCount > 0 ? ` (${failureCount} failed)` : "") + ` in ${(wallClockMs / 1e3).toFixed(1)}s
|
|
2343
|
+
`
|
|
2344
|
+
);
|
|
2345
|
+
process.stderr.write(`Snapshot saved to ${baselineDir}
|
|
2346
|
+
`);
|
|
2347
|
+
return { baselineDir, componentCount: total, failureCount, wallClockMs };
|
|
2348
|
+
}
|
|
2349
|
+
function registerBaselineSubCommand(reportCmd) {
|
|
2350
|
+
reportCmd.command("baseline").description("Capture a baseline snapshot (manifest + renders + compliance) for later diffing").option(
|
|
2351
|
+
"-o, --output <dir>",
|
|
2352
|
+
"Output directory for the baseline snapshot",
|
|
2353
|
+
DEFAULT_BASELINE_DIR
|
|
2354
|
+
).option("--components <glob>", "Glob pattern to baseline a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").action(
|
|
2355
|
+
async (opts) => {
|
|
2356
|
+
try {
|
|
2357
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
2358
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
2359
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
2360
|
+
await runBaseline({
|
|
2361
|
+
outputDir: opts.output,
|
|
2362
|
+
componentsGlob: opts.components,
|
|
2363
|
+
manifestPath: opts.manifest,
|
|
2364
|
+
viewportWidth,
|
|
2365
|
+
viewportHeight
|
|
2366
|
+
});
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2369
|
+
`);
|
|
2370
|
+
process.exit(1);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
1695
2376
|
// src/tree-formatter.ts
|
|
1696
2377
|
var BRANCH = "\u251C\u2500\u2500 ";
|
|
1697
2378
|
var LAST_BRANCH = "\u2514\u2500\u2500 ";
|
|
@@ -1972,16 +2653,16 @@ function buildStructuredReport(report) {
|
|
|
1972
2653
|
}
|
|
1973
2654
|
|
|
1974
2655
|
// src/tokens/commands.ts
|
|
1975
|
-
import { existsSync as
|
|
1976
|
-
import { resolve as
|
|
2656
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
2657
|
+
import { resolve as resolve6 } from "path";
|
|
1977
2658
|
import {
|
|
1978
2659
|
parseTokenFileSync,
|
|
1979
2660
|
TokenParseError,
|
|
1980
|
-
TokenResolver,
|
|
2661
|
+
TokenResolver as TokenResolver2,
|
|
1981
2662
|
TokenValidationError,
|
|
1982
2663
|
validateTokenFile
|
|
1983
2664
|
} from "@agent-scope/tokens";
|
|
1984
|
-
import { Command as
|
|
2665
|
+
import { Command as Command5 } from "commander";
|
|
1985
2666
|
var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
|
|
1986
2667
|
var CONFIG_FILE = "reactscope.config.json";
|
|
1987
2668
|
function isTTY2() {
|
|
@@ -2003,30 +2684,30 @@ function buildTable2(headers, rows) {
|
|
|
2003
2684
|
}
|
|
2004
2685
|
function resolveTokenFilePath(fileFlag) {
|
|
2005
2686
|
if (fileFlag !== void 0) {
|
|
2006
|
-
return
|
|
2687
|
+
return resolve6(process.cwd(), fileFlag);
|
|
2007
2688
|
}
|
|
2008
|
-
const configPath =
|
|
2009
|
-
if (
|
|
2689
|
+
const configPath = resolve6(process.cwd(), CONFIG_FILE);
|
|
2690
|
+
if (existsSync6(configPath)) {
|
|
2010
2691
|
try {
|
|
2011
|
-
const raw =
|
|
2692
|
+
const raw = readFileSync5(configPath, "utf-8");
|
|
2012
2693
|
const config = JSON.parse(raw);
|
|
2013
2694
|
if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
|
|
2014
2695
|
const file = config.tokens.file;
|
|
2015
|
-
return
|
|
2696
|
+
return resolve6(process.cwd(), file);
|
|
2016
2697
|
}
|
|
2017
2698
|
} catch {
|
|
2018
2699
|
}
|
|
2019
2700
|
}
|
|
2020
|
-
return
|
|
2701
|
+
return resolve6(process.cwd(), DEFAULT_TOKEN_FILE);
|
|
2021
2702
|
}
|
|
2022
2703
|
function loadTokens(absPath) {
|
|
2023
|
-
if (!
|
|
2704
|
+
if (!existsSync6(absPath)) {
|
|
2024
2705
|
throw new Error(
|
|
2025
2706
|
`Token file not found at ${absPath}.
|
|
2026
2707
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2027
2708
|
);
|
|
2028
2709
|
}
|
|
2029
|
-
const raw =
|
|
2710
|
+
const raw = readFileSync5(absPath, "utf-8");
|
|
2030
2711
|
return parseTokenFileSync(raw);
|
|
2031
2712
|
}
|
|
2032
2713
|
function getRawValue(node, segments) {
|
|
@@ -2066,7 +2747,7 @@ function registerGet2(tokensCmd) {
|
|
|
2066
2747
|
try {
|
|
2067
2748
|
const filePath = resolveTokenFilePath(opts.file);
|
|
2068
2749
|
const { tokens } = loadTokens(filePath);
|
|
2069
|
-
const resolver = new
|
|
2750
|
+
const resolver = new TokenResolver2(tokens);
|
|
2070
2751
|
const resolvedValue = resolver.resolve(tokenPath);
|
|
2071
2752
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2072
2753
|
if (useJson) {
|
|
@@ -2092,7 +2773,7 @@ function registerList2(tokensCmd) {
|
|
|
2092
2773
|
try {
|
|
2093
2774
|
const filePath = resolveTokenFilePath(opts.file);
|
|
2094
2775
|
const { tokens } = loadTokens(filePath);
|
|
2095
|
-
const resolver = new
|
|
2776
|
+
const resolver = new TokenResolver2(tokens);
|
|
2096
2777
|
const filtered = resolver.list(opts.type, category);
|
|
2097
2778
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2098
2779
|
if (useJson) {
|
|
@@ -2122,7 +2803,7 @@ function registerSearch(tokensCmd) {
|
|
|
2122
2803
|
try {
|
|
2123
2804
|
const filePath = resolveTokenFilePath(opts.file);
|
|
2124
2805
|
const { tokens } = loadTokens(filePath);
|
|
2125
|
-
const resolver = new
|
|
2806
|
+
const resolver = new TokenResolver2(tokens);
|
|
2126
2807
|
const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
|
|
2127
2808
|
const typesToSearch = opts.type ? [opts.type] : [
|
|
2128
2809
|
"color",
|
|
@@ -2205,7 +2886,7 @@ function registerResolve(tokensCmd) {
|
|
|
2205
2886
|
const filePath = resolveTokenFilePath(opts.file);
|
|
2206
2887
|
const absFilePath = filePath;
|
|
2207
2888
|
const { tokens, rawFile } = loadTokens(absFilePath);
|
|
2208
|
-
const resolver = new
|
|
2889
|
+
const resolver = new TokenResolver2(tokens);
|
|
2209
2890
|
resolver.resolve(tokenPath);
|
|
2210
2891
|
const chain = buildResolutionChain(tokenPath, rawFile.tokens);
|
|
2211
2892
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
@@ -2240,13 +2921,13 @@ function registerValidate(tokensCmd) {
|
|
|
2240
2921
|
).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
|
|
2241
2922
|
try {
|
|
2242
2923
|
const filePath = resolveTokenFilePath(opts.file);
|
|
2243
|
-
if (!
|
|
2924
|
+
if (!existsSync6(filePath)) {
|
|
2244
2925
|
throw new Error(
|
|
2245
2926
|
`Token file not found at ${filePath}.
|
|
2246
2927
|
Create a reactscope.tokens.json file or use --file to specify a path.`
|
|
2247
2928
|
);
|
|
2248
2929
|
}
|
|
2249
|
-
const raw =
|
|
2930
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
2250
2931
|
const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
|
|
2251
2932
|
const errors = [];
|
|
2252
2933
|
let parsed;
|
|
@@ -2314,7 +2995,7 @@ function outputValidationResult(filePath, errors, useJson) {
|
|
|
2314
2995
|
}
|
|
2315
2996
|
}
|
|
2316
2997
|
function createTokensCommand() {
|
|
2317
|
-
const tokensCmd = new
|
|
2998
|
+
const tokensCmd = new Command5("tokens").description(
|
|
2318
2999
|
"Query and validate design tokens from a reactscope.tokens.json file"
|
|
2319
3000
|
);
|
|
2320
3001
|
registerGet2(tokensCmd);
|
|
@@ -2327,7 +3008,7 @@ function createTokensCommand() {
|
|
|
2327
3008
|
|
|
2328
3009
|
// src/program.ts
|
|
2329
3010
|
function createProgram(options = {}) {
|
|
2330
|
-
const program2 = new
|
|
3011
|
+
const program2 = new Command6("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
|
|
2331
3012
|
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(
|
|
2332
3013
|
async (url, opts) => {
|
|
2333
3014
|
try {
|
|
@@ -2400,7 +3081,7 @@ function createProgram(options = {}) {
|
|
|
2400
3081
|
}
|
|
2401
3082
|
);
|
|
2402
3083
|
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) => {
|
|
2403
|
-
const raw =
|
|
3084
|
+
const raw = readFileSync6(tracePath, "utf-8");
|
|
2404
3085
|
const trace = loadTrace(raw);
|
|
2405
3086
|
const source = generateTest(trace, {
|
|
2406
3087
|
description: opts.description,
|
|
@@ -2413,6 +3094,11 @@ function createProgram(options = {}) {
|
|
|
2413
3094
|
program2.addCommand(createRenderCommand());
|
|
2414
3095
|
program2.addCommand(createTokensCommand());
|
|
2415
3096
|
program2.addCommand(createInstrumentCommand());
|
|
3097
|
+
program2.addCommand(createInitCommand());
|
|
3098
|
+
const existingReportCmd = program2.commands.find((c) => c.name() === "report");
|
|
3099
|
+
if (existingReportCmd !== void 0) {
|
|
3100
|
+
registerBaselineSubCommand(existingReportCmd);
|
|
3101
|
+
}
|
|
2416
3102
|
return program2;
|
|
2417
3103
|
}
|
|
2418
3104
|
|