@agent-scope/cli 1.17.2 → 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/index.cjs CHANGED
@@ -6,14 +6,16 @@ var manifest = require('@agent-scope/manifest');
6
6
  var render = require('@agent-scope/render');
7
7
  var tokens = require('@agent-scope/tokens');
8
8
  var commander = require('commander');
9
- var esbuild = require('esbuild');
9
+ var esbuild2 = require('esbuild');
10
10
  var module$1 = require('module');
11
11
  var readline = require('readline');
12
12
  var playwright = require('@agent-scope/playwright');
13
13
  var playwright$1 = require('playwright');
14
+ var os = require('os');
14
15
  var http = require('http');
15
16
  var site = require('@agent-scope/site');
16
17
 
18
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
17
19
  function _interopNamespace(e) {
18
20
  if (e && e.__esModule) return e;
19
21
  var n = Object.create(null);
@@ -32,13 +34,13 @@ function _interopNamespace(e) {
32
34
  return Object.freeze(n);
33
35
  }
34
36
 
35
- var esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild);
37
+ var esbuild2__namespace = /*#__PURE__*/_interopNamespace(esbuild2);
36
38
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
37
39
 
38
40
  // src/ci/commands.ts
39
- async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
41
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
40
42
  const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
41
- return wrapInHtml(bundledScript, viewportWidth, projectCss);
43
+ return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
42
44
  }
43
45
  async function bundleComponentToIIFE(filePath, componentName, props) {
44
46
  const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
@@ -74,7 +76,12 @@ import { createElement } from "react";
74
76
  window.__SCOPE_RENDER_COMPLETE__ = true;
75
77
  return;
76
78
  }
77
- createRoot(rootEl).render(createElement(Component, props));
79
+ // If a scope file wrapper was injected, use it to wrap the component
80
+ var Wrapper = (window).__SCOPE_WRAPPER__;
81
+ var element = Wrapper
82
+ ? createElement(Wrapper, null, createElement(Component, props))
83
+ : createElement(Component, props);
84
+ createRoot(rootEl).render(element);
78
85
  // Use requestAnimationFrame to let React flush the render
79
86
  requestAnimationFrame(function() {
80
87
  window.__SCOPE_RENDER_COMPLETE__ = true;
@@ -86,7 +93,7 @@ import { createElement } from "react";
86
93
  })();
87
94
  `
88
95
  );
89
- const result = await esbuild__namespace.build({
96
+ const result = await esbuild2__namespace.build({
90
97
  stdin: {
91
98
  contents: wrapperCode,
92
99
  // Resolve relative imports (within the component's dir)
@@ -124,10 +131,11 @@ ${msg}`);
124
131
  }
125
132
  return outputFile.text;
126
133
  }
