@storyshelf/runner-playwright 0.7.0 → 0.8.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.
Files changed (48) hide show
  1. package/README.md +6 -1
  2. package/dist/a11y.mjs +7 -5
  3. package/dist/a11y.mjs.map +1 -1
  4. package/dist/availability.mjs +2 -4
  5. package/dist/availability.mjs.map +1 -1
  6. package/dist/browser.mjs +31 -8
  7. package/dist/browser.mjs.map +1 -1
  8. package/dist/capture-retry.mjs +25 -0
  9. package/dist/capture-retry.mjs.map +1 -0
  10. package/dist/capture-runner.d.mts +2 -2
  11. package/dist/capture-runner.mjs +29 -12
  12. package/dist/capture-runner.mjs.map +1 -1
  13. package/dist/connect.mjs +23 -41
  14. package/dist/connect.mjs.map +1 -1
  15. package/dist/launch.mjs +1 -2
  16. package/dist/pages.mjs +1 -2
  17. package/dist/pipeline.mjs +84 -26
  18. package/dist/pipeline.mjs.map +1 -1
  19. package/dist/play.mjs +1 -2
  20. package/dist/remote/azure.d.mts +0 -2
  21. package/dist/remote/azure.mjs +6 -7
  22. package/dist/remote/azure.mjs.map +1 -1
  23. package/dist/remote/browserstack.d.mts +0 -2
  24. package/dist/remote/browserstack.mjs +4 -6
  25. package/dist/remote/browserstack.mjs.map +1 -1
  26. package/dist/remote/index.d.mts +2 -1
  27. package/dist/remote/index.mjs +2 -1
  28. package/dist/remote/playwright-server.d.mts +10 -5
  29. package/dist/remote/playwright-server.mjs +9 -8
  30. package/dist/remote/playwright-server.mjs.map +1 -1
  31. package/dist/remote/remote.d.mts +20 -4
  32. package/dist/remote/remote.mjs +20 -3
  33. package/dist/remote/remote.mjs.map +1 -1
  34. package/dist/remote-environment.mjs +31 -18
  35. package/dist/remote-environment.mjs.map +1 -1
  36. package/dist/runtime-params.mjs +1 -2
  37. package/dist/screenshot.mjs +65 -13
  38. package/dist/screenshot.mjs.map +1 -1
  39. package/dist/settle.mjs +15 -8
  40. package/dist/settle.mjs.map +1 -1
  41. package/dist/static-server.mjs +72 -10
  42. package/dist/static-server.mjs.map +1 -1
  43. package/dist/targets.d.mts +0 -2
  44. package/dist/targets.mjs +1 -2
  45. package/dist/tests.mjs +9 -12
  46. package/dist/tests.mjs.map +1 -1
  47. package/dist/version.mjs +1 -2
  48. package/package.json +8 -8
