@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/index.js
CHANGED
|
@@ -1,15 +1,359 @@
|
|
|
1
|
-
import { readFileSync,
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync, rmSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname } from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { generateManifest } from '@agent-scope/manifest';
|
|
5
6
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
6
7
|
import { chromium } from 'playwright';
|
|
7
8
|
import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer, BrowserPool } from '@agent-scope/render';
|
|
8
9
|
import * as esbuild from 'esbuild';
|
|
9
10
|
import { createRequire } from 'module';
|
|
10
|
-
import { TokenResolver, validateTokenFile, TokenValidationError, parseTokenFileSync, TokenParseError } from '@agent-scope/tokens';
|
|
11
|
+
import { TokenResolver, validateTokenFile, TokenValidationError, parseTokenFileSync, TokenParseError, ComplianceEngine } from '@agent-scope/tokens';
|
|
11
12
|
|
|
12
|
-
// src/
|
|
13
|
+
// src/init/index.ts
|
|
14
|
+
function hasConfigFile(dir, stem) {
|
|
15
|
+
if (!existsSync(dir)) return false;
|
|
16
|
+
try {
|
|
17
|
+
const entries = readdirSync(dir);
|
|
18
|
+
return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readSafe(path) {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(path, "utf-8");
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function detectFramework(rootDir, packageDeps) {
|
|
31
|
+
if (hasConfigFile(rootDir, "next.config")) return "next";
|
|
32
|
+
if (hasConfigFile(rootDir, "vite.config")) return "vite";
|
|
33
|
+
if (hasConfigFile(rootDir, "remix.config")) return "remix";
|
|
34
|
+
if ("react-scripts" in packageDeps) return "cra";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
function detectPackageManager(rootDir) {
|
|
38
|
+
if (existsSync(join(rootDir, "bun.lock"))) return "bun";
|
|
39
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
40
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
41
|
+
if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
|
|
42
|
+
return "npm";
|
|
43
|
+
}
|
|
44
|
+
function detectTypeScript(rootDir) {
|
|
45
|
+
const candidate = join(rootDir, "tsconfig.json");
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
return { typescript: true, tsconfigPath: candidate };
|
|
48
|
+
}
|
|
49
|
+
return { typescript: false, tsconfigPath: null };
|
|
50
|
+
}
|
|
51
|
+
var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
|
|
52
|
+
var COMPONENT_EXTS = [".tsx", ".jsx"];
|
|
53
|
+
function detectComponentPatterns(rootDir, typescript) {
|
|
54
|
+
const patterns = [];
|
|
55
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
56
|
+
const altExt = typescript ? "jsx" : "jsx";
|
|
57
|
+
for (const dir of COMPONENT_DIRS) {
|
|
58
|
+
const absDir = join(rootDir, dir);
|
|
59
|
+
if (!existsSync(absDir)) continue;
|
|
60
|
+
let hasComponents = false;
|
|
61
|
+
try {
|
|
62
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
63
|
+
hasComponents = entries.some(
|
|
64
|
+
(e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
|
|
65
|
+
);
|
|
66
|
+
if (!hasComponents) {
|
|
67
|
+
hasComponents = entries.some(
|
|
68
|
+
(e) => e.isDirectory() && (() => {
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(join(absDir, e.name)).some(
|
|
71
|
+
(f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (hasComponents) {
|
|
83
|
+
patterns.push(`${dir}/**/*.${ext}`);
|
|
84
|
+
if (altExt !== ext) {
|
|
85
|
+
patterns.push(`${dir}/**/*.${altExt}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const unique = [...new Set(patterns)];
|
|
90
|
+
if (unique.length === 0) {
|
|
91
|
+
return [`**/*.${ext}`];
|
|
92
|
+
}
|
|
93
|
+
return unique;
|
|
94
|
+
}
|
|
95
|
+
var TAILWIND_STEMS = ["tailwind.config"];
|
|
96
|
+
var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
|
|
97
|
+
var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
|
|
98
|
+
var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
|
|
99
|
+
function detectTokenSources(rootDir) {
|
|
100
|
+
const sources = [];
|
|
101
|
+
for (const stem of TAILWIND_STEMS) {
|
|
102
|
+
if (hasConfigFile(rootDir, stem)) {
|
|
103
|
+
try {
|
|
104
|
+
const entries = readdirSync(rootDir);
|
|
105
|
+
const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
|
|
106
|
+
if (match) {
|
|
107
|
+
sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const srcDir = join(rootDir, "src");
|
|
114
|
+
const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
|
|
115
|
+
for (const scanDir of dirsToScan) {
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(scanDir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
|
|
120
|
+
const filePath = join(scanDir, entry.name);
|
|
121
|
+
const content = readSafe(filePath);
|
|
122
|
+
if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
|
|
123
|
+
sources.push({ kind: "css-custom-properties", path: filePath });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (existsSync(srcDir)) {
|
|
131
|
+
try {
|
|
132
|
+
const entries = readdirSync(srcDir);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
|
|
135
|
+
sources.push({ kind: "theme-file", path: join(srcDir, entry) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return sources;
|
|
142
|
+
}
|
|
143
|
+
function detectProject(rootDir) {
|
|
144
|
+
const pkgPath = join(rootDir, "package.json");
|
|
145
|
+
let packageDeps = {};
|
|
146
|
+
const pkgContent = readSafe(pkgPath);
|
|
147
|
+
if (pkgContent !== null) {
|
|
148
|
+
try {
|
|
149
|
+
const pkg = JSON.parse(pkgContent);
|
|
150
|
+
packageDeps = {
|
|
151
|
+
...pkg.dependencies,
|
|
152
|
+
...pkg.devDependencies
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const framework = detectFramework(rootDir, packageDeps);
|
|
158
|
+
const { typescript, tsconfigPath } = detectTypeScript(rootDir);
|
|
159
|
+
const packageManager = detectPackageManager(rootDir);
|
|
160
|
+
const componentPatterns = detectComponentPatterns(rootDir, typescript);
|
|
161
|
+
const tokenSources = detectTokenSources(rootDir);
|
|
162
|
+
return {
|
|
163
|
+
framework,
|
|
164
|
+
typescript,
|
|
165
|
+
tsconfigPath,
|
|
166
|
+
componentPatterns,
|
|
167
|
+
tokenSources,
|
|
168
|
+
packageManager
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function buildDefaultConfig(detected, tokenFile, outputDir) {
|
|
172
|
+
const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
|
|
173
|
+
return {
|
|
174
|
+
components: {
|
|
175
|
+
include,
|
|
176
|
+
exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
|
|
177
|
+
wrappers: { providers: [], globalCSS: [] }
|
|
178
|
+
},
|
|
179
|
+
render: {
|
|
180
|
+
viewport: { default: { width: 1280, height: 800 } },
|
|
181
|
+
theme: "light",
|
|
182
|
+
warmBrowser: true
|
|
183
|
+
},
|
|
184
|
+
tokens: {
|
|
185
|
+
file: tokenFile,
|
|
186
|
+
compliance: { threshold: 90 }
|
|
187
|
+
},
|
|
188
|
+
output: {
|
|
189
|
+
dir: outputDir,
|
|
190
|
+
sprites: { format: "png", cellPadding: 8, labelAxes: true },
|
|
191
|
+
json: { pretty: true }
|
|
192
|
+
},
|
|
193
|
+
ci: {
|
|
194
|
+
complianceThreshold: 90,
|
|
195
|
+
failOnA11yViolations: true,
|
|
196
|
+
failOnConsoleErrors: false,
|
|
197
|
+
baselinePath: `${outputDir}baseline/`
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function createRL() {
|
|
202
|
+
return readline.createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function ask(rl, question) {
|
|
208
|
+
return new Promise((resolve7) => {
|
|
209
|
+
rl.question(question, (answer) => {
|
|
210
|
+
resolve7(answer.trim());
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function askWithDefault(rl, label, defaultValue) {
|
|
215
|
+
const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
|
|
216
|
+
return answer.length > 0 ? answer : defaultValue;
|
|
217
|
+
}
|
|
218
|
+
function ensureGitignoreEntry(rootDir, entry) {
|
|
219
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
220
|
+
if (existsSync(gitignorePath)) {
|
|
221
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
222
|
+
const normalised = entry.replace(/\/$/, "");
|
|
223
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
224
|
+
if (lines.includes(entry) || lines.includes(normalised)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
228
|
+
appendFileSync(gitignorePath, `${suffix}${entry}
|
|
229
|
+
`);
|
|
230
|
+
} else {
|
|
231
|
+
writeFileSync(gitignorePath, `${entry}
|
|
232
|
+
`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function scaffoldConfig(rootDir, config) {
|
|
236
|
+
const path = join(rootDir, "reactscope.config.json");
|
|
237
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
238
|
+
`);
|
|
239
|
+
return path;
|
|
240
|
+
}
|
|
241
|
+
function scaffoldTokenFile(rootDir, tokenFile) {
|
|
242
|
+
const path = join(rootDir, tokenFile);
|
|
243
|
+
if (!existsSync(path)) {
|
|
244
|
+
const stub = {
|
|
245
|
+
$schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
|
|
246
|
+
tokens: {}
|
|
247
|
+
};
|
|
248
|
+
writeFileSync(path, `${JSON.stringify(stub, null, 2)}
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
return path;
|
|
252
|
+
}
|
|
253
|
+
function scaffoldOutputDir(rootDir, outputDir) {
|
|
254
|
+
const dirPath = join(rootDir, outputDir);
|
|
255
|
+
mkdirSync(dirPath, { recursive: true });
|
|
256
|
+
const keepPath = join(dirPath, ".gitkeep");
|
|
257
|
+
if (!existsSync(keepPath)) {
|
|
258
|
+
writeFileSync(keepPath, "");
|
|
259
|
+
}
|
|
260
|
+
return dirPath;
|
|
261
|
+
}
|
|
262
|
+
async function runInit(options) {
|
|
263
|
+
const rootDir = options.cwd ?? process.cwd();
|
|
264
|
+
const configPath = join(rootDir, "reactscope.config.json");
|
|
265
|
+
const created = [];
|
|
266
|
+
if (existsSync(configPath) && !options.force) {
|
|
267
|
+
const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
|
|
268
|
+
process.stderr.write(`\u26A0\uFE0F ${msg}
|
|
269
|
+
`);
|
|
270
|
+
return { success: false, message: msg, created: [], skipped: true };
|
|
271
|
+
}
|
|
272
|
+
const detected = detectProject(rootDir);
|
|
273
|
+
const defaultTokenFile = "reactscope.tokens.json";
|
|
274
|
+
const defaultOutputDir = ".reactscope/";
|
|
275
|
+
let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
|
|
276
|
+
if (options.yes) {
|
|
277
|
+
process.stdout.write("\n\u{1F50D} Detected project settings:\n");
|
|
278
|
+
process.stdout.write(` Framework : ${detected.framework}
|
|
279
|
+
`);
|
|
280
|
+
process.stdout.write(` TypeScript : ${detected.typescript}
|
|
281
|
+
`);
|
|
282
|
+
process.stdout.write(` Include globs : ${config.components.include.join(", ")}
|
|
283
|
+
`);
|
|
284
|
+
process.stdout.write(` Token file : ${config.tokens.file}
|
|
285
|
+
`);
|
|
286
|
+
process.stdout.write(` Output dir : ${config.output.dir}
|
|
287
|
+
|
|
288
|
+
`);
|
|
289
|
+
} else {
|
|
290
|
+
const rl = createRL();
|
|
291
|
+
process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
|
|
292
|
+
process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
|
|
293
|
+
try {
|
|
294
|
+
process.stdout.write(` Detected framework: ${detected.framework}
|
|
295
|
+
`);
|
|
296
|
+
const includeRaw = await askWithDefault(
|
|
297
|
+
rl,
|
|
298
|
+
"Component include patterns (comma-separated)",
|
|
299
|
+
config.components.include.join(", ")
|
|
300
|
+
);
|
|
301
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
302
|
+
const excludeRaw = await askWithDefault(
|
|
303
|
+
rl,
|
|
304
|
+
"Component exclude patterns (comma-separated)",
|
|
305
|
+
config.components.exclude.join(", ")
|
|
306
|
+
);
|
|
307
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
308
|
+
const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
|
|
309
|
+
config.tokens.file = tokenFile;
|
|
310
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
311
|
+
const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
|
|
312
|
+
config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
|
|
313
|
+
config.ci.baselinePath = `${config.output.dir}baseline/`;
|
|
314
|
+
config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
|
|
315
|
+
config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
316
|
+
config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
317
|
+
} finally {
|
|
318
|
+
rl.close();
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write("\n");
|
|
321
|
+
}
|
|
322
|
+
const cfgPath = scaffoldConfig(rootDir, config);
|
|
323
|
+
created.push(cfgPath);
|
|
324
|
+
const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
|
|
325
|
+
created.push(tokPath);
|
|
326
|
+
const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
|
|
327
|
+
created.push(outDirPath);
|
|
328
|
+
ensureGitignoreEntry(rootDir, config.output.dir);
|
|
329
|
+
process.stdout.write("\u2705 Scope project initialised!\n\n");
|
|
330
|
+
process.stdout.write(" Created files:\n");
|
|
331
|
+
for (const p of created) {
|
|
332
|
+
process.stdout.write(` ${p}
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
message: "Project initialised successfully.",
|
|
339
|
+
created,
|
|
340
|
+
skipped: false
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function createInitCommand() {
|
|
344
|
+
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) => {
|
|
345
|
+
try {
|
|
346
|
+
const result = await runInit({ yes: opts.yes, force: opts.force });
|
|
347
|
+
if (!result.success && !result.skipped) {
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
352
|
+
`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
13
357
|
|
|
14
358
|
// src/manifest-formatter.ts
|
|
15
359
|
function isTTY() {
|
|
@@ -1653,6 +1997,325 @@ function createRenderCommand() {
|
|
|
1653
1997
|
registerRenderAll(renderCmd);
|
|
1654
1998
|
return renderCmd;
|
|
1655
1999
|
}
|
|
2000
|
+
var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
|
|
2001
|
+
var _pool3 = null;
|
|
2002
|
+
async function getPool3(viewportWidth, viewportHeight) {
|
|
2003
|
+
if (_pool3 === null) {
|
|
2004
|
+
_pool3 = new BrowserPool({
|
|
2005
|
+
size: { browsers: 1, pagesPerBrowser: 4 },
|
|
2006
|
+
viewportWidth,
|
|
2007
|
+
viewportHeight
|
|
2008
|
+
});
|
|
2009
|
+
await _pool3.init();
|
|
2010
|
+
}
|
|
2011
|
+
return _pool3;
|
|
2012
|
+
}
|
|
2013
|
+
async function shutdownPool3() {
|
|
2014
|
+
if (_pool3 !== null) {
|
|
2015
|
+
await _pool3.close();
|
|
2016
|
+
_pool3 = null;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
|
|
2020
|
+
const pool = await getPool3(viewportWidth, viewportHeight);
|
|
2021
|
+
const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
|
|
2022
|
+
const slot = await pool.acquire();
|
|
2023
|
+
const { page } = slot;
|
|
2024
|
+
try {
|
|
2025
|
+
await page.setContent(htmlHarness, { waitUntil: "load" });
|
|
2026
|
+
await page.waitForFunction(
|
|
2027
|
+
() => {
|
|
2028
|
+
const w = window;
|
|
2029
|
+
return w.__SCOPE_RENDER_COMPLETE__ === true;
|
|
2030
|
+
},
|
|
2031
|
+
{ timeout: 15e3 }
|
|
2032
|
+
);
|
|
2033
|
+
const renderError = await page.evaluate(() => {
|
|
2034
|
+
return window.__SCOPE_RENDER_ERROR__ ?? null;
|
|
2035
|
+
});
|
|
2036
|
+
if (renderError !== null) {
|
|
2037
|
+
throw new Error(`Component render error: ${renderError}`);
|
|
2038
|
+
}
|
|
2039
|
+
const rootDir = process.cwd();
|
|
2040
|
+
const classes = await page.evaluate(() => {
|
|
2041
|
+
const set = /* @__PURE__ */ new Set();
|
|
2042
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
2043
|
+
for (const c of el.className.split(/\s+/)) {
|
|
2044
|
+
if (c) set.add(c);
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
return [...set];
|
|
2048
|
+
});
|
|
2049
|
+
const projectCss = await getCompiledCssForClasses(rootDir, classes);
|
|
2050
|
+
if (projectCss != null && projectCss.length > 0) {
|
|
2051
|
+
await page.addStyleTag({ content: projectCss });
|
|
2052
|
+
}
|
|
2053
|
+
const startMs = performance.now();
|
|
2054
|
+
const rootLocator = page.locator("[data-reactscope-root]");
|
|
2055
|
+
const boundingBox = await rootLocator.boundingBox();
|
|
2056
|
+
if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
`Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const PAD = 24;
|
|
2062
|
+
const MIN_W = 320;
|
|
2063
|
+
const MIN_H = 200;
|
|
2064
|
+
const clipX = Math.max(0, boundingBox.x - PAD);
|
|
2065
|
+
const clipY = Math.max(0, boundingBox.y - PAD);
|
|
2066
|
+
const rawW = boundingBox.width + PAD * 2;
|
|
2067
|
+
const rawH = boundingBox.height + PAD * 2;
|
|
2068
|
+
const clipW = Math.max(rawW, MIN_W);
|
|
2069
|
+
const clipH = Math.max(rawH, MIN_H);
|
|
2070
|
+
const safeW = Math.min(clipW, viewportWidth - clipX);
|
|
2071
|
+
const safeH = Math.min(clipH, viewportHeight - clipY);
|
|
2072
|
+
const screenshot = await page.screenshot({
|
|
2073
|
+
clip: { x: clipX, y: clipY, width: safeW, height: safeH },
|
|
2074
|
+
type: "png"
|
|
2075
|
+
});
|
|
2076
|
+
const computedStylesRaw = {};
|
|
2077
|
+
const styles = await page.evaluate((sel) => {
|
|
2078
|
+
const el = document.querySelector(sel);
|
|
2079
|
+
if (el === null) return {};
|
|
2080
|
+
const computed = window.getComputedStyle(el);
|
|
2081
|
+
const out = {};
|
|
2082
|
+
for (const prop of [
|
|
2083
|
+
"display",
|
|
2084
|
+
"width",
|
|
2085
|
+
"height",
|
|
2086
|
+
"color",
|
|
2087
|
+
"backgroundColor",
|
|
2088
|
+
"fontSize",
|
|
2089
|
+
"fontFamily",
|
|
2090
|
+
"padding",
|
|
2091
|
+
"margin"
|
|
2092
|
+
]) {
|
|
2093
|
+
out[prop] = computed.getPropertyValue(prop);
|
|
2094
|
+
}
|
|
2095
|
+
return out;
|
|
2096
|
+
}, "[data-reactscope-root] > *");
|
|
2097
|
+
computedStylesRaw["[data-reactscope-root] > *"] = styles;
|
|
2098
|
+
const renderTimeMs = performance.now() - startMs;
|
|
2099
|
+
return {
|
|
2100
|
+
screenshot,
|
|
2101
|
+
width: Math.round(safeW),
|
|
2102
|
+
height: Math.round(safeH),
|
|
2103
|
+
renderTimeMs,
|
|
2104
|
+
computedStyles: computedStylesRaw
|
|
2105
|
+
};
|
|
2106
|
+
} finally {
|
|
2107
|
+
pool.release(slot);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function extractComputedStyles(computedStylesRaw) {
|
|
2111
|
+
const flat = {};
|
|
2112
|
+
for (const styles of Object.values(computedStylesRaw)) {
|
|
2113
|
+
Object.assign(flat, styles);
|
|
2114
|
+
}
|
|
2115
|
+
const colors = {};
|
|
2116
|
+
const spacing = {};
|
|
2117
|
+
const typography = {};
|
|
2118
|
+
const borders = {};
|
|
2119
|
+
const shadows = {};
|
|
2120
|
+
for (const [prop, value] of Object.entries(flat)) {
|
|
2121
|
+
if (prop === "color" || prop === "backgroundColor") {
|
|
2122
|
+
colors[prop] = value;
|
|
2123
|
+
} else if (prop === "padding" || prop === "margin") {
|
|
2124
|
+
spacing[prop] = value;
|
|
2125
|
+
} else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
|
|
2126
|
+
typography[prop] = value;
|
|
2127
|
+
} else if (prop === "borderRadius" || prop === "borderWidth") {
|
|
2128
|
+
borders[prop] = value;
|
|
2129
|
+
} else if (prop === "boxShadow") {
|
|
2130
|
+
shadows[prop] = value;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return { colors, spacing, typography, borders, shadows };
|
|
2134
|
+
}
|
|
2135
|
+
async function runBaseline(options = {}) {
|
|
2136
|
+
const {
|
|
2137
|
+
outputDir = DEFAULT_BASELINE_DIR,
|
|
2138
|
+
componentsGlob,
|
|
2139
|
+
manifestPath,
|
|
2140
|
+
viewportWidth = 375,
|
|
2141
|
+
viewportHeight = 812
|
|
2142
|
+
} = options;
|
|
2143
|
+
const startTime = performance.now();
|
|
2144
|
+
const rootDir = process.cwd();
|
|
2145
|
+
const baselineDir = resolve(rootDir, outputDir);
|
|
2146
|
+
const rendersDir = resolve(baselineDir, "renders");
|
|
2147
|
+
if (existsSync(baselineDir)) {
|
|
2148
|
+
rmSync(baselineDir, { recursive: true, force: true });
|
|
2149
|
+
}
|
|
2150
|
+
mkdirSync(rendersDir, { recursive: true });
|
|
2151
|
+
let manifest;
|
|
2152
|
+
if (manifestPath !== void 0) {
|
|
2153
|
+
const { readFileSync: readFileSync7 } = await import('fs');
|
|
2154
|
+
const absPath = resolve(rootDir, manifestPath);
|
|
2155
|
+
if (!existsSync(absPath)) {
|
|
2156
|
+
throw new Error(`Manifest not found at ${absPath}.`);
|
|
2157
|
+
}
|
|
2158
|
+
manifest = JSON.parse(readFileSync7(absPath, "utf-8"));
|
|
2159
|
+
process.stderr.write(`Loaded manifest from ${manifestPath}
|
|
2160
|
+
`);
|
|
2161
|
+
} else {
|
|
2162
|
+
process.stderr.write("Scanning for React components\u2026\n");
|
|
2163
|
+
manifest = await generateManifest({ rootDir });
|
|
2164
|
+
const count = Object.keys(manifest.components).length;
|
|
2165
|
+
process.stderr.write(`Found ${count} components.
|
|
2166
|
+
`);
|
|
2167
|
+
}
|
|
2168
|
+
writeFileSync(resolve(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
2169
|
+
let componentNames = Object.keys(manifest.components);
|
|
2170
|
+
if (componentsGlob !== void 0) {
|
|
2171
|
+
componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
|
|
2172
|
+
process.stderr.write(
|
|
2173
|
+
`Filtered to ${componentNames.length} components matching "${componentsGlob}".
|
|
2174
|
+
`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
const total = componentNames.length;
|
|
2178
|
+
if (total === 0) {
|
|
2179
|
+
process.stderr.write("No components to baseline.\n");
|
|
2180
|
+
const emptyReport = {
|
|
2181
|
+
components: {},
|
|
2182
|
+
totalProperties: 0,
|
|
2183
|
+
totalOnSystem: 0,
|
|
2184
|
+
totalOffSystem: 0,
|
|
2185
|
+
aggregateCompliance: 1,
|
|
2186
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2187
|
+
};
|
|
2188
|
+
writeFileSync(
|
|
2189
|
+
resolve(baselineDir, "compliance.json"),
|
|
2190
|
+
JSON.stringify(emptyReport, null, 2),
|
|
2191
|
+
"utf-8"
|
|
2192
|
+
);
|
|
2193
|
+
return {
|
|
2194
|
+
baselineDir,
|
|
2195
|
+
componentCount: 0,
|
|
2196
|
+
failureCount: 0,
|
|
2197
|
+
wallClockMs: performance.now() - startTime
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
process.stderr.write(`Rendering ${total} components\u2026
|
|
2201
|
+
`);
|
|
2202
|
+
const computedStylesMap = /* @__PURE__ */ new Map();
|
|
2203
|
+
let completed = 0;
|
|
2204
|
+
let failureCount = 0;
|
|
2205
|
+
const CONCURRENCY = 4;
|
|
2206
|
+
let nextIdx = 0;
|
|
2207
|
+
const renderOne = async (name) => {
|
|
2208
|
+
const descriptor = manifest.components[name];
|
|
2209
|
+
if (descriptor === void 0) return;
|
|
2210
|
+
const filePath = resolve(rootDir, descriptor.filePath);
|
|
2211
|
+
const outcome = await safeRender(
|
|
2212
|
+
() => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
|
|
2213
|
+
{
|
|
2214
|
+
props: {},
|
|
2215
|
+
sourceLocation: {
|
|
2216
|
+
file: descriptor.filePath,
|
|
2217
|
+
line: descriptor.loc.start,
|
|
2218
|
+
column: 0
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
);
|
|
2222
|
+
completed++;
|
|
2223
|
+
const pct = Math.round(completed / total * 100);
|
|
2224
|
+
if (isTTY()) {
|
|
2225
|
+
process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
|
|
2226
|
+
}
|
|
2227
|
+
if (outcome.crashed) {
|
|
2228
|
+
failureCount++;
|
|
2229
|
+
const errPath = resolve(rendersDir, `${name}.error.json`);
|
|
2230
|
+
writeFileSync(
|
|
2231
|
+
errPath,
|
|
2232
|
+
JSON.stringify(
|
|
2233
|
+
{
|
|
2234
|
+
component: name,
|
|
2235
|
+
errorMessage: outcome.error.message,
|
|
2236
|
+
heuristicFlags: outcome.error.heuristicFlags,
|
|
2237
|
+
propsAtCrash: outcome.error.propsAtCrash
|
|
2238
|
+
},
|
|
2239
|
+
null,
|
|
2240
|
+
2
|
|
2241
|
+
),
|
|
2242
|
+
"utf-8"
|
|
2243
|
+
);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const result = outcome.result;
|
|
2247
|
+
writeFileSync(resolve(rendersDir, `${name}.png`), result.screenshot);
|
|
2248
|
+
const jsonOutput = formatRenderJson(name, {}, result);
|
|
2249
|
+
writeFileSync(
|
|
2250
|
+
resolve(rendersDir, `${name}.json`),
|
|
2251
|
+
JSON.stringify(jsonOutput, null, 2),
|
|
2252
|
+
"utf-8"
|
|
2253
|
+
);
|
|
2254
|
+
computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
|
|
2255
|
+
};
|
|
2256
|
+
const worker = async () => {
|
|
2257
|
+
while (nextIdx < componentNames.length) {
|
|
2258
|
+
const i = nextIdx++;
|
|
2259
|
+
const name = componentNames[i];
|
|
2260
|
+
if (name !== void 0) {
|
|
2261
|
+
await renderOne(name);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
const workers = [];
|
|
2266
|
+
for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
|
|
2267
|
+
workers.push(worker());
|
|
2268
|
+
}
|
|
2269
|
+
await Promise.all(workers);
|
|
2270
|
+
await shutdownPool3();
|
|
2271
|
+
if (isTTY()) {
|
|
2272
|
+
process.stderr.write("\n");
|
|
2273
|
+
}
|
|
2274
|
+
const resolver = new TokenResolver([]);
|
|
2275
|
+
const engine = new ComplianceEngine(resolver);
|
|
2276
|
+
const batchReport = engine.auditBatch(computedStylesMap);
|
|
2277
|
+
writeFileSync(
|
|
2278
|
+
resolve(baselineDir, "compliance.json"),
|
|
2279
|
+
JSON.stringify(batchReport, null, 2),
|
|
2280
|
+
"utf-8"
|
|
2281
|
+
);
|
|
2282
|
+
const wallClockMs = performance.now() - startTime;
|
|
2283
|
+
const successCount = total - failureCount;
|
|
2284
|
+
process.stderr.write(
|
|
2285
|
+
`
|
|
2286
|
+
Baseline complete: ${successCount}/${total} components rendered` + (failureCount > 0 ? ` (${failureCount} failed)` : "") + ` in ${(wallClockMs / 1e3).toFixed(1)}s
|
|
2287
|
+
`
|
|
2288
|
+
);
|
|
2289
|
+
process.stderr.write(`Snapshot saved to ${baselineDir}
|
|
2290
|
+
`);
|
|
2291
|
+
return { baselineDir, componentCount: total, failureCount, wallClockMs };
|
|
2292
|
+
}
|
|
2293
|
+
function registerBaselineSubCommand(reportCmd) {
|
|
2294
|
+
reportCmd.command("baseline").description("Capture a baseline snapshot (manifest + renders + compliance) for later diffing").option(
|
|
2295
|
+
"-o, --output <dir>",
|
|
2296
|
+
"Output directory for the baseline snapshot",
|
|
2297
|
+
DEFAULT_BASELINE_DIR
|
|
2298
|
+
).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(
|
|
2299
|
+
async (opts) => {
|
|
2300
|
+
try {
|
|
2301
|
+
const [wStr, hStr] = opts.viewport.split("x");
|
|
2302
|
+
const viewportWidth = Number.parseInt(wStr ?? "375", 10);
|
|
2303
|
+
const viewportHeight = Number.parseInt(hStr ?? "812", 10);
|
|
2304
|
+
await runBaseline({
|
|
2305
|
+
outputDir: opts.output,
|
|
2306
|
+
componentsGlob: opts.components,
|
|
2307
|
+
manifestPath: opts.manifest,
|
|
2308
|
+
viewportWidth,
|
|
2309
|
+
viewportHeight
|
|
2310
|
+
});
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2313
|
+
`);
|
|
2314
|
+
process.exit(1);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
1656
2319
|
|
|
1657
2320
|
// src/tree-formatter.ts
|
|
1658
2321
|
var BRANCH = "\u251C\u2500\u2500 ";
|
|
@@ -2362,9 +3025,14 @@ function createProgram(options = {}) {
|
|
|
2362
3025
|
program.addCommand(createRenderCommand());
|
|
2363
3026
|
program.addCommand(createTokensCommand());
|
|
2364
3027
|
program.addCommand(createInstrumentCommand());
|
|
3028
|
+
program.addCommand(createInitCommand());
|
|
3029
|
+
const existingReportCmd = program.commands.find((c) => c.name() === "report");
|
|
3030
|
+
if (existingReportCmd !== void 0) {
|
|
3031
|
+
registerBaselineSubCommand(existingReportCmd);
|
|
3032
|
+
}
|
|
2365
3033
|
return program;
|
|
2366
3034
|
}
|
|
2367
3035
|
|
|
2368
|
-
export { createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
|
|
3036
|
+
export { createInitCommand, createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob, runInit };
|
|
2369
3037
|
//# sourceMappingURL=index.js.map
|
|
2370
3038
|
//# sourceMappingURL=index.js.map
|