127
- function wrapInHtml(bundledScript, viewportWidth, projectCss) {
134
+ function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
128
135
  const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
129
136
  ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
130
137
  </style>` : "";
138
+ const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
131
139
  return `<!DOCTYPE html>
132
140
  <html lang="en">
133
141
  <head>
@@ -142,6 +150,7 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
142
150
  </head>
143
151
  <body>
144
152
  <div id="scope-root" data-reactscope-root></div>
153
+ ${wrapperScriptBlock}
145
154
  <script>${bundledScript}</script>
146
155
  </body>
147
156
  </html>`;
@@ -505,16 +514,16 @@ async function getTailwindCompiler(cwd) {
505
514
  from: entryPath,
506
515
  loadStylesheet
507
516
  });
508
- const build2 = result.build.bind(result);
509
- compilerCache = { cwd, build: build2 };
510
- return build2;
517
+ const build3 = result.build.bind(result);
518
+ compilerCache = { cwd, build: build3 };
519
+ return build3;
511
520
  }
512
521
  async function getCompiledCssForClasses(cwd, classes) {
513
- const build2 = await getTailwindCompiler(cwd);
514
- if (build2 === null) return null;
522
+ const build3 = await getTailwindCompiler(cwd);
523
+ if (build3 === null) return null;
515
524
  const deduped = [...new Set(classes)].filter(Boolean);
516
525
  if (deduped.length === 0) return null;
517
- return build2(deduped);
526
+ return build3(deduped);
518
527
  }
519
528
 
520
529
  // src/ci/commands.ts
@@ -1132,9 +1141,9 @@ function createRL() {
1132
1141
  });
1133
1142
  }
1134
1143
  async function ask(rl, question) {
1135
- return new Promise((resolve17) => {
1144
+ return new Promise((resolve18) => {
1136
1145
  rl.question(question, (answer) => {
1137
- resolve17(answer.trim());
1146
+ resolve18(answer.trim());
1138
1147
  });
1139
1148
  });
1140
1149
  }
@@ -1706,8 +1715,15 @@ async function runHooksProfiling(componentName, filePath, props) {
1706
1715
  try {
1707
1716
  const context = await browser.newContext();
1708
1717
  const page = await context.newPage();
1709
- await page.addInitScript({ content: playwright.getBrowserEntryScript() });
1710
- const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1718
+ const scopeRuntime = playwright.getBrowserEntryScript();
1719
+ const htmlHarness = await buildComponentHarness(
1720
+ filePath,
1721
+ componentName,
1722
+ props,
1723
+ 1280,
1724
+ void 0,
1725
+ scopeRuntime
1726
+ );
1711
1727
  await page.setContent(htmlHarness, { waitUntil: "load" });
1712
1728
  await page.waitForFunction(
1713
1729
  () => {
@@ -1966,7 +1982,15 @@ async function runInteractionProfile(componentName, filePath, props, interaction
1966
1982
  try {
1967
1983
  const context = await browser.newContext();
1968
1984
  const page = await context.newPage();
1969
- const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1985
+ const scopeRuntime = playwright.getBrowserEntryScript();
1986
+ const htmlHarness = await buildComponentHarness(
1987
+ filePath,
1988
+ componentName,
1989
+ props,
1990
+ 1280,
1991
+ void 0,
1992
+ scopeRuntime
1993
+ );
1970
1994
  await page.setContent(htmlHarness, { waitUntil: "load" });
1971
1995
  await page.waitForFunction(
1972
1996
  () => {
@@ -2305,12 +2329,14 @@ async function runInstrumentTree(options) {
2305
2329
  viewport: { width: DEFAULT_VIEWPORT_WIDTH, height: DEFAULT_VIEWPORT_HEIGHT }
2306
2330
  });
2307
2331
  const page = await context.newPage();
2308
- await page.addInitScript({ content: playwright.getBrowserEntryScript() });
2332
+ const scopeRuntime = playwright.getBrowserEntryScript();
2309
2333
  const htmlHarness = await buildComponentHarness(
2310
2334
  filePath,
2311
2335
  componentName,
2312
2336
  {},
2313
- DEFAULT_VIEWPORT_WIDTH
2337
+ DEFAULT_VIEWPORT_WIDTH,
2338
+ void 0,
2339
+ scopeRuntime
2314
2340
  );
2315
2341
  await page.setContent(htmlHarness, { waitUntil: "load" });
2316
2342
  await page.waitForFunction(
@@ -2756,13 +2782,20 @@ Available: ${available}`
2756
2782
  }
2757
2783
  const rootDir = process.cwd();
2758
2784
  const filePath = path.resolve(rootDir, descriptor.filePath);
2759
- const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
2785
+ const preScript = playwright.getBrowserEntryScript() + "\n" + buildInstrumentationScript();
2786
+ const htmlHarness = await buildComponentHarness(
2787
+ filePath,
2788
+ options.componentName,
2789
+ {},
2790
+ 1280,
2791
+ void 0,
2792
+ preScript
2793
+ );
2760
2794
  const pool = await getPool2();
2761
2795
  const slot = await pool.acquire();
2762
2796
  const { page } = slot;
2763
2797
  const startMs = performance.now();
