@storyshelf/runner-puppeteer 0.6.2 → 0.7.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/pages.mjs ADDED
@@ -0,0 +1,89 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { installSettleRecorder } from "./settle.mjs";
5
+ //#region src/pages.ts
6
+ /** Lease a page for `viewport`: pooled when the context has a pool, otherwise a throwaway. */
7
+ async function acquirePage(ctx, viewport) {
8
+ if (ctx.pagePool) return await ctx.pagePool.acquire(viewport);
9
+ const page = await ctx.browser.newPage();
10
+ await page.setViewport({
11
+ width: viewport.width,
12
+ height: viewport.height
13
+ });
14
+ await installSettleRecorder(page);
15
+ return {
16
+ page,
17
+ release: async () => {
18
+ await page.close();
19
+ }
20
+ };
21
+ }
22
+ /**
23
+ * Reuses pages (each in its own browser context) across captures to skip
24
+ * per-capture startup and keep Storybook's JS in the HTTP cache. A page is
25
+ * reset between captures (storage and cookies cleared, blank document) and
26
+ * discarded, never reused, after a failed capture.
27
+ */
28
+ var PagePool = class {
29
+ free = [];
30
+ browser;
31
+ constructor(browser) {
32
+ this.browser = browser;
33
+ }
34
+ async acquire(viewport) {
35
+ const entry = this.free.pop() ?? await this.open();
36
+ await entry.page.setViewport({
37
+ width: viewport.width,
38
+ height: viewport.height
39
+ });
40
+ return {
41
+ page: entry.page,
42
+ release: async (reusable) => {
43
+ if (reusable && await reset(entry)) this.free.push(entry);
44
+ else await closeEntry(entry);
45
+ }
46
+ };
47
+ }
48
+ /** Close every idle page; call before the browser is replaced or closed. */
49
+ async dispose() {
50
+ await Promise.all(this.free.splice(0).map(async (entry) => {
51
+ await closeEntry(entry);
52
+ }));
53
+ }
54
+ async open() {
55
+ const context = await this.browser.createBrowserContext();
56
+ const page = await context.newPage();
57
+ await installSettleRecorder(page);
58
+ return {
59
+ context,
60
+ page
61
+ };
62
+ }
63
+ };
64
+ /** Wipe everything a story could leave behind; false when the page can't be trusted again. */
65
+ async function reset(entry) {
66
+ try {
67
+ await entry.page.evaluate(clearBrowserState);
68
+ await entry.context.deleteCookie(...await entry.context.cookies());
69
+ await entry.page.goto("about:blank");
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ async function closeEntry(entry) {
76
+ await entry.context.close().catch(() => {});
77
+ }
78
+ /** Runs in the page: clears web storage and IndexedDB for the current origin. */
79
+ async function clearBrowserState() {
80
+ const scope = globalThis;
81
+ scope.localStorage.clear();
82
+ scope.sessionStorage.clear();
83
+ const databases = await scope.indexedDB?.databases?.() ?? [];
84
+ for (const database of databases) if (database.name) scope.indexedDB?.deleteDatabase(database.name);
85
+ }
86
+ //#endregion
87
+ export { PagePool, acquirePage };
88
+
89
+ //# sourceMappingURL=pages.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.mjs","names":[],"sources":["../src/pages.ts"],"sourcesContent":["/** Page leasing for captures: a fresh page each time, or a small reusable pool (opt-in). */\n\nimport type { Viewport } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Browser, BrowserContext, Page } from \"puppeteer-core\";\nimport { installSettleRecorder } from \"./settle.ts\";\n\n/** A page on loan for one capture; `release(false)` discards it instead of reusing it. */\nexport interface PageLease {\n page: Page;\n release(reusable: boolean): Promise<void>;\n}\n\n/** Lease a page for `viewport`: pooled when the context has a pool, otherwise a throwaway. */\nexport async function acquirePage(\n ctx: { browser: Browser; pagePool?: PagePool },\n viewport: Viewport,\n): Promise<PageLease> {\n if (ctx.pagePool) return await ctx.pagePool.acquire(viewport);\n const page = await ctx.browser.newPage();\n await page.setViewport({ width: viewport.width, height: viewport.height });\n await installSettleRecorder(page);\n return {\n page,\n release: async () => {\n await page.close();\n },\n };\n}\n\ninterface Entry {\n context: BrowserContext;\n page: Page;\n}\n\n/**\n * Reuses pages (each in its own browser context) across captures to skip\n * per-capture startup and keep Storybook's JS in the HTTP cache. A page is\n * reset between captures (storage and cookies cleared, blank document) and\n * discarded, never reused, after a failed capture.\n */\nexport class PagePool {\n private readonly free: Entry[] = [];\n\n private readonly browser: Browser;\n\n constructor(browser: Browser) {\n this.browser = browser;\n }\n\n async acquire(viewport: Viewport): Promise<PageLease> {\n const entry = this.free.pop() ?? (await this.open());\n await entry.page.setViewport({ width: viewport.width, height: viewport.height });\n return {\n page: entry.page,\n release: async (reusable) => {\n if (reusable && (await reset(entry))) this.free.push(entry);\n else await closeEntry(entry);\n },\n };\n }\n\n /** Close every idle page; call before the browser is replaced or closed. */\n async dispose(): Promise<void> {\n await Promise.all(\n this.free.splice(0).map(async (entry) => {\n await closeEntry(entry);\n }),\n );\n }\n\n private async open(): Promise<Entry> {\n const context = await this.browser.createBrowserContext();\n const page = await context.newPage();\n await installSettleRecorder(page);\n return { context, page };\n }\n}\n\n/** Wipe everything a story could leave behind; false when the page can't be trusted again. */\nasync function reset(entry: Entry): Promise<boolean> {\n try {\n await entry.page.evaluate(clearBrowserState);\n await entry.context.deleteCookie(...(await entry.context.cookies()));\n await entry.page.goto(\"about:blank\");\n return true;\n } catch {\n return false;\n }\n}\n\nasync function closeEntry(entry: Entry): Promise<void> {\n await entry.context.close().catch(() => {});\n}\n\n/** Runs in the page: clears web storage and IndexedDB for the current origin. */\nasync function clearBrowserState(): Promise<void> {\n const scope = globalThis as unknown as {\n localStorage: { clear(): void };\n sessionStorage: { clear(): void };\n indexedDB?: {\n databases?: () => Promise<{ name?: string }[]>;\n deleteDatabase(name: string): unknown;\n };\n };\n scope.localStorage.clear();\n scope.sessionStorage.clear();\n const databases = (await scope.indexedDB?.databases?.()) ?? [];\n for (const database of databases) {\n if (database.name) scope.indexedDB?.deleteDatabase(database.name);\n }\n}\n"],"mappings":";;;;;;AAaA,eAAsB,YACpB,KACA,UACoB;CACpB,IAAI,IAAI,UAAU,OAAO,MAAM,IAAI,SAAS,QAAQ,QAAQ;CAC5D,MAAM,OAAO,MAAM,IAAI,QAAQ,QAAQ;CACvC,MAAM,KAAK,YAAY;EAAE,OAAO,SAAS;EAAO,QAAQ,SAAS;CAAO,CAAC;CACzE,MAAM,sBAAsB,IAAI;CAChC,OAAO;EACL;EACA,SAAS,YAAY;GACnB,MAAM,KAAK,MAAM;EACnB;CACF;AACF;;;;;;;AAaA,IAAa,WAAb,MAAsB;CACpB,OAAiC,CAAC;CAElC;CAEA,YAAY,SAAkB;EAC5B,KAAK,UAAU;CACjB;CAEA,MAAM,QAAQ,UAAwC;EACpD,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAM,MAAM,KAAK,KAAK;EAClD,MAAM,MAAM,KAAK,YAAY;GAAE,OAAO,SAAS;GAAO,QAAQ,SAAS;EAAO,CAAC;EAC/E,OAAO;GACL,MAAM,MAAM;GACZ,SAAS,OAAO,aAAa;IAC3B,IAAI,YAAa,MAAM,MAAM,KAAK,GAAI,KAAK,KAAK,KAAK,KAAK;SACrD,MAAM,WAAW,KAAK;GAC7B;EACF;CACF;;CAGA,MAAM,UAAyB;EAC7B,MAAM,QAAQ,IACZ,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,UAAU;GACvC,MAAM,WAAW,KAAK;EACxB,CAAC,CACH;CACF;CAEA,MAAc,OAAuB;EACnC,MAAM,UAAU,MAAM,KAAK,QAAQ,qBAAqB;EACxD,MAAM,OAAO,MAAM,QAAQ,QAAQ;EACnC,MAAM,sBAAsB,IAAI;EAChC,OAAO;GAAE;GAAS;EAAK;CACzB;AACF;;AAGA,eAAe,MAAM,OAAgC;CACnD,IAAI;EACF,MAAM,MAAM,KAAK,SAAS,iBAAiB;EAC3C,MAAM,MAAM,QAAQ,aAAa,GAAI,MAAM,MAAM,QAAQ,QAAQ,CAAE;EACnE,MAAM,MAAM,KAAK,KAAK,aAAa;EACnC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,WAAW,OAA6B;CACrD,MAAM,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;AAC5C;;AAGA,eAAe,oBAAmC;CAChD,MAAM,QAAQ;CAQd,MAAM,aAAa,MAAM;CACzB,MAAM,eAAe,MAAM;CAC3B,MAAM,YAAa,MAAM,MAAM,WAAW,YAAY,KAAM,CAAC;CAC7D,KAAK,MAAM,YAAY,WACrB,IAAI,SAAS,MAAM,MAAM,WAAW,eAAe,SAAS,IAAI;AAEpE"}
package/dist/pipeline.mjs CHANGED
@@ -2,74 +2,171 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { messageOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
5
+ import { PagePool } from "./pages.mjs";
6
+ import { measureRemote } from "./remote-environment.mjs";
5
7
  import { createRuntimeParamsState } from "./runtime-params.mjs";
6
8
  import { captureScreenshot } from "./screenshot.mjs";
7
9
  import { createStaticServer } from "./static-server.mjs";
10
+ import { TargetLaunchError, engineOf } from "@storyshelf/core/adapter/capture-runner";
11
+ import { DEFAULT_RECYCLE_BROWSER_EVERY, chunk, createPageLimiter, describeSizing, detectCaptureEnvironment, isTimeoutError, sessionGateFor, storybookBase } from "@storyshelf/core/utils";
8
12
  import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
9
- import puppeteer from "puppeteer-core";
10
13
  //#region src/pipeline.ts
14
+ /** Render orchestration: static server, browser, and per-story captures. */
11
15
  /** Render all story/viewport combinations for a build. */
12
- async function renderAll(input, active, runnerOptions) {
13
- const server = await createStaticServer(input.storybookDir);
14
- return await runWithBrowser(input, active, await launchBrowser(runnerOptions), server);
16
+ async function renderAll(rawInput, active, driver, defaultTarget) {
17
+ const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
18
+ assertReachable(name, remote, input.storybookUrl);
19
+ const browser = await driver.launch(name, input.logger);
20
+ const server = await serve(input, browser);
21
+ input.logger?.info(describeSizing({
22
+ ...input,
23
+ browser: engineOf(name)
24
+ }), "capture sizing");
25
+ const environment = remote ? await measureRemote(browser, remote.remote, input.logger) : await localEnvironment(browser);
26
+ const result = await runWithBrowser(input, active, browser, server, async () => await driver.launch(name, input.logger));
27
+ for (const row of [...result.captures, ...result.failures]) row.browser = name;
28
+ return {
29
+ ...result,
30
+ environment
31
+ };
32
+ }
33
+ /** The input with its target resolved, and the session gate when the target is remote. */
34
+ function planRun(rawInput, driver, defaultTarget) {
35
+ const name = rawInput.target ?? defaultTarget;
36
+ const remote = driver.remote(name);
37
+ const gate = remote && sessionGateFor(name, remote.remote.sessions);
38
+ return {
39
+ input: {
40
+ ...rawInput,
41
+ target: name,
42
+ gate
43
+ },
44
+ name,
45
+ remote
46
+ };
47
+ }
48
+ /** A target whose browser cannot route back to this host needs a URL; failing here names why. */
49
+ function assertReachable(target, remote, storybookUrl) {
50
+ if (remote?.reach === "url" && storybookUrl === void 0) throw new TargetLaunchError(target, "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", { transient: true });
51
+ if (storybookUrl !== void 0) assertStorybookRoot(target, storybookUrl);
52
+ }
53
+ /** The URL must be the build's directory root; refused before any browser connects. */
54
+ function assertStorybookRoot(target, storybookUrl) {
55
+ try {
56
+ storybookBase(storybookUrl);
57
+ } catch (error) {
58
+ throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));
59
+ }
60
+ }
61
+ /** The loopback server, or the URL given; a server that cannot start closes the browser it was for. */
62
+ async function serve(input, browser) {
63
+ if (input.storybookUrl !== void 0) return {
64
+ url: storybookBase(input.storybookUrl),
65
+ close: async () => {
66
+ await Promise.resolve();
67
+ }
68
+ };
69
+ return await createStaticServer(input.storybookDir).catch(async (error) => {
70
+ await safeCloseBrowser(browser);
71
+ throw error;
72
+ });
15
73
  }
16
- async function runWithBrowser(input, active, browser, server) {
74
+ /** This host's environment: a local browser renders on the machine the runner runs on. */
75
+ async function localEnvironment(browser) {
76
+ const version = typeof browser.version === "function" ? await browser.version() : void 0;
77
+ return await detectCaptureEnvironment(version);
78
+ }
79
+ async function runWithBrowser(input, active, browser, server, relaunch) {
17
80
  active.browser = browser;
18
81
  const ctx = buildContext(browser, server.url, input);
19
82
  const captures = [];
20
83
  const failures = [];
21
84
  try {
22
- await runAllCaptures(input, active, ctx, captures, failures);
85
+ await runAllCaptures(input, active, ctx, captures, failures, relaunch);
23
86
  return {
24
87
  captures,
25
88
  failures
26
89
  };
27
90
  } finally {
28
91
  active.browser = null;
29
- await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);
92
+ await ctx.pagePool?.dispose();
93
+ await Promise.all([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);
30
94
  }
31
95
  }
32
- async function launchBrowser(opts) {
33
- return await puppeteer.launch({
34
- headless: true,
35
- executablePath: opts.executablePath,
36
- args: opts.args ?? ["--no-sandbox", "--disable-setuid-sandbox"]
37
- });
38
- }
39
96
  function buildContext(browser, baseUrl, input) {
40
97
  return {
41
98
  browser,
42
99
  adapter: input.adapter ?? new StorybookAdapter(),
43
100
  baseUrl,
101
+ navigationTimeoutMs: input.navigationTimeoutMs,
44
102
  executePlay: input.executePlay,
45
103
  playTimeoutMs: input.playTimeoutMs,
46
104
  runA11y: input.runA11y,
47
- runtimeParams: createRuntimeParamsState()
105
+ ...input.logger ? { logger: input.logger } : {},
106
+ runtimeParams: createRuntimeParamsState(),
107
+ ...input.reusePages ? { pagePool: new PagePool(browser) } : {}
48
108
  };
49
109
  }
50
- async function runAllCaptures(input, active, ctx, captures, failures) {
51
- const tasks = input.stories.flatMap((story) => viewportsTasks(story, input, active, ctx, captures, failures));
52
- await Promise.all(tasks);
110
+ async function runAllCaptures(input, active, ctx, captures, failures, relaunch) {
111
+ const jobs = input.stories.flatMap((story) => resolveStoryViewports(story, input.viewports).map((viewport) => ({
112
+ story,
113
+ viewport
114
+ })));
115
+ const batches = chunk(jobs, input.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY);
116
+ const pages = createPageLimiter({
117
+ concurrency: input.concurrency,
118
+ ...input.gate === void 0 ? {} : { gate: input.gate },
119
+ browser: engineOf(input.target ?? "chromium"),
120
+ logger: input.logger
121
+ });
122
+ try {
123
+ await runBatches(batches, pages, input, active, ctx, {
124
+ captures,
125
+ failures,
126
+ relaunch
127
+ });
128
+ } finally {
129
+ pages.dispose();
130
+ }
131
+ }
132
+ async function runBatches(batches, pages, input, active, ctx, out) {
133
+ for (const [index, batch] of batches.entries()) {
134
+ if (index > 0) await recycleBrowser(active, ctx, out.relaunch, input.logger);
135
+ await pages.each(batch, async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out));
136
+ }
53
137
  }
54
- function viewportsTasks(story, input, active, ctx, captures, failures) {
55
- return resolveStoryViewports(story, input.viewports).map((viewport) => captureOne(input, active, ctx, story, viewport, captures, failures));
138
+ /** Swap in a fresh browser; the old one is closed first so two never coexist. */
139
+ async function recycleBrowser(active, ctx, relaunch, logger) {
140
+ if (active.cancelled) throw new Error("Capture cancelled");
141
+ await ctx.pagePool?.dispose();
142
+ await safeCloseBrowser(ctx.browser);
143
+ ctx.browser = await relaunch();
144
+ if (ctx.pagePool) ctx.pagePool = new PagePool(ctx.browser);
145
+ active.browser = ctx.browser;
146
+ logger?.info({}, "browser recycled");
56
147
  }
57
- async function captureOne(input, active, ctx, story, viewport, captures, failures) {
148
+ /** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */
149
+ async function captureOne(input, active, ctx, story, viewport, out) {
58
150
  if (active.cancelled) throw new Error("Capture cancelled");
59
151
  try {
60
- await pushCapture(ctx, story, viewport, captures, failures, input);
152
+ await pushCapture(ctx, story, viewport, out.captures, out.failures, input);
153
+ return false;
61
154
  } catch (error) {
62
- pushRenderFailure(failures, input, story.id, viewport.name, error);
155
+ pushRenderFailure(out.failures, input, story.id, viewport.name, error);
156
+ return isTimeoutError(error);
63
157
  }
64
158
  }
65
159
  async function pushCapture(ctx, story, viewport, captures, failures, input) {
66
160
  const result = await captureScreenshot(ctx, story, viewport);
67
- captures.push({
161
+ const snapshot = {
68
162
  story: result.story,
69
163
  viewportName: viewport.name,
70
164
  viewport,
71
- screenshot: result.screenshot
72
- });
165
+ screenshot: result.screenshot,
166
+ ...result.a11yViolations.length > 0 ? { a11y: result.a11yViolations } : {}
167
+ };
168
+ if (input.onCapture) await input.onCapture(snapshot);
169
+ else captures.push(snapshot);
73
170
  if (result.a11yViolations.length > 0) handleA11y(input, failures, story, viewport, result.a11yViolations);
74
171
  }
75
172
  function handleA11y(input, failures, story, viewport, violations) {
@@ -97,6 +194,6 @@ function pushRenderFailure(failures, input, storyId, viewportName, error) {
97
194
  }, "render failed for story");
98
195
  }
99
196
  //#endregion
100
- export { renderAll };
197
+ export { assertReachable, planRun, renderAll, serve };
101
198
 
102
199
  //# 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 type {\n RenderResult,\n RenderedSnapshot,\n StoryEntry,\n Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } from \"@storyshelf/core/capture\";\nimport type { Browser } from \"puppeteer-core\";\nimport puppeteer from \"puppeteer-core\";\nimport { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { createRuntimeParamsState } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type { ActiveRun, CaptureContext, PuppeteerRenderInput } from \"./types.ts\";\n\n/** Render all story/viewport combinations for a build. */\nexport async function renderAll(\n input: PuppeteerRenderInput,\n active: ActiveRun,\n runnerOptions: { executablePath?: string; args?: string[] },\n): Promise<RenderResult> {\n const server = await createStaticServer(input.storybookDir);\n const browser = await launchBrowser(runnerOptions);\n return await runWithBrowser(input, active, browser, server);\n}\n\nasync function runWithBrowser(\n input: PuppeteerRenderInput,\n active: ActiveRun,\n browser: Browser,\n server: Awaited<ReturnType<typeof createStaticServer>>,\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);\n return { captures, failures };\n } finally {\n active.browser = null;\n await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);\n }\n}\n\nasync function launchBrowser(opts: { executablePath?: string; args?: string[] }): Promise<Browser> {\n return await puppeteer.launch({\n headless: true,\n executablePath: opts.executablePath,\n args: opts.args ?? [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n });\n}\n\nfunction buildContext(\n browser: Browser,\n baseUrl: string,\n input: PuppeteerRenderInput,\n): CaptureContext {\n return {\n browser,\n adapter: input.adapter ?? new StorybookAdapter(),\n baseUrl,\n executePlay: input.executePlay,\n playTimeoutMs: input.playTimeoutMs,\n runA11y: input.runA11y,\n runtimeParams: createRuntimeParamsState(),\n };\n}\n\nasync function runAllCaptures(\n input: PuppeteerRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void> {\n const tasks = input.stories.flatMap((story) =>\n viewportsTasks(story, input, active, ctx, captures, failures),\n );\n await Promise.all(tasks);\n}\nfunction viewportsTasks(\n story: StoryEntry,\n input: PuppeteerRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void>[] {\n // oxlint-disable-next-line typescript/promise-function-async -- returns array of promises, not a single promise\n return resolveStoryViewports(story, input.viewports).map((viewport) =>\n captureOne(input, active, ctx, story, viewport, captures, failures),\n );\n}\n\nasync function captureOne(\n input: PuppeteerRenderInput,\n active: ActiveRun,\n ctx: CaptureContext,\n story: StoryEntry,\n viewport: Viewport,\n captures: RenderedSnapshot[],\n failures: RenderResult[\"failures\"],\n): Promise<void> {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n try {\n await pushCapture(ctx, story, viewport, captures, failures, input);\n } catch (error) {\n pushRenderFailure(failures, input, story.id, viewport.name, 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: PuppeteerRenderInput,\n): Promise<void> {\n const result = await captureScreenshot(ctx, story, viewport);\n captures.push({\n story: result.story,\n viewportName: viewport.name,\n viewport,\n screenshot: result.screenshot,\n });\n if (result.a11yViolations.length > 0)\n handleA11y(input, failures, story, viewport, result.a11yViolations);\n}\nfunction handleA11y(\n input: PuppeteerRenderInput,\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: PuppeteerRenderInput,\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":";;;;;;;;;;;AAkBA,eAAsB,UACpB,OACA,QACA,eACuB;CACvB,MAAM,SAAS,MAAM,mBAAmB,MAAM,YAAY;CAE1D,OAAO,MAAM,eAAe,OAAO,QAAQ,MADrB,cAAc,aAAa,GACG,MAAM;AAC5D;AAEA,eAAe,eACb,OACA,QACA,SACA,QACuB;CACvB,OAAO,UAAU;CACjB,MAAM,MAAM,aAAa,SAAS,OAAO,KAAK,KAAK;CACnD,MAAM,WAA+B,CAAC;CACtC,MAAM,WAAqC,CAAC;CAC5C,IAAI;EACF,MAAM,eAAe,OAAO,QAAQ,KAAK,UAAU,QAAQ;EAC3D,OAAO;GAAE;GAAU;EAAS;CAC9B,UAAU;EACR,OAAO,UAAU;EACjB,MAAM,QAAQ,IAAI,CAAC,iBAAiB,OAAO,GAAG,gBAAgB,MAAM,CAAC,CAAC;CACxE;AACF;AAEA,eAAe,cAAc,MAAsE;CACjG,OAAO,MAAM,UAAU,OAAO;EAC5B,UAAU;EACV,gBAAgB,KAAK;EACrB,MAAM,KAAK,QAAQ,CAAC,gBAAgB,0BAA0B;CAChE,CAAC;AACH;AAEA,SAAS,aACP,SACA,SACA,OACgB;CAChB,OAAO;EACL;EACA,SAAS,MAAM,WAAW,IAAI,iBAAiB;EAC/C;EACA,aAAa,MAAM;EACnB,eAAe,MAAM;EACrB,SAAS,MAAM;EACf,eAAe,yBAAyB;CAC1C;AACF;AAEA,eAAe,eACb,OACA,QACA,KACA,UACA,UACe;CACf,MAAM,QAAQ,MAAM,QAAQ,SAAS,UACnC,eAAe,OAAO,OAAO,QAAQ,KAAK,UAAU,QAAQ,CAC9D;CACA,MAAM,QAAQ,IAAI,KAAK;AACzB;AACA,SAAS,eACP,OACA,OACA,QACA,KACA,UACA,UACiB;CAEjB,OAAO,sBAAsB,OAAO,MAAM,SAAS,CAAC,CAAC,KAAK,aACxD,WAAW,OAAO,QAAQ,KAAK,OAAO,UAAU,UAAU,QAAQ,CACpE;AACF;AAEA,eAAe,WACb,OACA,QACA,KACA,OACA,UACA,UACA,UACe;CACf,IAAI,OAAO,WAAW,MAAM,IAAI,MAAM,mBAAmB;CACzD,IAAI;EACF,MAAM,YAAY,KAAK,OAAO,UAAU,UAAU,UAAU,KAAK;CACnE,SAAS,OAAO;EACd,kBAAkB,UAAU,OAAO,MAAM,IAAI,SAAS,MAAM,KAAK;CACnE;AACF;AAEA,eAAe,YACb,KACA,OACA,UACA,UACA,UACA,OACe;CACf,MAAM,SAAS,MAAM,kBAAkB,KAAK,OAAO,QAAQ;CAC3D,SAAS,KAAK;EACZ,OAAO,OAAO;EACd,cAAc,SAAS;EACvB;EACA,YAAY,OAAO;CACrB,CAAC;CACD,IAAI,OAAO,eAAe,SAAS,GACjC,WAAW,OAAO,UAAU,OAAO,UAAU,OAAO,cAAc;AACtE;AACA,SAAS,WACP,OACA,UACA,OACA,UACA,YACM;CACN,SAAS,KAAK;EACZ,SAAS,MAAM;EACf,cAAc,SAAS;EACvB,OAAO,SAAS,WAAW,KAAK,IAAI;CACtC,CAAC;CACD,MAAM,QAAQ,KACZ;EAAE,SAAS,MAAM;EAAI,UAAU,SAAS;EAAM;CAAW,GACzD,uBACF;AACF;AAEA,SAAS,kBACP,UACA,OACA,SACA,cACA,OACM;CACN,SAAS,KAAK;EAAE;EAAS;EAAc,OAAO,UAAU,KAAK;CAAE,CAAC;CAChE,MAAM,QAAQ,MAAM;EAAE;EAAS,UAAU;EAAc,KAAK;CAAM,GAAG,yBAAyB;AAChG"}
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 Viewport,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { StorybookAdapter, resolveStoryViewports } 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 \"puppeteer-core\";\nimport { messageOf, safeCloseBrowser, safeCloseServer } from \"./browser.ts\";\nimport { PagePool } from \"./pages.ts\";\nimport { measureRemote } from \"./remote-environment.ts\";\nimport { createRuntimeParamsState } from \"./runtime-params.ts\";\nimport { captureScreenshot } from \"./screenshot.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n PuppeteerRenderInput,\n RunInput,\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: PuppeteerRenderInput,\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 // Measured while the browser is open: Puppeteer cannot read the version of a closed one.\n const environment = remote\n ? await measureRemote(browser, remote.remote, input.logger)\n : await localEnvironment(browser);\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 };\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/** 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\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function localEnvironment(browser: Browser): Promise<CaptureEnvironment> {\n const version = typeof browser.version === \"function\" ? await browser.version() : undefined;\n return await detectCaptureEnvironment(version);\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: PuppeteerRenderInput,\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: PuppeteerRenderInput[\"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: PuppeteerRenderInput,\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: PuppeteerRenderInput,\n): Promise<void> {\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 ...(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: PuppeteerRenderInput,\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: PuppeteerRenderInput,\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
@@ -15,7 +15,7 @@ async function runPlay(page, storyId, timeoutMs) {
15
15
  }
16
16
  }
17
17
  /**
18
- * Runs inside the browser. Playwright serializes only this function's source, so it must not
18
+ * Runs inside the browser. Puppeteer serializes only this function's source, so it must not
19
19
  * reference anything from this module (helpers, imports): split it up and the page throws
20
20
  * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.
21
21
  */
@@ -29,7 +29,20 @@ async function inPagePlay({ storyId, timeoutMs }) {
29
29
  });
30
30
  } catch {}
31
31
  const canvasElement = globalThis.document?.querySelector("#storybook-root");
32
- const work = preview.executePlay ? preview.executePlay(storyId) : preview.storyStore?.fromId?.(storyId)?.play?.({ canvasElement });
32
+ const store = preview.storyStoreValue ?? preview.storyStore;
33
+ const runLoaded = async () => {
34
+ const story = await store?.loadStory?.({ storyId });
35
+ if (!story?.playFunction) return;
36
+ const context = (preview.storyRenders?.find((r) => r.id === storyId))?.storyContext?.() ?? store?.getStoryContext?.(story) ?? {};
37
+ await story.playFunction({
38
+ ...context,
39
+ canvasElement
40
+ });
41
+ };
42
+ let work;
43
+ if (preview.executePlay) work = preview.executePlay(storyId);
44
+ else if (store?.loadStory) work = runLoaded();
45
+ else work = store?.fromId?.(storyId)?.play?.({ canvasElement });
33
46
  if (!work) return;
34
47
  let timer;
35
48
  const timeout = new Promise((_resolve, reject) => {
package/dist/play.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"play.mjs","names":[],"sources":["../src/play.ts"],"sourcesContent":["/** Storybook `play` function execution inside the page. */\n\nimport type { Page } from \"puppeteer-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/** Arguments serialized into the page for {@link inPagePlay}. */\nexport interface PlayArgs {\n storyId: string;\n timeoutMs: number;\n}\n\n/** Execute the story's play function with a timeout race. */\nexport async function runPlay(page: Page, storyId: string, timeoutMs: number): Promise<void> {\n try {\n await page.evaluate(inPagePlay, { storyId, timeoutMs });\n } catch (error) {\n throw new Error(`play failed: ${messageOf(error)}`, { cause: error });\n }\n}\n\n/**\n * Runs inside the browser. Playwright serializes only this function's source, so it must not\n * reference anything from this module (helpers, imports): split it up and the page throws\n * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.\n */\n// oxlint-disable-next-line eslint/max-statements -- must stay one self-contained function (see above)\nexport async function inPagePlay({ storyId, timeoutMs }: PlayArgs): Promise<void> {\n type Preview = {\n executePlay?: (id: string) => Promise<void>;\n storyStore?: { fromId?: (id: string) => { play?: (ctx: unknown) => Promise<void> } };\n channel?: { on: (event: string, cb: (err: unknown) => void) => void };\n };\n const preview = (globalThis as unknown as Record<string, Preview | undefined>)[\n \"__STORYBOOK_PREVIEW__\"\n ];\n if (!preview) return;\n let playError: unknown = null;\n try {\n // Best-effort: play still runs without the channel hookup.\n preview.channel?.on(\"playFunctionThrewException\", (err) => {\n playError = err;\n });\n } catch {\n // ignore\n }\n const canvasElement = globalThis.document?.querySelector(\"#storybook-root\");\n const work = preview.executePlay\n ? preview.executePlay(storyId)\n : preview.storyStore?.fromId?.(storyId)?.play?.({ canvasElement });\n if (!work) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`play timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n try {\n await Promise.race([work, timeout]);\n } finally {\n clearTimeout(timer);\n }\n if (playError) throw playError;\n}\n"],"mappings":";;;;;;AAYA,eAAsB,QAAQ,MAAY,SAAiB,WAAkC;CAC3F,IAAI;EACF,MAAM,KAAK,SAAS,YAAY;GAAE;GAAS;EAAU,CAAC;CACxD,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,gBAAgB,UAAU,KAAK,KAAK,EAAE,OAAO,MAAM,CAAC;CACtE;AACF;;;;;;AAQA,eAAsB,WAAW,EAAE,SAAS,aAAsC;CAMhF,MAAM,UAAW,WACf;CAEF,IAAI,CAAC,SAAS;CACd,IAAI,YAAqB;CACzB,IAAI;EAEF,QAAQ,SAAS,GAAG,+BAA+B,QAAQ;GACzD,YAAY;EACd,CAAC;CACH,QAAQ,CAER;CACA,MAAM,gBAAgB,WAAW,UAAU,cAAc,iBAAiB;CAC1E,MAAM,OAAO,QAAQ,cACjB,QAAQ,YAAY,OAAO,IAC3B,QAAQ,YAAY,SAAS,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC;CACnE,IAAI,CAAC,MAAM;CACX,IAAI;CACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;EACvD,QAAQ,iBAAiB;GACvB,uBAAO,IAAI,MAAM,sBAAsB,UAAU,GAAG,CAAC;EACvD,GAAG,SAAS;CACd,CAAC;CACD,IAAI;EACF,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;CACpC,UAAU;EACR,aAAa,KAAK;CACpB;CACA,IAAI,WAAW,MAAM;AACvB"}
1
+ {"version":3,"file":"play.mjs","names":[],"sources":["../src/play.ts"],"sourcesContent":["/** Storybook `play` function execution inside the page. */\n\nimport type { Page } from \"puppeteer-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/** Arguments serialized into the page for {@link inPagePlay}. */\nexport interface PlayArgs {\n storyId: string;\n timeoutMs: number;\n}\n\n/** Execute the story's play function with a timeout race. */\nexport async function runPlay(page: Page, storyId: string, timeoutMs: number): Promise<void> {\n try {\n await page.evaluate(inPagePlay, { storyId, timeoutMs });\n } catch (error) {\n throw new Error(`play failed: ${messageOf(error)}`, { cause: error });\n }\n}\n\n/**\n * Runs inside the browser. Puppeteer serializes only this function's source, so it must not\n * reference anything from this module (helpers, imports): split it up and the page throws\n * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.\n */\n// oxlint-disable-next-line eslint/max-statements, eslint/max-lines-per-function -- must stay one self-contained function (see above)\nexport async function inPagePlay({ storyId, timeoutMs }: PlayArgs): Promise<void> {\n type Story = { playFunction?: (ctx: unknown) => Promise<void> };\n type Store = {\n loadStory?: (arg: { storyId: string }) => Promise<Story>;\n getStoryContext?: (story: Story) => Record<string, unknown>;\n fromId?: (id: string) => { play?: (ctx: unknown) => Promise<void> };\n };\n type Preview = {\n executePlay?: (id: string) => Promise<void>;\n storyStoreValue?: Store;\n storyStore?: Store;\n storyRenders?: { id: string; storyContext?: () => Record<string, unknown> }[];\n channel?: { on: (event: string, cb: (err: unknown) => void) => void };\n };\n const preview = (globalThis as unknown as Record<string, Preview | undefined>)[\n \"__STORYBOOK_PREVIEW__\"\n ];\n if (!preview) return;\n let playError: unknown = null;\n try {\n // Best-effort: play still runs without the channel hookup.\n preview.channel?.on(\"playFunctionThrewException\", (err) => {\n playError = err;\n });\n } catch {\n // ignore\n }\n const canvasElement = globalThis.document?.querySelector(\"#storybook-root\");\n // SB 8+ has no `executePlay`, and `fromId` throws until every CSF file is cached, so load just\n // this story and run its `playFunction` with the context of the render that is already mounted.\n const store = preview.storyStoreValue ?? preview.storyStore;\n const runLoaded = async (): Promise<void> => {\n const story = await store?.loadStory?.({ storyId });\n if (!story?.playFunction) return;\n const render = preview.storyRenders?.find((r) => r.id === storyId);\n const context = render?.storyContext?.() ?? store?.getStoryContext?.(story) ?? {};\n await story.playFunction({ ...context, canvasElement });\n };\n let work: Promise<void> | undefined;\n if (preview.executePlay) work = preview.executePlay(storyId);\n else if (store?.loadStory) work = runLoaded();\n else work = store?.fromId?.(storyId)?.play?.({ canvasElement });\n if (!work) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`play timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n try {\n await Promise.race([work, timeout]);\n } finally {\n clearTimeout(timer);\n }\n if (playError) throw playError;\n}\n"],"mappings":";;;;;;AAYA,eAAsB,QAAQ,MAAY,SAAiB,WAAkC;CAC3F,IAAI;EACF,MAAM,KAAK,SAAS,YAAY;GAAE;GAAS;EAAU,CAAC;CACxD,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,gBAAgB,UAAU,KAAK,KAAK,EAAE,OAAO,MAAM,CAAC;CACtE;AACF;;;;;;AAQA,eAAsB,WAAW,EAAE,SAAS,aAAsC;CAchF,MAAM,UAAW,WACf;CAEF,IAAI,CAAC,SAAS;CACd,IAAI,YAAqB;CACzB,IAAI;EAEF,QAAQ,SAAS,GAAG,+BAA+B,QAAQ;GACzD,YAAY;EACd,CAAC;CACH,QAAQ,CAER;CACA,MAAM,gBAAgB,WAAW,UAAU,cAAc,iBAAiB;CAG1E,MAAM,QAAQ,QAAQ,mBAAmB,QAAQ;CACjD,MAAM,YAAY,YAA2B;EAC3C,MAAM,QAAQ,MAAM,OAAO,YAAY,EAAE,QAAQ,CAAC;EAClD,IAAI,CAAC,OAAO,cAAc;EAE1B,MAAM,WADS,QAAQ,cAAc,MAAM,MAAM,EAAE,OAAO,OAAO,EAAA,EACzC,eAAe,KAAK,OAAO,kBAAkB,KAAK,KAAK,CAAC;EAChF,MAAM,MAAM,aAAa;GAAE,GAAG;GAAS;EAAc,CAAC;CACxD;CACA,IAAI;CACJ,IAAI,QAAQ,aAAa,OAAO,QAAQ,YAAY,OAAO;MACtD,IAAI,OAAO,WAAW,OAAO,UAAU;MACvC,OAAO,OAAO,SAAS,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC;CAC9D,IAAI,CAAC,MAAM;CACX,IAAI;CACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;EACvD,QAAQ,iBAAiB;GACvB,uBAAO,IAAI,MAAM,sBAAsB,UAAU,GAAG,CAAC;EACvD,GAAG,SAAS;CACd,CAAC;CACD,IAAI;EACF,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;CACpC,UAAU;EACR,aAAa,KAAK;CACpB;CACA,IAAI,WAAW,MAAM;AACvB"}
@@ -0,0 +1,29 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
4
+ //#region src/remote/cdp.d.ts
5
+ /** Options of {@link cdp}. */
6
+ interface CdpOptions {
7
+ /** The target name half (`chromium@<name>`): lowercase letters, digits and hyphens, at most 32. */
8
+ name: string;
9
+ /** `ws(s)://…`, or `http(s)://…` of a Chrome with remote debugging (its WebSocket is looked up). May carry a key: it is never logged. */
10
+ endpoint: string;
11
+ /** Handshake headers, such as `authorization`. Never logged. */
12
+ headers?: Record<string, string>;
13
+ /** Parallel pages the service allows (default 1: set it to the plan's cap). */
14
+ sessions?: number;
15
+ /** Engines to offer (default `chromium`; `chrome` when the service is real Chrome). */
16
+ engines?: readonly BrowserName[];
17
+ /** OS the service runs on, reported when the page cannot tell. */
18
+ platform?: string;
19
+ /** How long one connect attempt waits in ms (default 30 000). */
20
+ connectTimeoutMs?: number;
21
+ }
22
+ /**
23
+ * Targets on a CDP service, for example `chromium@cloudflare`. The browser cannot route back to
24
+ * this host, so every target needs a signed Storybook URL (ADR 0007, decision 5).
25
+ */
26
+ declare function cdp(options: CdpOptions): RemoteBrowser;
27
+ //#endregion
28
+ export { CdpOptions, cdp };
29
+ //# sourceMappingURL=cdp.d.mts.map
@@ -0,0 +1,34 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { defineRemote } from "@storyshelf/core/adapter/capture-runner";
5
+ //#region src/remote/cdp.ts
6
+ /** A Chrome DevTools Protocol endpoint: Browserless, Cloudflare Browser Rendering, a bare Chrome, … */
7
+ /** Puppeteer drives the Chromium family, over CDP only (ADR 0007, decision 1). */
8
+ const SUPPORT = {
9
+ engines: ["chromium", "chrome"],
10
+ protocols: ["cdp"],
11
+ label: "cdp()"
12
+ };
13
+ /**
14
+ * Targets on a CDP service, for example `chromium@cloudflare`. The browser cannot route back to
15
+ * this host, so every target needs a signed Storybook URL (ADR 0007, decision 5).
16
+ */
17
+ function cdp(options) {
18
+ return defineRemote({
19
+ name: options.name,
20
+ engines: options.engines ?? ["chromium"],
21
+ connect: () => ({
22
+ protocol: "cdp",
23
+ endpoint: options.endpoint,
24
+ ...options.headers === void 0 ? {} : { headers: options.headers }
25
+ }),
26
+ sessions: options.sessions ?? 1,
27
+ platform: options.platform,
28
+ connectTimeoutMs: options.connectTimeoutMs
29
+ }, SUPPORT);
30
+ }
31
+ //#endregion
32
+ export { cdp };
33
+
34
+ //# sourceMappingURL=cdp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp.mjs","names":[],"sources":["../../src/remote/cdp.ts"],"sourcesContent":["/** A Chrome DevTools Protocol endpoint: Browserless, Cloudflare Browser Rendering, a bare Chrome, … */\nimport {\n defineRemote,\n type BrowserName,\n type RemoteBrowser,\n type RemoteSupport,\n} from \"@storyshelf/core/adapter/capture-runner\";\n\n/** Puppeteer drives the Chromium family, over CDP only (ADR 0007, decision 1). */\nconst SUPPORT: RemoteSupport = {\n engines: [\"chromium\", \"chrome\"],\n protocols: [\"cdp\"],\n label: \"cdp()\",\n};\n\n/** Options of {@link cdp}. */\nexport interface CdpOptions {\n /** The target name half (`chromium@<name>`): lowercase letters, digits and hyphens, at most 32. */\n name: string;\n /** `ws(s)://…`, or `http(s)://…` of a Chrome with remote debugging (its WebSocket is looked up). May carry a key: it is never logged. */\n endpoint: string;\n /** Handshake headers, such as `authorization`. Never logged. */\n headers?: Record<string, string>;\n /** Parallel pages the service allows (default 1: set it to the plan's cap). */\n sessions?: number;\n /** Engines to offer (default `chromium`; `chrome` when the service is real Chrome). */\n engines?: readonly BrowserName[];\n /** OS the service runs on, reported when the page cannot tell. */\n platform?: string;\n /** How long one connect attempt waits in ms (default 30 000). */\n connectTimeoutMs?: number;\n}\n\n/**\n * Targets on a CDP service, for example `chromium@cloudflare`. The browser cannot route back to\n * this host, so every target needs a signed Storybook URL (ADR 0007, decision 5).\n */\nexport function cdp(options: CdpOptions): RemoteBrowser {\n return defineRemote(\n {\n name: options.name,\n engines: options.engines ?? [\"chromium\"],\n connect: () => ({\n protocol: \"cdp\",\n endpoint: options.endpoint,\n ...(options.headers === undefined ? {} : { headers: options.headers }),\n }),\n sessions: options.sessions ?? 1,\n platform: options.platform,\n connectTimeoutMs: options.connectTimeoutMs,\n },\n SUPPORT,\n );\n}\n"],"mappings":";;;;;;;AASA,MAAM,UAAyB;CAC7B,SAAS,CAAC,YAAY,QAAQ;CAC9B,WAAW,CAAC,KAAK;CACjB,OAAO;AACT;;;;;AAwBA,SAAgB,IAAI,SAAoC;CACtD,OAAO,aACL;EACE,MAAM,QAAQ;EACd,SAAS,QAAQ,WAAW,CAAC,UAAU;EACvC,gBAAgB;GACd,UAAU;GACV,UAAU,QAAQ;GAClB,GAAI,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;EACtE;EACA,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ;EAClB,kBAAkB,QAAQ;CAC5B,GACA,OACF;AACF"}
@@ -0,0 +1,5 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { CdpOptions, cdp } from "./cdp.mjs";
4
+ import { RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
5
+ export { type CdpOptions, type RemoteBrowser, cdp };
@@ -0,0 +1,5 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { cdp } from "./cdp.mjs";
5
+ export { cdp };
@@ -0,0 +1,43 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { messageOf } from "./browser.mjs";
5
+ import { FONT_PROBE_FAMILIES, inPageProbe, remoteEnvironment } from "@storyshelf/core/utils";
6
+ //#region src/remote-environment.ts
7
+ /**
8
+ * The fingerprint of the browser a render drove: its version, and what a blank page in it reports
9
+ * (OS, pixel ratio, installed fonts). Never `process.platform` or `fc-list`, which describe this
10
+ * host. A page that cannot be probed leaves those fields out, and an unknown component is not a
11
+ * change (ADR 0005), so a service outage here cannot rewrite baselines.
12
+ */
13
+ async function measureRemote(browser, remote, logger) {
14
+ return remoteEnvironment({
15
+ browserVersion: await versionOf(browser),
16
+ probe: await probeBlankPage(browser, logger),
17
+ declaredPlatform: remote.platform
18
+ });
19
+ }
20
+ async function versionOf(browser) {
21
+ try {
22
+ return typeof browser.version === "function" ? await browser.version() : void 0;
23
+ } catch {
24
+ return;
25
+ }
26
+ }
27
+ async function probeBlankPage(browser, logger) {
28
+ try {
29
+ const page = await browser.newPage();
30
+ try {
31
+ return await page.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);
32
+ } finally {
33
+ await page.close();
34
+ }
35
+ } catch (error) {
36
+ logger?.warn({ reason: messageOf(error) }, "could not measure the remote browser");
37
+ return;
38
+ }
39
+ }
40
+ //#endregion
41
+ export { measureRemote };
42
+
43
+ //# sourceMappingURL=remote-environment.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-environment.mjs","names":[],"sources":["../src/remote-environment.ts"],"sourcesContent":["/** Measuring a remote browser's environment from inside it (ADR 0007, decision 2). */\nimport type { CaptureEnvironment, RemoteBrowser } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n FONT_PROBE_FAMILIES,\n inPageProbe,\n remoteEnvironment,\n type BrowserProbe,\n} from \"@storyshelf/core/utils\";\nimport type { Browser } from \"puppeteer-core\";\nimport { messageOf } from \"./browser.ts\";\n\n/**\n * The fingerprint of the browser a render drove: its version, and what a blank page in it reports\n * (OS, pixel ratio, installed fonts). Never `process.platform` or `fc-list`, which describe this\n * host. A page that cannot be probed leaves those fields out, and an unknown component is not a\n * change (ADR 0005), so a service outage here cannot rewrite baselines.\n */\nexport async function measureRemote(\n browser: Browser,\n remote: RemoteBrowser,\n logger?: Logger,\n): Promise<CaptureEnvironment> {\n return remoteEnvironment({\n browserVersion: await versionOf(browser),\n probe: await probeBlankPage(browser, logger),\n declaredPlatform: remote.platform,\n });\n}\n\nasync function versionOf(browser: Browser): Promise<string | undefined> {\n try {\n return typeof browser.version === \"function\" ? await browser.version() : undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function probeBlankPage(\n browser: Browser,\n logger?: Logger,\n): Promise<BrowserProbe | undefined> {\n try {\n const page = await browser.newPage();\n try {\n return await page.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);\n } finally {\n await page.close();\n }\n } catch (error) {\n logger?.warn({ reason: messageOf(error) }, \"could not measure the remote browser\");\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,eAAsB,cACpB,SACA,QACA,QAC6B;CAC7B,OAAO,kBAAkB;EACvB,gBAAgB,MAAM,UAAU,OAAO;EACvC,OAAO,MAAM,eAAe,SAAS,MAAM;EAC3C,kBAAkB,OAAO;CAC3B,CAAC;AACH;AAEA,eAAe,UAAU,SAA+C;CACtE,IAAI;EACF,OAAO,OAAO,QAAQ,YAAY,aAAa,MAAM,QAAQ,QAAQ,IAAI,KAAA;CAC3E,QAAQ;EACN;CACF;AACF;AAEA,eAAe,eACb,SACA,QACmC;CACnC,IAAI;EACF,MAAM,OAAO,MAAM,QAAQ,QAAQ;EACnC,IAAI;GACF,OAAO,MAAM,KAAK,SAAS,aAAa,CAAC,GAAG,mBAAmB,CAAC;EAClE,UAAU;GACR,MAAM,KAAK,MAAM;EACnB;CACF,SAAS,OAAO;EACd,QAAQ,KAAK,EAAE,QAAQ,UAAU,KAAK,EAAE,GAAG,sCAAsC;EACjF;CACF;AACF"}