@agent-scope/cli 1.17.3 → 1.18.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 +316 -165
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +226 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +224 -79
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -4,18 +4,19 @@ import { generateManifest } from '@agent-scope/manifest';
|
|
|
4
4
|
import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
|
|
5
5
|
import { TokenResolver, ComplianceEngine, parseTokenFileSync, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ImpactAnalyzer } from '@agent-scope/tokens';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import * as
|
|
7
|
+
import * as esbuild2 from 'esbuild';
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import * as readline from 'readline';
|
|
10
10
|
import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
|
|
11
11
|
import { chromium } from 'playwright';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
12
13
|
import { createServer } from 'http';
|
|
13
14
|
import { buildSite } from '@agent-scope/site';
|
|
14
15
|
|
|
15
16
|
// src/ci/commands.ts
|
|
16
|
-
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss,
|
|
17
|
+
async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
|
|
17
18
|
const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
|
|
18
|
-
return wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
19
|
+
return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
|
|
19
20
|
}
|
|
20
21
|
async function bundleComponentToIIFE(filePath, componentName, props) {
|
|
21
22
|
const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
|
|
@@ -51,7 +52,12 @@ import { createElement } from "react";
|
|
|
51
52
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
// If a scope file wrapper was injected, use it to wrap the component
|
|
56
|
+
var Wrapper = (window).__SCOPE_WRAPPER__;
|
|
57
|
+
var element = Wrapper
|
|
58
|
+
? createElement(Wrapper, null, createElement(Component, props))
|
|
59
|
+
: createElement(Component, props);
|
|
60
|
+
createRoot(rootEl).render(element);
|
|
55
61
|
// Use requestAnimationFrame to let React flush the render
|
|
56
62
|
requestAnimationFrame(function() {
|
|
57
63
|
window.__SCOPE_RENDER_COMPLETE__ = true;
|
|
@@ -63,7 +69,7 @@ import { createElement } from "react";
|
|
|
63
69
|
})();
|
|
64
70
|
`
|
|
65
71
|
);
|
|
66
|
-
const result = await
|
|
72
|
+
const result = await esbuild2.build({
|
|
67
73
|
stdin: {
|
|
68
74
|
contents: wrapperCode,
|
|
69
75
|
// Resolve relative imports (within the component's dir)
|
|
@@ -101,12 +107,11 @@ ${msg}`);
|
|
|
101
107
|
}
|
|
102
108
|
return outputFile.text;
|
|
103
109
|
}
|
|
104
|
-
function wrapInHtml(bundledScript, viewportWidth, projectCss,
|
|
110
|
+
function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
|
|
105
111
|
const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
|
|
106
112
|
${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
107
113
|
</style>` : "";
|
|
108
|
-
const
|
|
109
|
-
` : "";
|
|
114
|
+
const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
|
|
110
115
|
return `<!DOCTYPE html>
|
|
111
116
|
<html lang="en">
|
|
112
117
|
<head>
|
|
@@ -121,7 +126,8 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
|
|
|
121
126
|
</head>
|
|
122
127
|
<body>
|
|
123
128
|
<div id="scope-root" data-reactscope-root></div>
|
|
124
|
-
${
|
|
129
|
+
${wrapperScriptBlock}
|
|
130
|
+
<script>${bundledScript}</script>
|
|
125
131
|
</body>
|
|
126
132
|
</html>`;
|
|
127
133
|
}
|
|
@@ -484,16 +490,16 @@ async function getTailwindCompiler(cwd) {
|
|
|
484
490
|
from: entryPath,
|
|
485
491
|
loadStylesheet
|
|
486
492
|
});
|
|
487
|
-
const
|
|
488
|
-
compilerCache = { cwd, build:
|
|
489
|
-
return
|
|
493
|
+
const build3 = result.build.bind(result);
|
|
494
|
+
compilerCache = { cwd, build: build3 };
|
|
495
|
+
return build3;
|
|
490
496
|
}
|
|
491
497
|
async function getCompiledCssForClasses(cwd, classes) {
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
498
|
+
const build3 = await getTailwindCompiler(cwd);
|
|
499
|
+
if (build3 === null) return null;
|
|
494
500
|
const deduped = [...new Set(classes)].filter(Boolean);
|
|
495
501
|
if (deduped.length === 0) return null;
|
|
496
|
-
return
|
|
502
|
+
return build3(deduped);
|
|
497
503
|
}
|
|
498
504
|
|
|
499
505
|
// src/ci/commands.ts
|
|
@@ -1111,9 +1117,9 @@ function createRL() {
|
|
|
1111
1117
|
});
|
|
1112
1118
|
}
|
|
1113
1119
|
async function ask(rl, question) {
|
|
1114
|
-
return new Promise((
|
|
1120
|
+
return new Promise((resolve18) => {
|
|
1115
1121
|
rl.question(question, (answer) => {
|
|
1116
|
-
|
|
1122
|
+
resolve18(answer.trim());
|
|
1117
1123
|
});
|
|
1118
1124
|
});
|
|
1119
1125
|
}
|
|
@@ -2935,6 +2941,122 @@ function writeReportToFile(report, outputPath, pretty) {
|
|
|
2935
2941
|
const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
|
|
2936
2942
|
writeFileSync(outputPath, json, "utf-8");
|
|
2937
2943
|
}
|
|
2944
|
+
var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
|
|
2945
|
+
function findScopeFile(componentFilePath) {
|
|
2946
|
+
const dir = dirname(componentFilePath);
|
|
2947
|
+
const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
2948
|
+
const baseName = stem.slice(dir.length + 1);
|
|
2949
|
+
for (const ext of SCOPE_EXTENSIONS) {
|
|
2950
|
+
const candidate = join(dir, `${baseName}${ext}`);
|
|
2951
|
+
if (existsSync(candidate)) return candidate;
|
|
2952
|
+
}
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
async function loadScopeFile(scopeFilePath) {
|
|
2956
|
+
const tmpDir = join(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
2957
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
2958
|
+
const outFile = join(tmpDir, "scope-file.cjs");
|
|
2959
|
+
try {
|
|
2960
|
+
const result = await esbuild2.build({
|
|
2961
|
+
entryPoints: [scopeFilePath],
|
|
2962
|
+
bundle: true,
|
|
2963
|
+
format: "cjs",
|
|
2964
|
+
platform: "node",
|
|
2965
|
+
target: "node18",
|
|
2966
|
+
outfile: outFile,
|
|
2967
|
+
write: true,
|
|
2968
|
+
jsx: "automatic",
|
|
2969
|
+
jsxImportSource: "react",
|
|
2970
|
+
// Externalize React — we don't need to execute JSX, just extract plain data
|
|
2971
|
+
external: ["react", "react-dom", "react/jsx-runtime"],
|
|
2972
|
+
define: {
|
|
2973
|
+
"process.env.NODE_ENV": '"development"'
|
|
2974
|
+
},
|
|
2975
|
+
logLevel: "silent"
|
|
2976
|
+
});
|
|
2977
|
+
if (result.errors.length > 0) {
|
|
2978
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
2979
|
+
throw new Error(`Failed to bundle scope file ${scopeFilePath}:
|
|
2980
|
+
${msg}`);
|
|
2981
|
+
}
|
|
2982
|
+
const req = createRequire(import.meta.url);
|
|
2983
|
+
delete req.cache[resolve(outFile)];
|
|
2984
|
+
const mod = req(outFile);
|
|
2985
|
+
const scenarios = extractScenarios(mod, scopeFilePath);
|
|
2986
|
+
const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
|
|
2987
|
+
return { filePath: scopeFilePath, scenarios, hasWrapper };
|
|
2988
|
+
} finally {
|
|
2989
|
+
try {
|
|
2990
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
2991
|
+
} catch {
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
async function loadScopeFileForComponent(componentFilePath) {
|
|
2996
|
+
const scopeFilePath = findScopeFile(componentFilePath);
|
|
2997
|
+
if (scopeFilePath === null) return null;
|
|
2998
|
+
return loadScopeFile(scopeFilePath);
|
|
2999
|
+
}
|
|
3000
|
+
function extractScenarios(mod, filePath) {
|
|
3001
|
+
const raw = mod.scenarios ?? mod.default?.scenarios;
|
|
3002
|
+
if (raw === void 0) return {};
|
|
3003
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
3004
|
+
console.warn(`[scope] ${filePath}: "scenarios" export is not a plain object \u2014 ignoring.`);
|
|
3005
|
+
return {};
|
|
3006
|
+
}
|
|
3007
|
+
const result = {};
|
|
3008
|
+
for (const [name, props] of Object.entries(raw)) {
|
|
3009
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) {
|
|
3010
|
+
console.warn(`[scope] ${filePath}: scenario "${name}" is not a plain object \u2014 skipping.`);
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
result[name] = props;
|
|
3014
|
+
}
|
|
3015
|
+
return result;
|
|
3016
|
+
}
|
|
3017
|
+
async function buildWrapperScript(scopeFilePath) {
|
|
3018
|
+
const wrapperEntry = (
|
|
3019
|
+
/* ts */
|
|
3020
|
+
`
|
|
3021
|
+
import * as __scopeMod from ${JSON.stringify(scopeFilePath)};
|
|
3022
|
+
// Expose the wrapper on window so the harness can access it
|
|
3023
|
+
var wrapper =
|
|
3024
|
+
__scopeMod.wrapper ??
|
|
3025
|
+
(__scopeMod.default && __scopeMod.default.wrapper) ??
|
|
3026
|
+
null;
|
|
3027
|
+
window.__SCOPE_WRAPPER__ = wrapper;
|
|
3028
|
+
`
|
|
3029
|
+
);
|
|
3030
|
+
const result = await esbuild2.build({
|
|
3031
|
+
stdin: {
|
|
3032
|
+
contents: wrapperEntry,
|
|
3033
|
+
resolveDir: dirname(scopeFilePath),
|
|
3034
|
+
loader: "tsx",
|
|
3035
|
+
sourcefile: "__scope_wrapper_entry__.tsx"
|
|
3036
|
+
},
|
|
3037
|
+
bundle: true,
|
|
3038
|
+
format: "iife",
|
|
3039
|
+
platform: "browser",
|
|
3040
|
+
target: "es2020",
|
|
3041
|
+
write: false,
|
|
3042
|
+
jsx: "automatic",
|
|
3043
|
+
jsxImportSource: "react",
|
|
3044
|
+
external: [],
|
|
3045
|
+
define: {
|
|
3046
|
+
"process.env.NODE_ENV": '"development"',
|
|
3047
|
+
global: "globalThis"
|
|
3048
|
+
},
|
|
3049
|
+
logLevel: "silent"
|
|
3050
|
+
});
|
|
3051
|
+
if (result.errors.length > 0) {
|
|
3052
|
+
const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
|
|
3053
|
+
throw new Error(`Failed to build wrapper script from ${scopeFilePath}:
|
|
3054
|
+
${msg}`);
|
|
3055
|
+
}
|
|
3056
|
+
return result.outputFiles?.[0]?.text ?? "";
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/render-commands.ts
|
|
2938
3060
|
var MANIFEST_PATH6 = ".reactscope/manifest.json";
|
|
2939
3061
|
var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
|
|
2940
3062
|
var _pool3 = null;
|
|
@@ -2955,7 +3077,7 @@ async function shutdownPool3() {
|
|
|
2955
3077
|
_pool3 = null;
|
|
2956
3078
|
}
|
|
2957
3079
|
}
|
|
2958
|
-
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
3080
|
+
function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
|
|
2959
3081
|
const satori = new SatoriRenderer({
|
|
2960
3082
|
defaultViewport: { width: viewportWidth, height: viewportHeight }
|
|
2961
3083
|
});
|
|
@@ -2968,7 +3090,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
2968
3090
|
filePath,
|
|
2969
3091
|
componentName,
|
|
2970
3092
|
props,
|
|
2971
|
-
viewportWidth
|
|
3093
|
+
viewportWidth,
|
|
3094
|
+
void 0,
|
|
3095
|
+
// projectCss (handled separately)
|
|
3096
|
+
wrapperScript
|
|
2972
3097
|
);
|
|
2973
3098
|
const slot = await pool.acquire();
|
|
2974
3099
|
const { page } = slot;
|
|
@@ -3059,8 +3184,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
|
|
|
3059
3184
|
}
|
|
3060
3185
|
};
|
|
3061
3186
|
}
|
|
3187
|
+
function buildScenarioMap(opts, scopeData) {
|
|
3188
|
+
if (opts.scenario !== void 0) {
|
|
3189
|
+
if (scopeData === null) {
|
|
3190
|
+
throw new Error(`--scenario "${opts.scenario}" requires a .scope file next to the component`);
|
|
3191
|
+
}
|
|
3192
|
+
const props = scopeData.scenarios[opts.scenario];
|
|
3193
|
+
if (props === void 0) {
|
|
3194
|
+
const available = Object.keys(scopeData.scenarios).join(", ") || "(none)";
|
|
3195
|
+
throw new Error(
|
|
3196
|
+
`Scenario "${opts.scenario}" not found in scope file.
|
|
3197
|
+
Available: ${available}`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
return { [opts.scenario]: props };
|
|
3201
|
+
}
|
|
3202
|
+
if (opts.props !== void 0) {
|
|
3203
|
+
let parsed;
|
|
3204
|
+
try {
|
|
3205
|
+
parsed = JSON.parse(opts.props);
|
|
3206
|
+
} catch {
|
|
3207
|
+
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3208
|
+
}
|
|
3209
|
+
return { __default__: parsed };
|
|
3210
|
+
}
|
|
3211
|
+
if (scopeData !== null && Object.keys(scopeData.scenarios).length > 0) {
|
|
3212
|
+
return scopeData.scenarios;
|
|
3213
|
+
}
|
|
3214
|
+
return { __default__: {} };
|
|
3215
|
+
}
|
|
3062
3216
|
function registerRenderSingle(renderCmd) {
|
|
3063
|
-
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
3217
|
+
renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--scenario <name>", "Run a named scenario from the component's .scope file").option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
|
|
3064
3218
|
async (componentName, opts) => {
|
|
3065
3219
|
try {
|
|
3066
3220
|
const manifest = loadManifest(opts.manifest);
|
|
@@ -3072,80 +3226,71 @@ function registerRenderSingle(renderCmd) {
|
|
|
3072
3226
|
Available: ${available}`
|
|
3073
3227
|
);
|
|
3074
3228
|
}
|
|
3075
|
-
let props = {};
|
|
3076
|
-
if (opts.props !== void 0) {
|
|
3077
|
-
try {
|
|
3078
|
-
props = JSON.parse(opts.props);
|
|
3079
|
-
} catch {
|
|
3080
|
-
throw new Error(`Invalid props JSON: ${opts.props}`);
|
|
3081
|
-
}
|
|
3082
|
-
}
|
|
3083
3229
|
const { width, height } = parseViewport(opts.viewport);
|
|
3084
3230
|
const rootDir = process.cwd();
|
|
3085
3231
|
const filePath = resolve(rootDir, descriptor.filePath);
|
|
3086
|
-
const
|
|
3232
|
+
const scopeData = await loadScopeFileForComponent(filePath);
|
|
3233
|
+
const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
|
|
3234
|
+
const scenarios = buildScenarioMap(opts, scopeData);
|
|
3235
|
+
const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
|
|
3087
3236
|
process.stderr.write(
|
|
3088
3237
|
`Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
|
|
3089
3238
|
`
|
|
3090
3239
|
);
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3240
|
+
const fmt2 = resolveSingleFormat(opts.format);
|
|
3241
|
+
let anyFailed = false;
|
|
3242
|
+
for (const [scenarioName, props] of Object.entries(scenarios)) {
|
|
3243
|
+
const isNamed = scenarioName !== "__default__";
|
|
3244
|
+
const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
|
|
3245
|
+
const outcome = await safeRender(
|
|
3246
|
+
() => renderer.renderCell(props, descriptor.complexityClass),
|
|
3247
|
+
{
|
|
3248
|
+
props,
|
|
3249
|
+
sourceLocation: {
|
|
3250
|
+
file: descriptor.filePath,
|
|
3251
|
+
line: descriptor.loc.start,
|
|
3252
|
+
column: 0
|
|
3253
|
+
}
|
|
3099
3254
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
if (outcome.crashed) {
|
|
3104
|
-
process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
|
|
3255
|
+
);
|
|
3256
|
+
if (outcome.crashed) {
|
|
3257
|
+
process.stderr.write(`\u2717 ${label} render failed: ${outcome.error.message}
|
|
3105
3258
|
`);
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3259
|
+
const hintList = outcome.error.heuristicFlags.join(", ");
|
|
3260
|
+
if (hintList.length > 0) {
|
|
3261
|
+
process.stderr.write(` Hints: ${hintList}
|
|
3109
3262
|
`);
|
|
3263
|
+
}
|
|
3264
|
+
anyFailed = true;
|
|
3265
|
+
continue;
|
|
3110
3266
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
`\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3267
|
+
const result = outcome.result;
|
|
3268
|
+
const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
|
|
3269
|
+
if (opts.output !== void 0 && !isNamed) {
|
|
3270
|
+
const outPath = resolve(process.cwd(), opts.output);
|
|
3271
|
+
writeFileSync(outPath, result.screenshot);
|
|
3272
|
+
process.stdout.write(
|
|
3273
|
+
`\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3119
3274
|
`
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
if (fmt2 === "json") {
|
|
3125
|
-
const json = formatRenderJson(componentName, props, result);
|
|
3126
|
-
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3275
|
+
);
|
|
3276
|
+
} else if (fmt2 === "json") {
|
|
3277
|
+
const json = formatRenderJson(label, props, result);
|
|
3278
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}
|
|
3127
3279
|
`);
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
`
|
|
3137
|
-
);
|
|
3138
|
-
} else {
|
|
3139
|
-
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3140
|
-
mkdirSync(dir, { recursive: true });
|
|
3141
|
-
const outPath = resolve(dir, `${componentName}.png`);
|
|
3142
|
-
writeFileSync(outPath, result.screenshot);
|
|
3143
|
-
const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
|
|
3144
|
-
process.stdout.write(
|
|
3145
|
-
`\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3280
|
+
} else {
|
|
3281
|
+
const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3282
|
+
mkdirSync(dir, { recursive: true });
|
|
3283
|
+
const outPath = resolve(dir, outFileName);
|
|
3284
|
+
writeFileSync(outPath, result.screenshot);
|
|
3285
|
+
const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
|
|
3286
|
+
process.stdout.write(
|
|
3287
|
+
`\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
|
|
3146
3288
|
`
|
|
3147
|
-
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3148
3291
|
}
|
|
3292
|
+
await shutdownPool3();
|
|
3293
|
+
if (anyFailed) process.exit(1);
|
|
3149
3294
|
} catch (err) {
|
|
3150
3295
|
await shutdownPool3();
|
|
3151
3296
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|