2764
2798
  try {
2765
- await page.addInitScript(buildInstrumentationScript());
2766
2799
  await page.setContent(htmlHarness, { waitUntil: "load" });
2767
2800
  await page.waitForFunction(
2768
2801
  () => window.__SCOPE_RENDER_COMPLETE__ === true,
@@ -2932,6 +2965,122 @@ function writeReportToFile(report, outputPath, pretty) {
2932
2965
  const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
2933
2966
  fs.writeFileSync(outputPath, json, "utf-8");
2934
2967
  }
2968
+ var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
2969
+ function findScopeFile(componentFilePath) {
2970
+ const dir = path.dirname(componentFilePath);
2971
+ const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
2972
+ const baseName = stem.slice(dir.length + 1);
2973
+ for (const ext of SCOPE_EXTENSIONS) {
2974
+ const candidate = path.join(dir, `${baseName}${ext}`);
2975
+ if (fs.existsSync(candidate)) return candidate;
2976
+ }
2977
+ return null;
2978
+ }
2979
+ async function loadScopeFile(scopeFilePath) {
2980
+ const tmpDir = path.join(os.tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
2981
+ fs.mkdirSync(tmpDir, { recursive: true });
2982
+ const outFile = path.join(tmpDir, "scope-file.cjs");
2983
+ try {
2984
+ const result = await esbuild2__namespace.build({
2985
+ entryPoints: [scopeFilePath],
2986
+ bundle: true,
2987
+ format: "cjs",
2988
+ platform: "node",
2989
+ target: "node18",
2990
+ outfile: outFile,
2991
+ write: true,
2992
+ jsx: "automatic",
2993
+ jsxImportSource: "react",
2994
+ // Externalize React — we don't need to execute JSX, just extract plain data
2995
+ external: ["react", "react-dom", "react/jsx-runtime"],
2996
+ define: {
2997
+ "process.env.NODE_ENV": '"development"'
2998
+ },
2999
+ logLevel: "silent"
3000
+ });
3001
+ if (result.errors.length > 0) {
3002
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
3003
+ throw new Error(`Failed to bundle scope file ${scopeFilePath}:
3004
+ ${msg}`);
3005
+ }
3006
+ const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
3007
+ delete req.cache[path.resolve(outFile)];
3008
+ const mod = req(outFile);
3009
+ const scenarios = extractScenarios(mod, scopeFilePath);
3010
+ const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
3011
+ return { filePath: scopeFilePath, scenarios, hasWrapper };
3012
+ } finally {
3013
+ try {
3014
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3015
+ } catch {
3016
+ }
3017
+ }
3018
+ }
3019
+ async function loadScopeFileForComponent(componentFilePath) {
3020
+ const scopeFilePath = findScopeFile(componentFilePath);
3021
+ if (scopeFilePath === null) return null;
3022
+ return loadScopeFile(scopeFilePath);
3023
+ }
3024
+ function extractScenarios(mod, filePath) {
3025
+ const raw = mod.scenarios ?? mod.default?.scenarios;
3026
+ if (raw === void 0) return {};
3027
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
3028
+ console.warn(`[scope] ${filePath}: "scenarios" export is not a plain object \u2014 ignoring.`);
3029
+ return {};
3030
+ }
3031
+ const result = {};
3032
+ for (const [name, props] of Object.entries(raw)) {
3033
+ if (typeof props !== "object" || props === null || Array.isArray(props)) {
3034
+ console.warn(`[scope] ${filePath}: scenario "${name}" is not a plain object \u2014 skipping.`);
3035
+ continue;
3036
+ }
3037
+ result[name] = props;
3038
+ }
3039
+ return result;
3040
+ }
3041
+ async function buildWrapperScript(scopeFilePath) {
3042
+ const wrapperEntry = (
3043
+ /* ts */
3044
+ `
3045
+ import * as __scopeMod from ${JSON.stringify(scopeFilePath)};
3046
+ // Expose the wrapper on window so the harness can access it
3047
+ var wrapper =
3048
+ __scopeMod.wrapper ??
3049
+ (__scopeMod.default && __scopeMod.default.wrapper) ??
3050
+ null;
3051
+ window.__SCOPE_WRAPPER__ = wrapper;
3052
+ `
3053
+ );
3054
+ const result = await esbuild2__namespace.build({
3055
+ stdin: {
3056
+ contents: wrapperEntry,
3057
+ resolveDir: path.dirname(scopeFilePath),
3058
+ loader: "tsx",
3059
+ sourcefile: "__scope_wrapper_entry__.tsx"
3060
+ },
3061
+ bundle: true,
3062
+ format: "iife",
3063
+ platform: "browser",
3064
+ target: "es2020",
3065
+ write: false,
3066
+ jsx: "automatic",
3067
+ jsxImportSource: "react",
3068
+ external: [],
3069
+ define: {
3070
+ "process.env.NODE_ENV": '"development"',
3071
+ global: "globalThis"
3072
+ },
3073
+ logLevel: "silent"
3074
+ });
3075
+ if (result.errors.length > 0) {
3076
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
3077
+ throw new Error(`Failed to build wrapper script from ${scopeFilePath}:
3078
+ ${msg}`);
3079
+ }
3080
+ return result.outputFiles?.[0]?.text ?? "";
3081
+ }
3082
+
3083
+ // src/render-commands.ts
2935
3084
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
2936
3085
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
2937
3086
  var _pool3 = null;
@@ -2952,7 +3101,7 @@ async function shutdownPool3() {
2952
3101
  _pool3 = null;
2953
3102
  }
2954
3103
  }
2955
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
3104
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
2956
3105
  const satori = new render.SatoriRenderer({
2957
3106
  defaultViewport: { width: viewportWidth, height: viewportHeight }
2958
3107
  });
@@ -2965,7 +3114,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
2965
3114
  filePath,
2966
3115
  componentName,
2967
3116
  props,
2968
- viewportWidth
3117
+ viewportWidth,
3118
+ void 0,
3119
+ // projectCss (handled separately)
3120
+ wrapperScript
2969
3121
  );