package/dist/pipeline.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { messageOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
4
+ import { finishing, messageOf, outcomeOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
5
+ import { TransientCaptureError } from "./capture-retry.mjs";
5
6
  import { PagePool } from "./pages.mjs";
6
7
  import { measureRemote } from "./remote-environment.mjs";
7
8
  import { createRuntimeParamsState, extractAllParameters } from "./runtime-params.mjs";
@@ -10,41 +11,71 @@ import { createStaticServer } from "./static-server.mjs";
10
11
  import { TargetLaunchError, engineOf } from "@storyshelf/core/adapter/capture-runner";
11
12
  import { DEFAULT_RECYCLE_BROWSER_EVERY, chunk, createPageLimiter, describeSizing, detectCaptureEnvironment, isTimeoutError, sessionGateFor, storybookBase } from "@storyshelf/core/utils";
12
13
  import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
13
- //#region src/pipeline.ts
14
+ // src/pipeline.ts
14
15
  /** Render orchestration: static server, browser, and per-story captures. */
15
16
  /** Render all story/viewport combinations for a build. */
16
17
  async function renderAll(rawInput, active, driver, defaultTarget) {
17
18
  const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
18
19
  assertReachable(name, remote, input.storybookUrl);
19
- const browser = await driver.launch(name, input.logger);
20
+ const launch = async () => await driver.launch(name, input.logger, launchContextOf(input, "render", active.abort?.signal));
21
+ const browser = await launch();
20
22
  const server = await serve(input, browser);
21
23
  input.logger?.info(describeSizing({
22
24
  ...input,
23
25
  browser: engineOf(name)
24
26
  }), "capture sizing");
25
- const measured = remote ? await measureRemote(browser, remote.remote, input.logger) : void 0;
26
- const result = await runWithBrowser(input, active, browser, server, async () => await driver.launch(name, input.logger));
27
+ const measured = remote ? await measureRemote(browser, remote.remote, {
28
+ logger: input.logger,
29
+ signal: active.abort?.signal
30
+ }).catch(async (error) => {
31
+ await Promise.all([safeCloseBrowser(browser, outcomeOf(active, true), error), safeCloseServer(server)]);
32
+ throw error;
33
+ }) : void 0;
34
+ const result = await runWithBrowser({
35
+ ...input,
36
+ retryRemoteCapture: remote !== void 0
37
+ }, active, browser, server, launch);
27
38
  for (const row of [...result.captures, ...result.failures]) row.browser = name;
28
39
  return {
29
40
  ...result,
30
41
  environment: measured ?? await localEnvironment(browser)
31
42
  };
32
43
  }
44
+ /** What a remote service's `acquire` is told about the run a browser is for. */
45
+ function launchContextOf(input, purpose, signal) {
46
+ return {
47
+ buildId: input.buildId,
48
+ runId: input.runId ?? input.buildId,
49
+ shardIndex: input.shardIndex,
50
+ purpose,
51
+ signal
52
+ };
53
+ }
33
54
  /** The input with its target resolved, and the session gate when the target is remote. */
34
55
  function planRun(rawInput, driver, defaultTarget) {
35
56
  const name = rawInput.target ?? defaultTarget;
36
57
  const remote = driver.remote(name);
37
- const gate = remote && sessionGateFor(name, remote.remote.sessions);
58
+ const gate = remote && sessionGateFor(name, remote.remote.sessions, (value) => {
59
+ rawInput.logger?.warn({
60
+ target: name,
61
+ value: String(value)
62
+ }, "sessions() returned an invalid value; keeping the last good one");
63
+ });
38
64
  return {
39
65
  input: {
40
66
  ...rawInput,
41
67
  target: name,
42
- gate
68
+ gate,
69
+ maxPagesPerConnection: remote?.remote.maxPagesPerConnection
43
70
  },
44
71
  name,
45
72
  remote
46
73
  };
47
74
  }
75
+ /** Apply the connection's cap without changing the target-wide session budget. */
76
+ function connectionConcurrency(input) {
77
+ return input.maxPagesPerConnection === void 0 ? input.concurrency : Math.min(input.concurrency ?? Infinity, input.maxPagesPerConnection);
78
+ }
48
79
  /**
49
80
  * Load the preview once and read every story's resolved parameters
50
81
  * (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.
@@ -54,13 +85,11 @@ async function extractAll(input, driver, defaultTarget) {
54
85
  if (!first) return {};
55
86
  const name = input.target ?? defaultTarget;
56
87
  assertReachable(name, driver.remote(name), input.storybookUrl);
57
- const browser = await driver.launch(name, input.logger);
88
+ const browser = await driver.launch(name, input.logger, launchContextOf(input, "extract"));
58
89
  const server = await serve(input, browser);
59
- try {
60
- return await readParameters(browser, server.url, first.id, input.adapter);
61
- } finally {
62
- await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);
63
- }
90
+ return await finishing(async () => await readParameters(browser, server.url, first.id, input.adapter), async (failed, error) => {
91
+ await Promise.all([safeCloseBrowser(browser, failed ? "failed" : "finished", error), safeCloseServer(server)]);
92
+ });
64
93
  }
65
94
  /** A target whose browser cannot route back to this host needs a URL; failing here names why. */
66
95
  function assertReachable(target, remote, storybookUrl) {
@@ -83,8 +112,8 @@ async function serve(input, browser) {
83
112
  await Promise.resolve();
84
113
  }
85
114
  };
86
- return await createStaticServer(input.storybookDir).catch(async (error) => {
87
- await safeCloseBrowser(browser);
115
+ return await createStaticServer(input.storybookDir, { compression: input.staticCompression }).catch(async (error) => {
116
+ await safeCloseBrowser(browser, "failed", error);
88
117
  throw error;
89
118
  });
90
119
  }
@@ -108,17 +137,17 @@ async function runWithBrowser(input, active, browser, server, relaunch) {
108
137
  const ctx = buildContext(browser, server.url, input);
109
138
  const captures = [];
110
139
  const failures = [];
111
- try {
140
+ return await finishing(async () => {
112
141
  await runAllCaptures(input, active, ctx, captures, failures, relaunch);
113
142
  return {
114
143
  captures,
115
144
  failures
116
145
  };
117
- } finally {
146
+ }, async (failed, error) => {
118
147
  active.browser = null;
119
148
  await ctx.pagePool?.dispose();
120
- await Promise.all([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);
121
- }
149
+ await Promise.all([safeCloseBrowser(ctx.browser, outcomeOf(active, failed), error), safeCloseServer(server)]);
150
+ });
122
151
  }
123
152
  function buildContext(browser, baseUrl, input) {
124
153
  return {
@@ -141,7 +170,7 @@ async function runAllCaptures(input, active, ctx, captures, failures, relaunch)
141
170
  })));
142
171
  const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);
143
172
  const pages = createPageLimiter({
144
- concurrency: input.concurrency,
173
+ concurrency: connectionConcurrency(input),
145
174
  ...input.gate === void 0 ? {} : { gate: input.gate },
146
175
  browser: engineOf(input.target ?? "chromium"),
147
176
  logger: input.logger
@@ -167,6 +196,7 @@ async function recycleBrowser(active, ctx, relaunch, logger) {
167
196
  if (active.cancelled) throw new Error("Capture cancelled");
168
197
  await ctx.pagePool?.dispose();
169
198
  await safeCloseBrowser(ctx.browser);
199
+ if (active.cancelled) throw new Error("Capture cancelled");
170
200
  ctx.browser = await relaunch();
171
201
  if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);
172
202
  active.browser = ctx.browser;
@@ -176,16 +206,16 @@ async function recycleBrowser(active, ctx, relaunch, logger) {
176
206
  async function captureOne(input, active, ctx, story, viewport, out) {
177
207
  if (active.cancelled) throw new Error("Capture cancelled");
178
208
  try {
179
- await pushCapture(ctx, story, viewport, out.captures, out.failures, input);
209
+ await pushCapture(ctx, story, viewport, out.captures, out.failures, input, active);
180
210
  return false;
181
211
  } catch (error) {
182
212
  pushRenderFailure(out.failures, input, story.id, viewport.name, error);
183
- return isTimeoutError(error);
213
+ return isTimeoutError(error instanceof TransientCaptureError ? error.cause : error);
184
214
  }
185
215
  }
186
- async function pushCapture(ctx, story, viewport, captures, failures, input) {
216
+ async function pushCapture(ctx, story, viewport, captures, failures, input, active) {
187
217
  const started = performance.now();
188
- const result = await captureScreenshot(ctx, story, viewport);
218
+ const result = await screenshotWithRetry(ctx, story, viewport, input, active);
189
219
  const snapshot = {
190
220
  story: result.story,
191
221
  viewportName: viewport.name,
@@ -198,6 +228,35 @@ async function pushCapture(ctx, story, viewport, captures, failures, input) {
198
228
  else captures.push(snapshot);
199
229
  if (result.a11yViolations.length > 0) handleA11y(input, failures, story, viewport, result.a11yViolations);
200
230
  }
231
+ /** Retry capture only: delivery and accessibility reporting must never be repeated. */
232
+ async function screenshotWithRetry(ctx, story, viewport, input, active) {
233
+ try {
234
+ const result = await captureScreenshot(ctx, story, viewport);
235
+ if (active.cancelled || active.abort?.signal.aborted) throw new Error("Capture cancelled");
236
+ return result;
237
+ } catch (error) {
238
+ if (!input.retryRemoteCapture || !(error instanceof TransientCaptureError) || active.cancelled || active.abort?.signal.aborted || !ctx.browser.isConnected()) throw error;
239
+ return await freshScreenshot(ctx, story, viewport, input, active, error);
240
+ }
241
+ }
242
+ async function freshScreenshot(ctx, story, viewport, input, active, firstError) {
243
+ const detail = {
244
+ storyId: story.id,
245
+ viewport: viewport.name,
246
+ phase: firstError.phase,
247
+ attempt: 2
248
+ };
249
+ input.logger?.warn(detail, "retrying remote capture on a fresh page");
250
+ try {
251
+ const result = await captureScreenshot(ctx, story, viewport, true);
252
+ if (active.cancelled || active.abort?.signal.aborted) throw new Error("Capture cancelled", { cause: firstError });
253
+ input.logger?.info(detail, "remote capture retry succeeded");
254
+ return result;
255
+ } catch (retryError) {
256
+ input.logger?.warn(detail, "remote capture retry failed");
257
+ throw retryError;
258
+ }
259
+ }
201
260
  function handleA11y(input, failures, story, viewport, violations) {
202
261
  failures.push({
203
262
  storyId: story.id,
@@ -222,7 +281,6 @@ function pushRenderFailure(failures, input, storyId, viewportName, error) {
222
281
  err: error
223
282
  }, "render failed for story");
224
283
  }
225
- //#endregion
226
- export { assertReachable, extractAll, planRun, renderAll, serve };
284
+ export { assertReachable, connectionConcurrency, extractAll, launchContextOf, planRun, renderAll, screenshotWithRetry, serve };
227
285
 
228
286
  //# sourceMappingURL=pipeline.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["/** Render orchestration: static server, browser, and per-story captures. */\n\nimport {\n TargetLaunchError,\n engineOf,\n type CaptureEnvironment,\n type CaptureTarget,\n type RemoteTarget,\n type RenderResult,\n type RenderedSnapshot,\n type StoryEntry,\n type StorySourceAdapter,\n type Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport {\n DEFAULT_RECYCLE_BROWSER_EVERY,\n chunk,\n describeSizing,\n createPageLimiter,\n detectCaptureEnvironment,\n isTimeoutError,\n sessionGateFor,\n storybookBase,\n type SessionGate,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"playwright-core\";\nimport { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { PagePool } from \"./pages.ts\";\nimport { measureRemote } from \"./remote-environment.ts\";\nimport { createRuntimeParamsState, extractAllParameters } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n ExtractInput,\n PlaywrightRenderInput,\n RunInput,\n RuntimeParamsPage,\n TargetDriver,\n} from \"./types.ts\";\n\n/** Where the browser finds the Storybook: the loopback server, or the URL core handed over. */\ninterface Serving {\n url: string;\n close(): Promise<void>;\n}\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n rawInput: PlaywrightRenderInput,\n active: ActiveRun,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<RenderResult> {\n const { input, name, remote } = planRun(rawInput, driver, defaultTarget);\n assertReachable(name, remote, input.storybookUrl);\n // Launch first: a target that will not start must fail before a static server is bound.\n const browser = await driver.launch(name, input.logger);\n const server = await serve(input, browser);\n input.logger?.info(describeSizing({ ...input, browser: engineOf(name) }), \"capture sizing\");\n const measured = remote ? await measureRemote(browser, remote.remote, input.logger) : undefined;\n const result = await runWithBrowser(\n input,\n active,\n browser,\n server,\n async () => await driver.launch(name, input.logger),\n );\n // Echo the target so persisted rows are attributable without the request.\n for (const row of [...result.captures, ...result.failures]) row.browser = name;\n return { ...result, environment: measured ?? (await localEnvironment(browser)) };\n}\n\n/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<T extends { target?: CaptureTarget | undefined }>(\n rawInput: T,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): {\n input: T & { target: CaptureTarget; gate?: SessionGate | undefined };\n name: CaptureTarget;\n remote: RemoteTarget | undefined;\n} {\n const name = rawInput.target ?? defaultTarget;\n const remote = driver.remote(name);\n const gate = remote && sessionGateFor(name, remote.remote.sessions);\n return { input: { ...rawInput, target: name, gate }, name, remote };\n}\n\n/**\n * Load the preview once and read every story's resolved parameters\n * (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.\n */\nexport async function extractAll(\n input: ExtractInput,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<Record<string, StoryParameters>> {\n const first = input.stories[0];\n if (!first) return {};\n const name = input.target ?? defaultTarget;\n assertReachable(name, driver.remote(name), input.storybookUrl);\n const browser = await driver.launch(name, input.logger);\n const server = await serve(input, browser);\n try {\n return await readParameters(browser, server.url, first.id, input.adapter);\n } finally {\n await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);\n }\n}\n\n/** A target whose browser cannot route back to this host needs a URL; failing here names why. */\nexport function assertReachable(\n target: CaptureTarget,\n remote: RemoteTarget | undefined,\n storybookUrl: string | undefined,\n): void {\n if (remote?.reach === \"url\" && storybookUrl === undefined) {\n throw new TargetLaunchError(\n target,\n \"its browser cannot reach this host, so it needs a Storybook URL (a public origin and a secret to sign it) and none was given\",\n { transient: true },\n );\n }\n if (storybookUrl !== undefined) {\n assertStorybookRoot(target, storybookUrl);\n }\n}\n\n/** The URL must be the build's directory root; refused before any browser connects. */\nfunction assertStorybookRoot(target: CaptureTarget, storybookUrl: string): void {\n try {\n storybookBase(storybookUrl);\n } catch (error) {\n throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));\n }\n}\n\n/** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */\nexport async function serve(\n input: Pick<RunInput, \"storybookDir\" | \"storybookUrl\">,\n browser: Browser,\n): Promise<Serving> {\n if (input.storybookUrl !== undefined) {\n return {\n url: storybookBase(input.storybookUrl),\n close: async () => {\n await Promise.resolve();\n },\n };\n }\n return await createStaticServer(input.storybookDir).catch(async (error: unknown) => {\n await safeCloseBrowser(browser);\n throw error;\n });\n}\n\nasync function readParameters(\n browser: Browser,\n baseUrl: string,\n storyId: string,\n custom: StorySourceAdapter | undefined,\n): Promise<Record<string, StoryParameters>> {\n const adapter: StorySourceAdapter = custom ?? new StorybookAdapter();\n const page = await browser.newPage();\n await page.goto(adapter.buildUrl(baseUrl, storyId), { waitUntil: \"networkidle\" });\n await adapter.waitForReady?.(page);\n return await extractAllParameters(page as unknown as RuntimeParamsPage);\n}\n\n/** The driver's browser version; undefined for a driver (or test double) that does not report one. */\nfunction browserVersionOf(browser: Browser): string | undefined {\n return typeof browser.version === \"function\" ? browser.version() : undefined;\n}\n\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function localEnvironment(browser: Browser): Promise<CaptureEnvironment> {\n return await detectCaptureEnvironment(browserVersionOf(browser));\n}\n\nasync function runWithBrowser(\n input: RunInput,\n active: ActiveRun,\n browser: Browser,\n server: Serving,\n relaunch: () => Promise<Browser>,\n): Promise<RenderResult> {\n active.browser = browser;\n const ctx = buildContext(browser, server.url, input);\n const captures: RenderedSnapshot[] = [];\n const failures: RenderResult[\"failures\"] = [];\n try {\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n } finally {\n active.browser = null;\n await ctx.pagePool?.dispose();\n // ctx.browser is the latest one: recycling replaces the browser mid-run.\n await Promise.all([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);\n }\n}\n\nfunction buildContext(\n browser: Browser,\n baseUrl: string,\n input: PlaywrightRenderInput,\n): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n navigationTimeoutMs: input.navigationTimeoutMs,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n ...(input.logger ? { logger: input.logger } : {}),\n runtimeParams: createRuntimeParamsState(),\n ...(input.reusePages ? { pagePool: new PagePool(browser) } : {}),\n };\n}\n\nasync function runAllCaptures(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n relaunch: () => Promise<Browser>,\n): Promise<void> {\n // Bounded: one page per story x viewport all at once exhausts memory on large Storybooks.\n const jobs = input.stories.flatMap((story) =>\n resolveStoryViewports(story, input.viewports).map((viewport) => ({ story, viewport })),\n );\n // Batches drain in-flight pages, so the browser can be swapped between them.\n const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);\n const pages = createPageLimiter({\n concurrency: input.concurrency,\n ...(input.gate === undefined ? {} : { gate: input.gate }),\n browser: engineOf(input.target ?? \"chromium\"),\n logger: input.logger,\n });\n try {\n await runBatches(batches, pages, input, active, ctx, { captures, failures, relaunch });\n } finally {\n pages.dispose();\n }\n}\n\nasync function runBatches(\n batches: { story: StoryEntry; viewport: Viewport }[][],\n pages: ReturnType<typeof createPageLimiter>,\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n out: {\n captures: RenderedSnapshot[];\n failures: RenderResult[\"failures\"];\n relaunch: () => Promise<Browser>;\n },\n): Promise<void> {\n for (const [index, batch] of batches.entries()) {\n if (index > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- recycle only between drained batches\n await recycleBrowser(active, ctx, out.relaunch, input.logger);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- batches run one after another\n await pages.each(\n batch,\n async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out),\n );\n }\n}\n\n/** Swap in a fresh browser; the old one is closed first so two never coexist. */\nasync function recycleBrowser(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<Browser>,\n logger: PlaywrightRenderInput[\"logger\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n await ctx.pagePool?.dispose();\n await safeCloseBrowser(ctx.browser);\n ctx.browser = await relaunch();\n if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);\n active.browser = ctx.browser;\n logger?.info({}, \"browser recycled\");\n}\n\n/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */\nasync function captureOne(\n input: PlaywrightRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n out: { captures: RenderedSnapshot[]; failures: RenderResult[\"failures\"] },\n): Promise<boolean> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, out.captures, out.failures, input);\n return false;\n } catch (error) {\n pushRenderFailure(out.failures, input, story.id, viewport.name, error);\n return isTimeoutError(error);\n }\n}\n\nasync function pushCapture(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n input: PlaywrightRenderInput,\n): Promise<void> {\n const started = performance.now();\n const result = await captureScreenshot(ctx, story, viewport);\n const snapshot: RenderedSnapshot = {\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n durationMs: Math.round(performance.now() - started),\n ...(result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}),\n };\n // Streaming: hand it over and drop the buffer rather than holding every screenshot.\n if (input.onCapture) await input.onCapture(snapshot);\n else captures.push(snapshot);\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\n}\nfunction handleA11y(\n input: PlaywrightRenderInput,\n failures: RenderResult[\"failures\"],\n story: StoryEntry,\n viewport: Viewport,\n violations: string[],\n): void {\n failures.push({\n storyId: story.id,\n viewportName: viewport.name,\n error: `a11y: ${violations.join(\"; \")}`,\n });\n input.logger?.warn(\n { storyId: story.id, viewport: viewport.name, violations },\n \"a11y violations found\",\n );\n}\n\nfunction pushRenderFailure(\n failures: RenderResult[\"failures\"],\n input: PlaywrightRenderInput,\n storyId: string,\n viewportName: string,\n error: unknown,\n): void {\n failures.push({ storyId, viewportName, error: messageOf(error) });\n input.logger?.error({ storyId, viewport: viewportName, err: error }, \"render failed for story\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["/** Render orchestration: static server, browser, and per-story captures. */\n\nimport {\n TargetLaunchError,\n engineOf,\n type CaptureEnvironment,\n type CaptureTarget,\n type RemoteTarget,\n type RenderResult,\n type RenderedSnapshot,\n type StoryEntry,\n type StorySourceAdapter,\n type Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport {\n DEFAULT_RECYCLE_BROWSER_EVERY,\n chunk,\n describeSizing,\n createPageLimiter,\n detectCaptureEnvironment,\n isTimeoutError,\n sessionGateFor,\n storybookBase,\n type SessionGate,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"playwright-core\";\nimport { finishing, messageOf, outcomeOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { TransientCaptureError } from \"./capture-retry.ts\";\nimport { PagePool } from \"./pages.ts\";\nimport { measureRemote } from \"./remote-environment.ts\";\nimport { createRuntimeParamsState, extractAllParameters } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n ExtractInput,\n PlaywrightRenderInput,\n RunInput,\n RuntimeParamsPage,\n LaunchContext,\n TargetDriver,\n} from \"./types.ts\";\n\n/** Where the browser finds the Storybook: the loopback server, or the URL core handed over. */\ninterface Serving {\n url: string;\n close(): Promise<void>;\n}\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n rawInput: RunInput,\n active: ActiveRun,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<RenderResult> {\n const { input, name, remote } = planRun(rawInput, driver, defaultTarget);\n assertReachable(name, remote, input.storybookUrl);\n // Launch first: a target that will not start must fail before a static server is bound.\n const launch = async (): Promise<Browser> =>\n await driver.launch(name, input.logger, launchContextOf(input, \"render\", active.abort?.signal));\n const browser = await launch();\n const server = await serve(input, browser);\n input.logger?.info(describeSizing({ ...input, browser: engineOf(name) }), \"capture sizing\");\n const measured = remote\n ? await measureRemote(browser, remote.remote, {\n logger: input.logger,\n signal: active.abort?.signal,\n }).catch(async (error: unknown) => {\n // A cancel that interrupted the probe releases the lease as cancelled, not failed.\n await Promise.all([\n safeCloseBrowser(browser, outcomeOf(active, true), error),\n safeCloseServer(server),\n ]);\n throw error;\n })\n : undefined;\n const result = await runWithBrowser(\n { ...input, retryRemoteCapture: remote !== undefined },\n active,\n browser,\n server,\n launch,\n );\n // Echo the target so persisted rows are attributable without the request.\n for (const row of [...result.captures, ...result.failures]) row.browser = name;\n return { ...result, environment: measured ?? (await localEnvironment(browser)) };\n}\n\n/** What a remote service's `acquire` is told about the run a browser is for. */\nexport function launchContextOf(\n input: { buildId: string; runId?: string | undefined; shardIndex?: number | undefined },\n purpose: LaunchContext[\"purpose\"],\n signal?: AbortSignal,\n): LaunchContext {\n return {\n buildId: input.buildId,\n runId: input.runId ?? input.buildId,\n shardIndex: input.shardIndex,\n purpose,\n signal,\n };\n}\n\n/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<\n T extends { target?: CaptureTarget | undefined; logger?: PlaywrightRenderInput[\"logger\"] },\n>(\n rawInput: T,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): {\n input: T & {\n target: CaptureTarget;\n gate?: SessionGate | undefined;\n maxPagesPerConnection?: number | undefined;\n };\n name: CaptureTarget;\n remote: RemoteTarget | undefined;\n} {\n const name = rawInput.target ?? defaultTarget;\n const remote = driver.remote(name);\n const gate =\n remote &&\n sessionGateFor(name, remote.remote.sessions, (value) => {\n rawInput.logger?.warn(\n { target: name, value: String(value) },\n \"sessions() returned an invalid value; keeping the last good one\",\n );\n });\n return {\n input: {\n ...rawInput,\n target: name,\n gate,\n maxPagesPerConnection: remote?.remote.maxPagesPerConnection,\n },\n name,\n remote,\n };\n}\n\n/** Apply the connection's cap without changing the target-wide session budget. */\nexport function connectionConcurrency(input: {\n concurrency?: number | undefined;\n maxPagesPerConnection?: number | undefined;\n}): number | undefined {\n return input.maxPagesPerConnection === undefined\n ? input.concurrency\n : Math.min(input.concurrency ?? Infinity, input.maxPagesPerConnection);\n}\n\n/**\n * Load the preview once and read every story's resolved parameters\n * (`__STORYBOOK_PREVIEW__.extract()`), so the orchestrator can plan modes.\n */\nexport async function extractAll(\n input: ExtractInput,\n driver: TargetDriver,\n defaultTarget: CaptureTarget,\n): Promise<Record<string, StoryParameters>> {\n const first = input.stories[0];\n if (!first) return {};\n const name = input.target ?? defaultTarget;\n assertReachable(name, driver.remote(name), input.storybookUrl);\n const browser = await driver.launch(name, input.logger, launchContextOf(input, \"extract\"));\n const server = await serve(input, browser);\n return await finishing(\n async () => await readParameters(browser, server.url, first.id, input.adapter),\n async (failed, error) => {\n await Promise.all([\n safeCloseBrowser(browser, failed ? \"failed\" : \"finished\", error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\n/** A target whose browser cannot route back to this host needs a URL; failing here names why. */\nexport function assertReachable(\n target: CaptureTarget,\n remote: RemoteTarget | undefined,\n storybookUrl: string | undefined,\n): void {\n if (remote?.reach === \"url\" && storybookUrl === undefined) {\n throw new TargetLaunchError(\n target,\n \"its browser cannot reach this host, so it needs a Storybook URL (a public origin and a secret to sign it) and none was given\",\n { transient: true },\n );\n }\n if (storybookUrl !== undefined) {\n assertStorybookRoot(target, storybookUrl);\n }\n}\n\n/** The URL must be the build's directory root; refused before any browser connects. */\nfunction assertStorybookRoot(target: CaptureTarget, storybookUrl: string): void {\n try {\n storybookBase(storybookUrl);\n } catch (error) {\n throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));\n }\n}\n\n/** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */\nexport async function serve(\n input: Pick<RunInput, \"storybookDir\" | \"storybookUrl\" | \"staticCompression\">,\n browser: Browser,\n): Promise<Serving> {\n if (input.storybookUrl !== undefined) {\n return {\n url: storybookBase(input.storybookUrl),\n close: async () => {\n await Promise.resolve();\n },\n };\n }\n return await createStaticServer(input.storybookDir, {\n compression: input.staticCompression,\n }).catch(async (error: unknown) => {\n await safeCloseBrowser(browser, \"failed\", error);\n throw error;\n });\n}\n\nasync function readParameters(\n browser: Browser,\n baseUrl: string,\n storyId: string,\n custom: StorySourceAdapter | undefined,\n): Promise<Record<string, StoryParameters>> {\n const adapter: StorySourceAdapter = custom ?? new StorybookAdapter();\n const page = await browser.newPage();\n await page.goto(adapter.buildUrl(baseUrl, storyId), { waitUntil: \"networkidle\" });\n await adapter.waitForReady?.(page);\n return await extractAllParameters(page as unknown as RuntimeParamsPage);\n}\n\n/** The driver's browser version; undefined for a driver (or test double) that does not report one. */\nfunction browserVersionOf(browser: Browser): string | undefined {\n return typeof browser.version === \"function\" ? browser.version() : undefined;\n}\n\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function localEnvironment(browser: Browser): Promise<CaptureEnvironment> {\n return await detectCaptureEnvironment(browserVersionOf(browser));\n}\n\nasync function runWithBrowser(\n input: RunInput,\n active: ActiveRun,\n browser: Browser,\n server: Serving,\n relaunch: () => Promise<Browser>,\n): Promise<RenderResult> {\n active.browser = browser;\n const ctx = buildContext(browser, server.url, input);\n const captures: RenderedSnapshot[] = [];\n const failures: RenderResult[\"failures\"] = [];\n return await finishing(\n async () => {\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n },\n async (failed, error) => {\n active.browser = null;\n await ctx.pagePool?.dispose();\n // ctx.browser is the latest one: recycling replaces the browser mid-run.\n await Promise.all([\n safeCloseBrowser(ctx.browser, outcomeOf(active, failed), error),\n safeCloseServer(server),\n ]);\n },\n );\n}\n\nfunction buildContext(browser: Browser, baseUrl: string, input: RunInput): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n navigationTimeoutMs: input.navigationTimeoutMs,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n ...(input.logger ? { logger: input.logger } : {}),\n runtimeParams: createRuntimeParamsState(),\n ...(input.reusePages ? { pagePool: new PagePool(browser) } : {}),\n };\n}\n\nasync function runAllCaptures(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n relaunch: () => Promise<Browser>,\n): Promise<void> {\n // Bounded: one page per story x viewport all at once exhausts memory on large Storybooks.\n const jobs = input.stories.flatMap((story) =>\n resolveStoryViewports(story, input.viewports).map((viewport) => ({ story, viewport })),\n );\n // Batches drain in-flight pages, so the browser can be swapped between them.\n const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);\n const pages = createPageLimiter({\n concurrency: connectionConcurrency(input),\n ...(input.gate === undefined ? {} : { gate: input.gate }),\n browser: engineOf(input.target ?? \"chromium\"),\n logger: input.logger,\n });\n try {\n await runBatches(batches, pages, input, active, ctx, { captures, failures, relaunch });\n } finally {\n pages.dispose();\n }\n}\n\nasync function runBatches(\n batches: { story: StoryEntry; viewport: Viewport }[][],\n pages: ReturnType<typeof createPageLimiter>,\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n out: {\n captures: RenderedSnapshot[];\n failures: RenderResult[\"failures\"];\n relaunch: () => Promise<Browser>;\n },\n): Promise<void> {\n for (const [index, batch] of batches.entries()) {\n if (index > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- recycle only between drained batches\n await recycleBrowser(active, ctx, out.relaunch, input.logger);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- batches run one after another\n await pages.each(\n batch,\n async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out),\n );\n }\n}\n\n/** Swap in a fresh browser; the old one is closed first so two never coexist. */\nasync function recycleBrowser(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<Browser>,\n logger: PlaywrightRenderInput[\"logger\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n await ctx.pagePool?.dispose();\n await safeCloseBrowser(ctx.browser);\n // A cancel that arrived while the old browser closed must not start a new one.\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n ctx.browser = await relaunch();\n if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);\n active.browser = ctx.browser;\n logger?.info({}, \"browser recycled\");\n}\n\n/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */\nasync function captureOne(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n out: { captures: RenderedSnapshot[]; failures: RenderResult[\"failures\"] },\n): Promise<boolean> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, out.captures, out.failures, input, active);\n return false;\n } catch (error) {\n pushRenderFailure(out.failures, input, story.id, viewport.name, error);\n return isTimeoutError(error instanceof TransientCaptureError ? error.cause : error);\n }\n}\n\nasync function pushCapture(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n input: RunInput,\n active: ActiveRun,\n): Promise<void> {\n const started = performance.now();\n const result = await screenshotWithRetry(ctx, story, viewport, input, active);\n const snapshot: RenderedSnapshot = {\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n durationMs: Math.round(performance.now() - started),\n ...(result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}),\n };\n // Streaming: hand it over and drop the buffer rather than holding every screenshot.\n if (input.onCapture) await input.onCapture(snapshot);\n else captures.push(snapshot);\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\n}\n\n/** Retry capture only: delivery and accessibility reporting must never be repeated. */\nexport async function screenshotWithRetry(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n): ReturnType<typeof captureScreenshot> {\n try {\n const result = await captureScreenshot(ctx, story, viewport);\n if (active.cancelled || active.abort?.signal.aborted) throw new Error(\"Capture cancelled\");\n return result;\n } catch (error) {\n if (\n !input.retryRemoteCapture ||\n !(error instanceof TransientCaptureError) ||\n active.cancelled ||\n active.abort?.signal.aborted ||\n !ctx.browser.isConnected()\n )\n throw error;\n return await freshScreenshot(ctx, story, viewport, input, active, error);\n }\n}\n\nasync function freshScreenshot(\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n input: RunInput,\n active: ActiveRun,\n firstError: TransientCaptureError,\n): ReturnType<typeof captureScreenshot> {\n const detail = {\n storyId: story.id,\n viewport: viewport.name,\n phase: firstError.phase,\n attempt: 2,\n };\n input.logger?.warn(detail, \"retrying remote capture on a fresh page\");\n try {\n const result = await captureScreenshot(ctx, story, viewport, true);\n if (active.cancelled || active.abort?.signal.aborted)\n throw new Error(\"Capture cancelled\", { cause: firstError });\n input.logger?.info(detail, \"remote capture retry succeeded\");\n return result;\n } catch (retryError) {\n input.logger?.warn(detail, \"remote capture retry failed\");\n throw retryError;\n }\n}\nfunction handleA11y(\n input: PlaywrightRenderInput,\n failures: RenderResult[\"failures\"],\n story: StoryEntry,\n viewport: Viewport,\n violations: string[],\n): void {\n failures.push({\n storyId: story.id,\n viewportName: viewport.name,\n error: `a11y: ${violations.join(\"; \")}`,\n });\n input.logger?.warn(\n { storyId: story.id, viewport: viewport.name, violations },\n \"a11y violations found\",\n );\n}\n\nfunction pushRenderFailure(\n failures: RenderResult[\"failures\"],\n input: PlaywrightRenderInput,\n storyId: string,\n viewportName: string,\n error: unknown,\n): void {\n failures.push({ storyId, viewportName, error: messageOf(error) });\n input.logger?.error({ storyId, viewport: viewportName, err: error }, \"render failed for story\");\n}\n"],"mappings":"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"}
package/dist/play.mjs CHANGED
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { messageOf } from "./browser.mjs";
5
- //#region src/play.ts
5
+ // src/play.ts
6
6
  /** Execute the story's play function with a timeout race. */
7
7
  async function runPlay(page, storyId, timeoutMs) {
8
8
  try {
@@ -57,7 +57,6 @@ async function inPagePlay({ storyId, timeoutMs }) {
57
57
  }
58
58
  if (playError) throw playError;
59
59
  }
60
- //#endregion
61
60
  export { inPagePlay, runPlay };
62
61
 
63
62
  //# sourceMappingURL=play.mjs.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
4
- //#region src/remote/azure.d.ts
5
4
  /** Options of {@link azurePlaywright}. */
6
5
  interface AzurePlaywrightOptions {
7
6
  /** The target name half; default `azure`. */
@@ -21,6 +20,5 @@ interface AzurePlaywrightOptions {
21
20
  }
22
21
  /** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */
23
22
  declare function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser;
24
- //#endregion
25
23
  export { AzurePlaywrightOptions, azurePlaywright };
26
24
  //# sourceMappingURL=azure.d.mts.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { remote } from "./remote.mjs";
5
- //#region src/remote/azure.ts
5
+ // src/remote/azure.ts
6
6
  /** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */
7
7
  function azurePlaywright(options) {
8
8
  const os = options.os ?? "linux";
@@ -13,15 +13,15 @@ function azurePlaywright(options) {
13
13
  "firefox",
14
14
  "webkit"
15
15
  ],
16
- connect: () => ({
17
- protocol: "playwright",
16
+ protocol: "playwright",
17
+ reach: "loopback",
18
+ connect: {
18
19
  endpoint: withParams(options.endpoint, {
19
20
  os,
20
21
  "api-version": options.apiVersion ?? "2025-09-01"
21
22
  }),
22
- headers: { authorization: `Bearer ${options.token}` },
23
- exposeNetwork: "<loopback>"
24
- }),
23
+ headers: { authorization: `Bearer ${options.token}` }
24
+ },
25
25
  sessions: options.sessions ?? 1,
26
26
  platform: os
27
27
  });
@@ -31,7 +31,6 @@ function withParams(endpoint, params) {
31
31
  for (const [key, value] of Object.entries(params)) if (!url.searchParams.has(key)) url.searchParams.set(key, value);
32
32
  return url.toString();
33
33
  }
34
- //#endregion
35
34
  export { azurePlaywright };
36
35
 
37
36
  //# sourceMappingURL=azure.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"azure.mjs","names":[],"sources":["../../src/remote/azure.ts"],"sourcesContent":["/** Microsoft Azure Playwright Workspaces (Azure App Testing). */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link azurePlaywright}. */\nexport interface AzurePlaywrightOptions {\n /** The target name half; default `azure`. */\n name?: string;\n /** The workspace's browser endpoint (`PLAYWRIGHT_SERVICE_URL`): `wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<id>/browsers`. */\n endpoint: string;\n /** Access token (`PLAYWRIGHT_SERVICE_ACCESS_TOKEN`). */\n token: string;\n /** OS of the cloud browsers (default `linux`). */\n os?: \"linux\" | \"windows\";\n /** API version the service is called with (default `2025-09-01`). */\n apiVersion?: string;\n /** Parallel pages (the workspace's parallel-worker limit; default 1). */\n sessions?: number;\n /** Engines to offer (default all three). */\n engines?: readonly BrowserName[];\n}\n\n/** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */\nexport function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser {\n const os = options.os ?? \"linux\";\n return remote({\n name: options.name ?? \"azure\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n connect: () => ({\n protocol: \"playwright\",\n endpoint: withParams(options.endpoint, {\n os,\n \"api-version\": options.apiVersion ?? \"2025-09-01\",\n }),\n headers: { authorization: `Bearer ${options.token}` },\n // The service routes requests for the host's loopback back through the connection.\n exposeNetwork: \"<loopback>\",\n }),\n sessions: options.sessions ?? 1,\n platform: os,\n });\n}\n\nfunction withParams(endpoint: string, params: Record<string, string>): string {\n const url = new URL(endpoint);\n for (const [key, value] of Object.entries(params)) {\n if (!url.searchParams.has(key)) url.searchParams.set(key, value);\n }\n return url.toString();\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,gBAAgB,SAAgD;CAC9E,MAAM,KAAK,QAAQ,MAAM;CACzB,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,gBAAgB;GACd,UAAU;GACV,UAAU,WAAW,QAAQ,UAAU;IACrC;IACA,eAAe,QAAQ,cAAc;GACvC,CAAC;GACD,SAAS,EAAE,eAAe,UAAU,QAAQ,QAAQ;GAEpD,eAAe;EACjB;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU;CACZ,CAAC;AACH;AAEA,SAAS,WAAW,UAAkB,QAAwC;CAC5E,MAAM,MAAM,IAAI,IAAI,QAAQ;CAC5B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG,IAAI,aAAa,IAAI,KAAK,KAAK;CAEjE,OAAO,IAAI,SAAS;AACtB"}
1
+ {"version":3,"file":"azure.mjs","names":[],"sources":["../../src/remote/azure.ts"],"sourcesContent":["/** Microsoft Azure Playwright Workspaces (Azure App Testing). */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link azurePlaywright}. */\nexport interface AzurePlaywrightOptions {\n /** The target name half; default `azure`. */\n name?: string;\n /** The workspace's browser endpoint (`PLAYWRIGHT_SERVICE_URL`): `wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<id>/browsers`. */\n endpoint: string;\n /** Access token (`PLAYWRIGHT_SERVICE_ACCESS_TOKEN`). */\n token: string;\n /** OS of the cloud browsers (default `linux`). */\n os?: \"linux\" | \"windows\";\n /** API version the service is called with (default `2025-09-01`). */\n apiVersion?: string;\n /** Parallel pages (the workspace's parallel-worker limit; default 1). */\n sessions?: number;\n /** Engines to offer (default all three). */\n engines?: readonly BrowserName[];\n}\n\n/** Targets on a workspace: `chromium@azure`, `firefox@azure`, `webkit@azure`. */\nexport function azurePlaywright(options: AzurePlaywrightOptions): RemoteBrowser {\n const os = options.os ?? \"linux\";\n return remote({\n name: options.name ?? \"azure\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n // The service routes requests for the host's loopback back through the connection.\n reach: \"loopback\",\n connect: {\n endpoint: withParams(options.endpoint, {\n os,\n \"api-version\": options.apiVersion ?? \"2025-09-01\",\n }),\n headers: { authorization: `Bearer ${options.token}` },\n },\n sessions: options.sessions ?? 1,\n platform: os,\n });\n}\n\nfunction withParams(endpoint: string, params: Record<string, string>): string {\n const url = new URL(endpoint);\n for (const [key, value] of Object.entries(params)) {\n if (!url.searchParams.has(key)) url.searchParams.set(key, value);\n }\n return url.toString();\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,gBAAgB,SAAgD;CAC9E,MAAM,KAAK,QAAQ,MAAM;CACzB,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU;EAEV,OAAO;EACP,SAAS;GACP,UAAU,WAAW,QAAQ,UAAU;IACrC;IACA,eAAe,QAAQ,cAAc;GACvC,CAAC;GACD,SAAS,EAAE,eAAe,UAAU,QAAQ,QAAQ;EACtD;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU;CACZ,CAAC;AACH;AAEA,SAAS,WAAW,UAAkB,QAAwC;CAC5E,MAAM,MAAM,IAAI,IAAI,QAAQ;CAC5B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG,IAAI,aAAa,IAAI,KAAK,KAAK;CAEjE,OAAO,IAAI,SAAS;AACtB"}
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
4
- //#region src/remote/browserstack.d.ts
5
4
  /** Options of {@link browserstack}. */
6
5
  interface BrowserStackOptions {
7
6
  /** The target name half; default `browserstack`. A second entry needs its own. */
@@ -28,6 +27,5 @@ interface BrowserStackOptions {
28
27
  * travel in the endpoint's capabilities and are scrubbed from every message and log.
29
28
  */
30
29
  declare function browserstack(options: BrowserStackOptions): RemoteBrowser;
31
- //#endregion
32
30
  export { BrowserStackOptions, browserstack };
33
31
  //# sourceMappingURL=browserstack.d.mts.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { playwrightVersion } from "../version.mjs";
5
5
  import { remote } from "./remote.mjs";
6
- //#region src/remote/browserstack.ts
6
+ // src/remote/browserstack.ts
7
7
  /** BrowserStack's `browser` capability per engine. */
8
8
  const BROWSER = {
9
9
  chromium: "playwright-chromium",
@@ -24,10 +24,9 @@ function browserstack(options) {
24
24
  "firefox",
25
25
  "webkit"
26
26
  ],
27
- connect: (engine) => ({
28
- protocol: "playwright",
29
- endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(capabilities(options, engine)))}`
30
- }),
27
+ protocol: "playwright",
28
+ reach: "url",
29
+ connect: ({ engine }) => ({ endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(capabilities(options, engine)))}` }),
31
30
  sessions: options.sessions ?? 1,
32
31
  platform: options.os?.name.toLowerCase().replace(/^os x$/u, "macos")
33
32
  });
@@ -46,7 +45,6 @@ function capabilities(options, engine) {
46
45
  ...options.build === void 0 ? {} : { build: options.build }
47
46
  };
48
47
  }
49
- //#endregion
50
48
  export { browserstack };
51
49
 
52
50
  //# sourceMappingURL=browserstack.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"browserstack.mjs","names":[],"sources":["../../src/remote/browserstack.ts"],"sourcesContent":["/** BrowserStack Automate over Playwright's protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { playwrightVersion } from \"../version.ts\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link browserstack}. */\nexport interface BrowserStackOptions {\n /** The target name half; default `browserstack`. A second entry needs its own. */\n name?: string;\n username: string;\n accessKey: string;\n /** OS the browsers run on, as BrowserStack names it (`{ name: \"Windows\", version: \"11\" }`). */\n os?: { name: string; version: string };\n /** Browser version (default `latest`). Pin it: a moving build is a baseline migration. */\n browserVersion?: string;\n /** Engines to offer (default `chromium`, `firefox`, `webkit`; `chrome` is real Chrome). */\n engines?: readonly BrowserName[];\n /** Parallel sessions of the plan (default 1). */\n sessions?: number;\n /** Build name shown in BrowserStack's dashboard. */\n build?: string;\n}\n\n/** BrowserStack's `browser` capability per engine. */\nconst BROWSER: Record<BrowserName, string> = {\n chromium: \"playwright-chromium\",\n firefox: \"playwright-firefox\",\n webkit: \"playwright-webkit\",\n chrome: \"chrome\",\n};\n\n/**\n * Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to\n * this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials\n * travel in the endpoint's capabilities and are scrubbed from every message and log.\n */\nexport function browserstack(options: BrowserStackOptions): RemoteBrowser {\n return remote({\n name: options.name ?? \"browserstack\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n connect: (engine) => ({\n protocol: \"playwright\",\n endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(\n JSON.stringify(capabilities(options, engine)),\n )}`,\n }),\n sessions: options.sessions ?? 1,\n platform: options.os?.name.toLowerCase().replace(/^os x$/u, \"macos\"),\n });\n}\n\nfunction capabilities(options: BrowserStackOptions, engine: BrowserName): Record<string, unknown> {\n return {\n browser: BROWSER[engine],\n browser_version: options.browserVersion ?? \"latest\",\n ...(options.os === undefined ? {} : { os: options.os.name, os_version: options.os.version }),\n \"browserstack.username\": options.username,\n \"browserstack.accessKey\": options.accessKey,\n \"client.playwrightVersion\": playwrightVersion(),\n ...(options.build === undefined ? {} : { build: options.build }),\n };\n}\n"],"mappings":";;;;;;;AAwBA,MAAM,UAAuC;CAC3C,UAAU;CACV,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;;;;;;AAOA,SAAgB,aAAa,SAA6C;CACxE,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU,YAAY;GACpB,UAAU;GACV,UAAU,8CAA8C,mBACtD,KAAK,UAAU,aAAa,SAAS,MAAM,CAAC,CAC9C;EACF;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,WAAW,OAAO;CACrE,CAAC;AACH;AAEA,SAAS,aAAa,SAA8B,QAA8C;CAChG,OAAO;EACL,SAAS,QAAQ;EACjB,iBAAiB,QAAQ,kBAAkB;EAC3C,GAAI,QAAQ,OAAO,KAAA,IAAY,CAAC,IAAI;GAAE,IAAI,QAAQ,GAAG;GAAM,YAAY,QAAQ,GAAG;EAAQ;EAC1F,yBAAyB,QAAQ;EACjC,0BAA0B,QAAQ;EAClC,4BAA4B,kBAAkB;EAC9C,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAChE;AACF"}
1
+ {"version":3,"file":"browserstack.mjs","names":[],"sources":["../../src/remote/browserstack.ts"],"sourcesContent":["/** BrowserStack Automate over Playwright's protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { playwrightVersion } from \"../version.ts\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link browserstack}. */\nexport interface BrowserStackOptions {\n /** The target name half; default `browserstack`. A second entry needs its own. */\n name?: string;\n username: string;\n accessKey: string;\n /** OS the browsers run on, as BrowserStack names it (`{ name: \"Windows\", version: \"11\" }`). */\n os?: { name: string; version: string };\n /** Browser version (default `latest`). Pin it: a moving build is a baseline migration. */\n browserVersion?: string;\n /** Engines to offer (default `chromium`, `firefox`, `webkit`; `chrome` is real Chrome). */\n engines?: readonly BrowserName[];\n /** Parallel sessions of the plan (default 1). */\n sessions?: number;\n /** Build name shown in BrowserStack's dashboard. */\n build?: string;\n}\n\n/** BrowserStack's `browser` capability per engine. */\nconst BROWSER: Record<BrowserName, string> = {\n chromium: \"playwright-chromium\",\n firefox: \"playwright-firefox\",\n webkit: \"playwright-webkit\",\n chrome: \"chrome\",\n};\n\n/**\n * Targets on BrowserStack, for example `chromium@browserstack`. BrowserStack cannot route back to\n * this host, so each target needs a signed Storybook URL (ADR 0007, decision 5). The credentials\n * travel in the endpoint's capabilities and are scrubbed from every message and log.\n */\nexport function browserstack(options: BrowserStackOptions): RemoteBrowser {\n return remote({\n name: options.name ?? \"browserstack\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n reach: \"url\",\n connect: ({ engine }) => ({\n endpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(\n JSON.stringify(capabilities(options, engine)),\n )}`,\n }),\n sessions: options.sessions ?? 1,\n platform: options.os?.name.toLowerCase().replace(/^os x$/u, \"macos\"),\n });\n}\n\nfunction capabilities(options: BrowserStackOptions, engine: BrowserName): Record<string, unknown> {\n return {\n browser: BROWSER[engine],\n browser_version: options.browserVersion ?? \"latest\",\n ...(options.os === undefined ? {} : { os: options.os.name, os_version: options.os.version }),\n \"browserstack.username\": options.username,\n \"browserstack.accessKey\": options.accessKey,\n \"client.playwrightVersion\": playwrightVersion(),\n ...(options.build === undefined ? {} : { build: options.build }),\n };\n}\n"],"mappings":";;;;;;;AAwBA,MAAM,UAAuC;CAC3C,UAAU;CACV,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;;;;;;AAOA,SAAgB,aAAa,SAA6C;CACxE,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU;EACV,OAAO;EACP,UAAU,EAAE,cAAc,EACxB,UAAU,8CAA8C,mBACtD,KAAK,UAAU,aAAa,SAAS,MAAM,CAAC,CAC9C,IACF;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,WAAW,OAAO;CACrE,CAAC;AACH;AAEA,SAAS,aAAa,SAA8B,QAA8C;CAChG,OAAO;EACL,SAAS,QAAQ;EACjB,iBAAiB,QAAQ,kBAAkB;EAC3C,GAAI,QAAQ,OAAO,KAAA,IAAY,CAAC,IAAI;GAAE,IAAI,QAAQ,GAAG;GAAM,YAAY,QAAQ,GAAG;EAAQ;EAC1F,yBAAyB,QAAQ;EACjC,0BAA0B,QAAQ;EAClC,4BAA4B,kBAAkB;EAC9C,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;CAChE;AACF"}
@@ -4,4 +4,5 @@ import { BrowserName, RemoteBrowser, RemoteConnection, RemoteOptions, remote } f
4
4
  import { PlaywrightServerOptions, playwrightServer } from "./playwright-server.mjs";
5
5
  import { AzurePlaywrightOptions, azurePlaywright } from "./azure.mjs";
6
6
  import { BrowserStackOptions, browserstack } from "./browserstack.mjs";
7
- export { type AzurePlaywrightOptions, type BrowserName, type BrowserStackOptions, type PlaywrightServerOptions, type RemoteBrowser, type RemoteConnection, type RemoteOptions, azurePlaywright, browserstack, playwrightServer, remote };
7
+ import { PoolLease, RefCounted, RefCountedOptions, RemoteConnectContext, RemoteConnectError, RemoteReleaseInfo, refCounted } from "@storyshelf/core/adapter/capture-runner";
8
+ export { type AzurePlaywrightOptions, type BrowserName, type BrowserStackOptions, type PlaywrightServerOptions, type PoolLease, type RefCounted, type RefCountedOptions, type RemoteBrowser, type RemoteConnectContext, RemoteConnectError, type RemoteConnection, type RemoteOptions, type RemoteReleaseInfo, azurePlaywright, browserstack, playwrightServer, refCounted, remote };
@@ -5,4 +5,5 @@ import { remote } from "./remote.mjs";
5
5
  import { playwrightServer } from "./playwright-server.mjs";
6
6
  import { azurePlaywright } from "./azure.mjs";
7
7
  import { browserstack } from "./browserstack.mjs";
8
- export { azurePlaywright, browserstack, playwrightServer, remote };
8
+ import { RemoteConnectError, refCounted } from "@storyshelf/core/adapter/capture-runner";
9
+ export { RemoteConnectError, azurePlaywright, browserstack, playwrightServer, refCounted, remote };
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
4
- //#region src/remote/playwright-server.d.ts
5
4
  /** Options of {@link playwrightServer}. */
6
5
  interface PlaywrightServerOptions {
7
6
  /** The target name half (`chromium@<name>`); default `grid`. */
@@ -17,12 +16,19 @@ interface PlaywrightServerOptions {
17
16
  headers?: Record<string, string>;
18
17
  /**
19
18
  * Route the remote browser's requests for the loopback Storybook server back through the
20
- * connection (default `"<loopback>"`). `false` when the server cannot, which makes every target
21
- * need a signed Storybook URL.
19
+ * connection (default `"<loopback>"`, the only pattern supported). `false` when the server
20
+ * cannot, which makes every target need a signed Storybook URL.
22
21
  */
23
- exposeNetwork?: string | false;
22
+ exposeNetwork?: "<loopback>" | false;
24
23
  /** Parallel pages the farm should take from this runner (default 1: set it to the farm's cap). */
25
24
  sessions?: number;
25
+ /** Maximum concurrent pages on each connection; the shared sessions limit still applies. */
26
+ maxPagesPerConnection?: number;
27
+ /**
28
+ * Maximum connections this process holds to the server at once (default: no cap), separate from
29
+ * the page limits above. Renders over the cap wait; per process, not per deployment.
30
+ */
31
+ maxConnections?: number;
26
32
  /** Engines to offer (default all three; the server must have them installed). */
27
33
  engines?: readonly BrowserName[];
28
34
  /** OS the server runs on, reported when the page cannot tell. */
@@ -35,6 +41,5 @@ interface PlaywrightServerOptions {
35
41
  }
36
42
  /** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */
37
43
  declare function playwrightServer(options: PlaywrightServerOptions): RemoteBrowser;
38
- //#endregion
39
44
  export { PlaywrightServerOptions, playwrightServer };
40
45
  //# sourceMappingURL=playwright-server.d.mts.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { remote } from "./remote.mjs";
5
- //#region src/remote/playwright-server.ts
5
+ // src/remote/playwright-server.ts
6
6
  /** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */
7
7
  function playwrightServer(options) {
8
8
  const headers = {
@@ -17,18 +17,19 @@ function playwrightServer(options) {
17
17
  "firefox",
18
18
  "webkit"
19
19
  ],
20
- connect: () => ({
21
- protocol: "playwright",
20
+ protocol: "playwright",
21
+ reach: exposeNetwork === false ? "url" : "loopback",
22
+ connect: {
22
23
  endpoint: options.endpoint,
23
- headers,
24
- ...exposeNetwork === false ? {} : { exposeNetwork }
25
- }),
24
+ headers
25
+ },
26
26
  sessions: options.sessions ?? 1,
27
+ maxPagesPerConnection: options.maxPagesPerConnection,
28
+ maxConnections: options.maxConnections,
27
29
  platform: options.platform,
28
- connectTimeoutMs: options.connectTimeoutMs
30
+ timeouts: { connectMs: options.connectTimeoutMs }
29
31
  });
30
32
  }
31
- //#endregion
32
33
  export { playwrightServer };
33
34
 
34
35
  //# sourceMappingURL=playwright-server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-server.mjs","names":[],"sources":["../../src/remote/playwright-server.ts"],"sourcesContent":["/** A self-hosted Playwright server (`playwright run-server`), or anything speaking its protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link playwrightServer}. */\nexport interface PlaywrightServerOptions {\n /** The target name half (`chromium@<name>`); default `grid`. */\n name?: string;\n /** `ws://host:3000/` or `wss://…`: the server's address and `--path`. */\n endpoint: string;\n /**\n * Bearer token for a reverse proxy in front of the server: a bare `run-server` has no\n * authentication of its own. Sent as `authorization: Bearer …`.\n */\n token?: string;\n /** Further handshake headers. */\n headers?: Record<string, string>;\n /**\n * Route the remote browser's requests for the loopback Storybook server back through the\n * connection (default `\"<loopback>\"`). `false` when the server cannot, which makes every target\n * need a signed Storybook URL.\n */\n exposeNetwork?: string | false;\n /** Parallel pages the farm should take from this runner (default 1: set it to the farm's cap). */\n sessions?: number;\n /** Engines to offer (default all three; the server must have them installed). */\n engines?: readonly BrowserName[];\n /** OS the server runs on, reported when the page cannot tell. */\n platform?: string;\n /**\n * How long one connect attempt waits in ms (default 30 000). A server started with\n * `--max-clients` queues extra connections: raise this when renders outnumber its clients.\n */\n connectTimeoutMs?: number;\n}\n\n/** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */\nexport function playwrightServer(options: PlaywrightServerOptions): RemoteBrowser {\n const headers = {\n ...(options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }),\n ...options.headers,\n };\n const exposeNetwork = options.exposeNetwork ?? \"<loopback>\";\n return remote({\n name: options.name ?? \"grid\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n connect: () => ({\n protocol: \"playwright\",\n endpoint: options.endpoint,\n headers,\n ...(exposeNetwork === false ? {} : { exposeNetwork }),\n }),\n sessions: options.sessions ?? 1,\n platform: options.platform,\n connectTimeoutMs: options.connectTimeoutMs,\n });\n}\n"],"mappings":";;;;;;AAqCA,SAAgB,iBAAiB,SAAiD;CAChF,MAAM,UAAU;EACd,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe,UAAU,QAAQ,QAAQ;EAClF,GAAG,QAAQ;CACb;CACA,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,gBAAgB;GACd,UAAU;GACV,UAAU,QAAQ;GAClB;GACA,GAAI,kBAAkB,QAAQ,CAAC,IAAI,EAAE,cAAc;EACrD;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ;EAClB,kBAAkB,QAAQ;CAC5B,CAAC;AACH"}
1
+ {"version":3,"file":"playwright-server.mjs","names":[],"sources":["../../src/remote/playwright-server.ts"],"sourcesContent":["/** A self-hosted Playwright server (`playwright run-server`), or anything speaking its protocol. */\nimport type { BrowserName, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport { remote } from \"./remote.ts\";\n\n/** Options of {@link playwrightServer}. */\nexport interface PlaywrightServerOptions {\n /** The target name half (`chromium@<name>`); default `grid`. */\n name?: string;\n /** `ws://host:3000/` or `wss://…`: the server's address and `--path`. */\n endpoint: string;\n /**\n * Bearer token for a reverse proxy in front of the server: a bare `run-server` has no\n * authentication of its own. Sent as `authorization: Bearer …`.\n */\n token?: string;\n /** Further handshake headers. */\n headers?: Record<string, string>;\n /**\n * Route the remote browser's requests for the loopback Storybook server back through the\n * connection (default `\"<loopback>\"`, the only pattern supported). `false` when the server\n * cannot, which makes every target need a signed Storybook URL.\n */\n exposeNetwork?: \"<loopback>\" | false;\n /** Parallel pages the farm should take from this runner (default 1: set it to the farm's cap). */\n sessions?: number;\n /** Maximum concurrent pages on each connection; the shared sessions limit still applies. */\n maxPagesPerConnection?: number;\n /**\n * Maximum connections this process holds to the server at once (default: no cap), separate from\n * the page limits above. Renders over the cap wait; per process, not per deployment.\n */\n maxConnections?: number;\n /** Engines to offer (default all three; the server must have them installed). */\n engines?: readonly BrowserName[];\n /** OS the server runs on, reported when the page cannot tell. */\n platform?: string;\n /**\n * How long one connect attempt waits in ms (default 30 000). A server started with\n * `--max-clients` queues extra connections: raise this when renders outnumber its clients.\n */\n connectTimeoutMs?: number;\n}\n\n/** Targets on a Playwright server: `chromium@grid`, `firefox@grid`, `webkit@grid`. */\nexport function playwrightServer(options: PlaywrightServerOptions): RemoteBrowser {\n const headers = {\n ...(options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }),\n ...options.headers,\n };\n const exposeNetwork = options.exposeNetwork ?? \"<loopback>\";\n return remote({\n name: options.name ?? \"grid\",\n engines: options.engines ?? [\"chromium\", \"firefox\", \"webkit\"],\n protocol: \"playwright\",\n reach: exposeNetwork === false ? \"url\" : \"loopback\",\n connect: { endpoint: options.endpoint, headers },\n sessions: options.sessions ?? 1,\n maxPagesPerConnection: options.maxPagesPerConnection,\n maxConnections: options.maxConnections,\n platform: options.platform,\n timeouts: { connectMs: options.connectTimeoutMs },\n });\n}\n"],"mappings":";;;;;;AA4CA,SAAgB,iBAAiB,SAAiD;CAChF,MAAM,UAAU;EACd,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe,UAAU,QAAQ,QAAQ;EAClF,GAAG,QAAQ;CACb;CACA,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,OAAO,OAAO;EACZ,MAAM,QAAQ,QAAQ;EACtB,SAAS,QAAQ,WAAW;GAAC;GAAY;GAAW;EAAQ;EAC5D,UAAU;EACV,OAAO,kBAAkB,QAAQ,QAAQ;EACzC,SAAS;GAAE,UAAU,QAAQ;GAAU;EAAQ;EAC/C,UAAU,QAAQ,YAAY;EAC9B,uBAAuB,QAAQ;EAC/B,gBAAgB,QAAQ;EACxB,UAAU,QAAQ;EAClB,UAAU,EAAE,WAAW,QAAQ,iBAAiB;CAClD,CAAC;AACH"}
@@ -1,22 +1,38 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BrowserName as BrowserName$1, RemoteBrowser as RemoteBrowser$1, RemoteConnection, RemoteOptions } from "@storyshelf/core/adapter/capture-runner";
4
- //#region src/remote/remote.d.ts
5
4
  /**
6
5
  * A browser service the runner connects to instead of launching, for `browsers`. It offers one
7
6
  * target per engine, `<engine>@<name>`, each with its own baselines (ADR 0007, decision 3), and
8
7
  * throws when the runner is created for a bad name, engine, protocol or endpoint.
9
8
  *
10
9
  * ```ts
10
+ * // A fixed service:
11
11
  * remote({
12
12
  * name: "mygrid",
13
13
  * engines: ["chromium", "firefox", "webkit"],
14
- * connect: () => ({ protocol: "playwright", endpoint: "wss://grid.example/", exposeNetwork: "<loopback>" }),
14
+ * protocol: "playwright",
15
+ * reach: "loopback", // its browser can route back to this host
16
+ * connect: { endpoint: "wss://grid.example/" },
15
17
  * sessions: 4,
18
+ * maxPagesPerConnection: 2,
19
+ * });
20
+ *
21
+ * // A service that must be woken first, with a lease handed back to `release`:
22
+ * remote<{ id: string }>({
23
+ * name: "pool",
24
+ * engines: ["chromium"],
25
+ * protocol: "playwright",
26
+ * reach: "loopback",
27
+ * sessions: 4,
28
+ * connect: async ({ shardIndex, signal }) => {
29
+ * const server = await wake(shardIndex, signal);
30
+ * return { endpoint: server.endpoint, lease: { id: server.id } };
31
+ * },
32
+ * release: async (lease, { outcome }) => await suspend(lease.id, outcome),
16
33
  * });
17
34
  * ```
18
35
  */
19
- declare function remote(options: RemoteOptions): RemoteBrowser$1;
20
- //#endregion
36
+ declare function remote<TLease = unknown>(options: RemoteOptions<TLease>): RemoteBrowser$1;
21
37
  export { type BrowserName$1 as BrowserName, type RemoteBrowser$1 as RemoteBrowser, type RemoteConnection, type RemoteOptions, remote };
22
38
  //# sourceMappingURL=remote.d.mts.map