@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.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, preScript) {
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, preScript);
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,12 +131,11 @@ ${msg}`);
124
131
  }
125
132
  return outputFile.text;
126
133
  }
127
- function wrapInHtml(bundledScript, viewportWidth, projectCss, preScript) {
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>` : "";
131
- const preScriptBlock = preScript != null && preScript.length > 0 ? `<script>${preScript}</script>
132
- ` : "";
138
+ const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
133
139
  return `<!DOCTYPE html>
134
140
  <html lang="en">
135
141
  <head>
@@ -144,7 +150,8 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
144
150
  </head>
145
151
  <body>
146
152
  <div id="scope-root" data-reactscope-root></div>
147
- ${preScriptBlock}<script>${bundledScript}</script>
153
+ ${wrapperScriptBlock}
154
+ <script>${bundledScript}</script>
148
155
  </body>
149
156
  </html>`;
150
157
  }
@@ -507,16 +514,16 @@ async function getTailwindCompiler(cwd) {
507
514
  from: entryPath,
508
515
  loadStylesheet
509
516
  });
510
- const build2 = result.build.bind(result);
511
- compilerCache = { cwd, build: build2 };
512
- return build2;
517
+ const build3 = result.build.bind(result);
518
+ compilerCache = { cwd, build: build3 };
519
+ return build3;
513
520
  }
514
521
  async function getCompiledCssForClasses(cwd, classes) {
515
- const build2 = await getTailwindCompiler(cwd);
516
- if (build2 === null) return null;
522
+ const build3 = await getTailwindCompiler(cwd);
523
+ if (build3 === null) return null;
517
524
  const deduped = [...new Set(classes)].filter(Boolean);
518
525
  if (deduped.length === 0) return null;
519
- return build2(deduped);
526
+ return build3(deduped);
520
527
  }
521
528
 
522
529
  // src/ci/commands.ts
@@ -1134,9 +1141,9 @@ function createRL() {
1134
1141
  });
1135
1142
  }
1136
1143
  async function ask(rl, question) {
1137
- return new Promise((resolve17) => {
1144
+ return new Promise((resolve18) => {
1138
1145
  rl.question(question, (answer) => {
1139
- resolve17(answer.trim());
1146
+ resolve18(answer.trim());
1140
1147
  });
1141
1148
  });
1142
1149
  }
@@ -2958,6 +2965,122 @@ function writeReportToFile(report, outputPath, pretty) {
2958
2965
  const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
2959
2966
  fs.writeFileSync(outputPath, json, "utf-8");
2960
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
2961
3084
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
2962
3085
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
2963
3086
  var _pool3 = null;
@@ -2978,7 +3101,7 @@ async function shutdownPool3() {
2978
3101
  _pool3 = null;
2979
3102
  }
2980
3103
  }
2981
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
3104
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
2982
3105
  const satori = new render.SatoriRenderer({
2983
3106
  defaultViewport: { width: viewportWidth, height: viewportHeight }
2984
3107
  });
@@ -2991,7 +3114,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
2991
3114
  filePath,
2992
3115
  componentName,
2993
3116
  props,
2994
- viewportWidth
3117
+ viewportWidth,
3118
+ void 0,
3119
+ // projectCss (handled separately)
3120
+ wrapperScript
2995
3121
  );
2996
3122
  const slot = await pool.acquire();
2997
3123
  const { page } = slot;
@@ -3082,8 +3208,37 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
3082
3208
  }
3083
3209
  };
3084
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
+ }
3085
3240
  function registerRenderSingle(renderCmd) {
3086
- 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(
3087
3242
  async (componentName, opts) => {
3088
3243
  try {
3089
3244
  const manifest = loadManifest(opts.manifest);
@@ -3095,80 +3250,71 @@ function registerRenderSingle(renderCmd) {
3095
3250
  Available: ${available}`
3096
3251
  );
3097
3252
  }
3098
- let props = {};
3099
- if (opts.props !== void 0) {
3100
- try {
3101
- props = JSON.parse(opts.props);
3102
- } catch {
3103
- throw new Error(`Invalid props JSON: ${opts.props}`);
3104
- }
3105
- }
3106
3253
  const { width, height } = parseViewport(opts.viewport);
3107
3254
  const rootDir = process.cwd();
3108
3255
  const filePath = path.resolve(rootDir, descriptor.filePath);
3109
- 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);
3110
3260
  process.stderr.write(
3111
3261
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3112
3262
  `
3113
3263
  );
3114
- const outcome = await render.safeRender(
3115
- () => renderer.renderCell(props, descriptor.complexityClass),
3116
- {
3117
- props,
3118
- sourceLocation: {
3119
- file: descriptor.filePath,
3120
- line: descriptor.loc.start,
3121
- 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
+ }
3122
3278
  }
3123
- }
3124
- );
3125
- await shutdownPool3();
3126
- if (outcome.crashed) {
3127
- 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}
3128
3282
  `);
3129
- const hintList = outcome.error.heuristicFlags.join(", ");
3130
- if (hintList.length > 0) {
3131
- process.stderr.write(` Hints: ${hintList}
3283
+ const hintList = outcome.error.heuristicFlags.join(", ");
3284
+ if (hintList.length > 0) {
3285
+ process.stderr.write(` Hints: ${hintList}
3132
3286
  `);
3287
+ }
3288
+ anyFailed = true;
3289
+ continue;
3133
3290
  }
3134
- process.exit(1);
3135
- }
3136
- const result = outcome.result;
3137
- if (opts.output !== void 0) {
3138
- const outPath = path.resolve(process.cwd(), opts.output);
3139
- fs.writeFileSync(outPath, result.screenshot);
3140
- process.stdout.write(
3141
- `\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)
3142
3298
  `
3143
- );
3144
- return;
3145
- }
3146
- const fmt2 = resolveSingleFormat(opts.format);
3147
- if (fmt2 === "json") {
3148
- const json = formatRenderJson(componentName, props, result);
3149
- 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)}
3150
3303
  `);
3151
- } else if (fmt2 === "file") {
3152
- const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3153
- fs.mkdirSync(dir, { recursive: true });
3154
- const outPath = path.resolve(dir, `${componentName}.png`);
3155
- fs.writeFileSync(outPath, result.screenshot);
3156
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3157
- process.stdout.write(
3158
- `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3159
- `
3160
- );
3161
- } else {
3162
- const dir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3163
- fs.mkdirSync(dir, { recursive: true });
3164
- const outPath = path.resolve(dir, `${componentName}.png`);
3165
- fs.writeFileSync(outPath, result.screenshot);
3166
- const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
3167
- process.stdout.write(
3168
- `\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)
3169
3312
  `
3170
- );
3313
+ );
3314
+ }
3171
3315
  }
3316
+ await shutdownPool3();
3317
+ if (anyFailed) process.exit(1);
3172
3318
  } catch (err) {
3173
3319
  await shutdownPool3();
3174
3320
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}