@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/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 esbuild from 'esbuild';
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, preScript) {
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, preScript);
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
- createRoot(rootEl).render(createElement(Component, props));
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 esbuild.build({
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, preScript) {
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 preScriptBlock = preScript != null && preScript.length > 0 ? `<script>${preScript}</script>
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
- ${preScriptBlock}<script>${bundledScript}</script>
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 build2 = result.build.bind(result);
488
- compilerCache = { cwd, build: build2 };
489
- return build2;
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 build2 = await getTailwindCompiler(cwd);
493
- if (build2 === null) return null;
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 build2(deduped);
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((resolve17) => {
1120
+ return new Promise((resolve18) => {
1115
1121
  rl.question(question, (answer) => {
1116
- resolve17(answer.trim());
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 renderer = buildRenderer(filePath, componentName, width, height);
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 outcome = await safeRender(
3092
- () => renderer.renderCell(props, descriptor.complexityClass),
3093
- {
3094
- props,
3095
- sourceLocation: {
3096
- file: descriptor.filePath,
3097
- line: descriptor.loc.start,
3098
- column: 0
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
- await shutdownPool3();
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
- const hintList = outcome.error.heuristicFlags.join(", ");
3107
- if (hintList.length > 0) {
3108
- process.stderr.write(` Hints: ${hintList}
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
- process.exit(1);
3112
- }
3113
- const result = outcome.result;
3114
- if (opts.output !== void 0) {
3115
- const outPath = resolve(process.cwd(), opts.output);
3116
- writeFileSync(outPath, result.screenshot);
3117
- process.stdout.write(
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
- return;
3122
- }
3123
- const fmt2 = resolveSingleFormat(opts.format);
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
- } else if (fmt2 === "file") {
3129
- const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3130
- mkdirSync(dir, { recursive: true });
3131
- const outPath = resolve(dir, `${componentName}.png`);
3132
- writeFileSync(outPath, result.screenshot);
3133
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3134
- process.stdout.write(
3135
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
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)}