2970
3122
  const slot = await pool.acquire();
2971
3123
  const { page } = slot;
@@ -3056,8 +3208,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
3056
3208
  }
3057
3209
  };
3058
3210
  }
3211
+ function buildScenarioMap(opts, scopeData) {
3212
+ if (opts.scenario !== void 0) {
3213
+ if (scopeData === null) {
3214
+ throw new Error(`--scenario "${opts.scenario}" requires a .scope file next to the component`);
3215
+ }
3216
+ const props = scopeData.scenarios[opts.scenario];
3217
+ if (props === void 0) {
3218
+ const available = Object.keys(scopeData.scenarios).join(", ") || "(none)";
3219
+ throw new Error(
3220
+ `Scenario "${opts.scenario}" not found in scope file.
3221
+ Available: ${available}`
3222
+ );
3223
+ }
3224
+ return { [opts.scenario]: props };
3225
+ }
3226
+ if (opts.props !== void 0) {
3227
+ let parsed;
3228
+ try {
3229
+ parsed = JSON.parse(opts.props);
3230
+ } catch {
3231
+ throw new Error(`Invalid props JSON: ${opts.props}`);
3232
+ }
3233
+ return { __default__: parsed };
3234
+ }
3235
+ if (scopeData !== null && Object.keys(scopeData.scenarios).length > 0) {
3236
+ return scopeData.scenarios;
3237
+ }
3238
+ return { __default__: {} };
3239
+ }
3059
3240
  function registerRenderSingle(renderCmd) {
3060
- 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(
3241
+ 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(
3061
3242
  async (componentName, opts) => {
3062
3243
  try {
3063
3244
  const manifest = loadManifest(opts.manifest);
@@ -3069,80 +3250,71 @@ function registerRenderSingle(renderCmd) {
3069
3250
  Available: ${available}`
3070
3251
  );
3071
3252
  }
3072
- let props = {};
3073
- if (opts.props !== void 0) {
3074
- try {
3075
- props = JSON.parse(opts.props);
3076
- } catch {
3077
- throw new Error(`Invalid props JSON: ${opts.props}`);
3078
- }
3079
- }
3080
3253
  const { width, height } = parseViewport(opts.viewport);
3081
3254
  const rootDir = process.cwd();
3082
3255
  const filePath = path.resolve(rootDir, descriptor.filePath);
3083
- const renderer = buildRenderer(filePath, componentName, width, height);
3256
+ const scopeData = await loadScopeFileForComponent(filePath);
3257
+ const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3258
+ const scenarios = buildScenarioMap(opts, scopeData);
3259
+ const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
3084
3260
  process.stderr.write(
3085
3261
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3086
3262
  `
3087
3263
  );
3088
- const outcome = await render.safeRender(
3089
- () => renderer.renderCell(props, descriptor.complexityClass),
3090
- {
3091
- props,
3092
- sourceLocation: {
3093
- file: descriptor.filePath,
3094
- line: descriptor.loc.start,
3095
- column: 0
3264
+ const fmt2 = resolveSingleFormat(opts.format);
3265
+ let anyFailed = false;
3266
+ for (const [scenarioName, props] of Object.entries(scenarios)) {
3267
+ const isNamed = scenarioName !== "__default__";
3268
+ const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
3269
+ const outcome = await render.safeRender(
3270
+ () => renderer.renderCell(props, descriptor.complexityClass),
3271
+ {
3272
+ props,
3273
+ sourceLocation: {
3274
+ file: descriptor.filePath,
3275
+ line: descriptor.loc.start,
3276
+ column: 0
3277
+ }
3096
3278
  }
3097
- }
3098
- );
3099
- await shutdownPool3();
3100
- if (outcome.crashed) {
3101
- process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
3279
+ );
3280
+ if (outcome.crashed) {
3281
+ process.stderr.write(`\u2717 ${label} render failed: ${outcome.error.message}
3102
3282
  `);
3103
- const hintList = outcome.error.heuristicFlags.join(", ");
3104
- if (hintList.length > 0) {
3105
- process.stderr.write(` Hints: ${hintList}
3283
+ const hintList = outcome.error.heuristicFlags.join(", ");
3284
+ if (hintList.length > 0) {
3285
+ process.stderr.write(` Hints: ${hintList}
3106
3286
  `);
3287
+ }
3288
+ anyFailed = true;
3289
+ continue;
3107
3290
  }
3108
- process.exit(1);
3109
- }
3110
- const result = outcome.result;
3111
- if (opts.output !== void 0) {
3112
- const outPath = path.resolve(process.cwd(), opts.output);
3113
- fs.writeFileSync(outPath, result.screenshot);
3114
- process.stdout.write(
3115
- `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3291
+ const result = outcome.result;
3292
+ const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
3293
+ if (opts.output !== void 0 && !isNamed) {
3294
+ const outPath = path.resolve(process.cwd(), opts.output);
3295
+ fs.writeFileSync(outPath, result.screenshot);
3296
+ process.stdout.write(
3297
+ `\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3116
3298
  `
3117
- );
3118
- return;
3119
- }
3120
- const fmt2 = resolveSingleFormat(opts.format);
3121
- if (fmt2 === "json") {
3122
- const json = formatRenderJson(componentName, props, result);
3123
- process.stdout.write(`${JSON.stringify(json, null, 2)}
3299
+ );
3300
+ } else if (fmt2 === "json") {
3301
+ const json = formatRenderJson(label, props, result);
3302
+ process.stdout.write(`${JSON.stringify(json, null, 2)}
3124
3303
  `);
3125
- } else if (fmt2 === "file") {
3126
- const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3127
- fs.mkdirSync(dir, { recursive: true });
3128
- const outPath = path.resolve(dir, `${componentName}.png`);
3129
- fs.writeFileSync(outPath, result.screenshot);
3130
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3131
- process.stdout.write(
3132
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3133
- `
3134
- );
3135
- } else {
3136
- const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3137
- fs.mkdirSync(dir, { recursive: true });
3138
- const outPath = path.resolve(dir, `${componentName}.png`);
3139
- fs.writeFileSync(outPath, result.screenshot);
3140
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3141
- process.stdout.write(
3142
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3304
+ } else {
3305
+ const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3306
+ fs.mkdirSync(dir, { recursive: true });
3307
+ const outPath = path.resolve(dir, outFileName);
3308
+ fs.writeFileSync(outPath, result.screenshot);
3309
+ const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
3310
+ process.stdout.write(
3311
+ `\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3143
3312
  `
3144
- );
3313
+ );
3314
+ }
3145
3315
  }
3316
+ await shutdownPool3();
3317
+ if (anyFailed) process.exit(1);
3146
3318
  } catch (err) {
3147
3319
  await shutdownPool3();
3148
3320
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}