@storyshelf/runner-puppeteer 0.0.0-stage → 0.5.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siddhant Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,29 @@
1
- # Temporary Holding Version
1
+ # @storyshelf/runner-puppeteer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Puppeteer capture runner for StoryShelf (Chromium via chrome-headless-shell), a smaller-image alternative to the Playwright runner.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/runner-puppeteer
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/runner-puppeteer
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createPuppeteerCaptureRunner } from "@storyshelf/runner-puppeteer";
21
+
22
+ const capture = createPuppeteerCaptureRunner();
23
+ ```
24
+
25
+ See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
26
+
27
+ ## License
28
+
29
+ MIT
package/dist/a11y.mjs ADDED
@@ -0,0 +1,31 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/a11y.ts
5
+ /** Run lightweight a11y checks inside the page context. */
6
+ async function checkA11y(page) {
7
+ return await page.evaluate(inPageA11y);
8
+ }
9
+ /**
10
+ * Runs inside the browser. Playwright/Puppeteer serialize only this function's source, so it must
11
+ * not reference anything from this module (helpers, imports): split it up and the page throws
12
+ * `ReferenceError`. `a11y.test.ts` evaluates it in a scope without module bindings to guard that.
13
+ */
14
+ function inPageA11y() {
15
+ const doc = globalThis.document;
16
+ const violations = [];
17
+ const root = doc?.querySelector("#storybook-root");
18
+ if (!root) return violations;
19
+ for (const img of root.querySelectorAll("img:not([alt])")) violations.push(`img missing alt: ${img.outerHTML.slice(0, 120)}`);
20
+ for (const btn of root.querySelectorAll("button")) if (!(btn.hasAttribute("aria-label") || btn.hasAttribute("aria-labelledby") || (btn.textContent ?? "").trim() !== "")) violations.push(`button missing label: ${btn.outerHTML.slice(0, 120)}`);
21
+ for (const anchor of root.querySelectorAll("a")) if (!anchor.hasAttribute("href")) violations.push(`a missing href: ${anchor.outerHTML.slice(0, 120)}`);
22
+ for (const input of root.querySelectorAll("input, select, textarea")) {
23
+ const el = input;
24
+ if (!(el.type === "hidden" || el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby") || el.id === "" || Boolean(doc.querySelector(`label[for="${el.id}"]`)))) violations.push(`input missing label: ${el.outerHTML.slice(0, 120)}`);
25
+ }
26
+ return violations.slice(0, 10);
27
+ }
28
+ //#endregion
29
+ export { checkA11y, inPageA11y };
30
+
31
+ //# sourceMappingURL=a11y.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a11y.mjs","names":[],"sources":["../src/a11y.ts"],"sourcesContent":["/** Accessibility checks for captured pages. */\n\nimport type { Page } from \"puppeteer-core\";\n\n/** Run lightweight a11y checks inside the page context. */\nexport async function checkA11y(page: Page): Promise<string[]> {\n return await page.evaluate(inPageA11y);\n}\n\n/**\n * Runs inside the browser. Playwright/Puppeteer serialize only this function's source, so it must\n * not reference anything from this module (helpers, imports): split it up and the page throws\n * `ReferenceError`. `a11y.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, eslint/complexity -- must stay one self-contained function (see above)\nexport function inPageA11y(): string[] {\n const doc = globalThis.document;\n const violations: string[] = [];\n const root = doc?.querySelector(\"#storybook-root\");\n if (!root) return violations;\n for (const img of root.querySelectorAll(\"img:not([alt])\")) {\n violations.push(`img missing alt: ${img.outerHTML.slice(0, 120)}`);\n }\n for (const btn of root.querySelectorAll(\"button\")) {\n const labelled =\n btn.hasAttribute(\"aria-label\") ||\n btn.hasAttribute(\"aria-labelledby\") ||\n (btn.textContent ?? \"\").trim() !== \"\";\n if (!labelled) violations.push(`button missing label: ${btn.outerHTML.slice(0, 120)}`);\n }\n for (const anchor of root.querySelectorAll(\"a\")) {\n if (!anchor.hasAttribute(\"href\"))\n violations.push(`a missing href: ${anchor.outerHTML.slice(0, 120)}`);\n }\n for (const input of root.querySelectorAll(\"input, select, textarea\")) {\n const el = input as HTMLInputElement;\n const labelled =\n el.type === \"hidden\" ||\n el.hasAttribute(\"aria-label\") ||\n el.hasAttribute(\"aria-labelledby\") ||\n el.id === \"\" ||\n Boolean(doc.querySelector(`label[for=\"${el.id}\"]`));\n if (!labelled) violations.push(`input missing label: ${el.outerHTML.slice(0, 120)}`);\n }\n return violations.slice(0, 10);\n}\n"],"mappings":";;;;;AAKA,eAAsB,UAAU,MAA+B;CAC7D,OAAO,MAAM,KAAK,SAAS,UAAU;AACvC;;;;;;AAQA,SAAgB,aAAuB;CACrC,MAAM,MAAM,WAAW;CACvB,MAAM,aAAuB,CAAC;CAC9B,MAAM,OAAO,KAAK,cAAc,iBAAiB;CACjD,IAAI,CAAC,MAAM,OAAO;CAClB,KAAK,MAAM,OAAO,KAAK,iBAAiB,gBAAgB,GACtD,WAAW,KAAK,oBAAoB,IAAI,UAAU,MAAM,GAAG,GAAG,GAAG;CAEnE,KAAK,MAAM,OAAO,KAAK,iBAAiB,QAAQ,GAK9C,IAAI,EAHF,IAAI,aAAa,YAAY,KAC7B,IAAI,aAAa,iBAAiB,MACjC,IAAI,eAAe,GAAA,CAAI,KAAK,MAAM,KACtB,WAAW,KAAK,yBAAyB,IAAI,UAAU,MAAM,GAAG,GAAG,GAAG;CAEvF,KAAK,MAAM,UAAU,KAAK,iBAAiB,GAAG,GAC5C,IAAI,CAAC,OAAO,aAAa,MAAM,GAC7B,WAAW,KAAK,mBAAmB,OAAO,UAAU,MAAM,GAAG,GAAG,GAAG;CAEvE,KAAK,MAAM,SAAS,KAAK,iBAAiB,yBAAyB,GAAG;EACpE,MAAM,KAAK;EAOX,IAAI,EALF,GAAG,SAAS,YACZ,GAAG,aAAa,YAAY,KAC5B,GAAG,aAAa,iBAAiB,KACjC,GAAG,OAAO,MACV,QAAQ,IAAI,cAAc,cAAc,GAAG,GAAG,GAAG,CAAC,IACrC,WAAW,KAAK,wBAAwB,GAAG,UAAU,MAAM,GAAG,GAAG,GAAG;CACrF;CACA,OAAO,WAAW,MAAM,GAAG,EAAE;AAC/B"}
@@ -0,0 +1,34 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/browser.ts
5
+ /** Tracks in-flight renders so `cancel` can abort them. */
6
+ const activeRuns = /* @__PURE__ */ new Map();
7
+ /** Close a browser if present; `cancel` must never throw. */
8
+ async function closeBrowser(browser) {
9
+ if (!browser) return;
10
+ await tryCloseBrowser(browser);
11
+ }
12
+ async function tryCloseBrowser(browser) {
13
+ try {
14
+ await browser.close();
15
+ } catch {}
16
+ }
17
+ /** Best-effort close for the render's `finally` block. */
18
+ async function safeCloseBrowser(browser) {
19
+ await tryCloseBrowser(browser);
20
+ }
21
+ /** Best-effort close for the static server. */
22
+ async function safeCloseServer(server) {
23
+ try {
24
+ await server.close();
25
+ } catch {}
26
+ }
27
+ /** Normalize any thrown value to a string. */
28
+ function messageOf(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+ //#endregion
32
+ export { activeRuns, closeBrowser, messageOf, safeCloseBrowser, safeCloseServer };
33
+
34
+ //# sourceMappingURL=browser.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["/** Browser lifecycle and active-run tracking for Puppeteer. */\n\nimport type { Browser } from \"puppeteer-core\";\nimport type { ActiveRun } from \"./types.ts\";\n\n/** Tracks in-flight renders so `cancel` can abort them. */\nexport const activeRuns = new Map<string, ActiveRun>();\n\n/** Close a browser if present; `cancel` must never throw. */\nexport async function closeBrowser(browser: Browser | null): Promise<void> {\n if (!browser) return;\n await tryCloseBrowser(browser);\n}\n\nasync function tryCloseBrowser(browser: Browser): Promise<void> {\n try {\n await browser.close();\n } catch {\n // The run's own `finally` performs final cleanup; cancel must never throw.\n }\n}\n\n/** Best-effort close for the render's `finally` block. */\nexport async function safeCloseBrowser(browser: Browser): Promise<void> {\n await tryCloseBrowser(browser);\n}\n\n/** Best-effort close for the static server. */\nexport async function safeCloseServer(server: { close(): Promise<void> }): Promise<void> {\n try {\n await server.close();\n } catch {\n // Best-effort; teardown must never mask a render result.\n }\n}\n\n/** Normalize any thrown value to a string. */\nexport function messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;;;AAMA,MAAa,6BAAa,IAAI,IAAuB;;AAGrD,eAAsB,aAAa,SAAwC;CACzE,IAAI,CAAC,SAAS;CACd,MAAM,gBAAgB,OAAO;AAC/B;AAEA,eAAe,gBAAgB,SAAiC;CAC9D,IAAI;EACF,MAAM,QAAQ,MAAM;CACtB,QAAQ,CAER;AACF;;AAGA,eAAsB,iBAAiB,SAAiC;CACtE,MAAM,gBAAgB,OAAO;AAC/B;;AAGA,eAAsB,gBAAgB,QAAmD;CACvF,IAAI;EACF,MAAM,OAAO,MAAM;CACrB,QAAQ,CAER;AACF;;AAGA,SAAgB,UAAU,OAAwB;CAChD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
@@ -0,0 +1,16 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import "./types.mjs";
4
+ import "./runtime-params.mjs";
5
+ import { BrowserName, CaptureRunner } from "@storyshelf/core/adapter/capture-runner";
6
+ //#region src/capture-runner.d.ts
7
+ /** Create a CaptureRunner that renders Storybook stories with Puppeteer (Chromium via chrome-headless-shell). */
8
+ declare function createPuppeteerCaptureRunner(options?: {
9
+ browser?: BrowserName;
10
+ supportedBrowsers?: readonly BrowserName[];
11
+ executablePath?: string;
12
+ args?: string[];
13
+ }): CaptureRunner;
14
+ //#endregion
15
+ export { createPuppeteerCaptureRunner };
16
+ //# sourceMappingURL=capture-runner.d.mts.map
@@ -0,0 +1,42 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { activeRuns, closeBrowser } from "./browser.mjs";
5
+ import "./runtime-params.mjs";
6
+ import { renderAll } from "./pipeline.mjs";
7
+ //#region src/capture-runner.ts
8
+ /** Create a CaptureRunner that renders Storybook stories with Puppeteer (Chromium via chrome-headless-shell). */
9
+ function createPuppeteerCaptureRunner(options = {}) {
10
+ return {
11
+ metadata: {
12
+ name: "Puppeteer",
13
+ version: globalThis.__PKG_VERSION__ ?? "0.0.0",
14
+ description: "Puppeteer capture runner (chrome-headless-shell)",
15
+ kind: "puppeteer",
16
+ category: "capture-runner",
17
+ supportedBrowsers: options.supportedBrowsers ?? ["chromium", "chrome"]
18
+ },
19
+ async render(input) {
20
+ const active = {
21
+ cancelled: false,
22
+ browser: null
23
+ };
24
+ activeRuns.set(input.buildId, active);
25
+ try {
26
+ return await renderAll(input, active, options);
27
+ } finally {
28
+ activeRuns.delete(input.buildId);
29
+ }
30
+ },
31
+ async cancel(buildId) {
32
+ const active = activeRuns.get(buildId);
33
+ if (!active) return;
34
+ active.cancelled = true;
35
+ await closeBrowser(active.browser);
36
+ }
37
+ };
38
+ }
39
+ //#endregion
40
+ export { createPuppeteerCaptureRunner };
41
+
42
+ //# sourceMappingURL=capture-runner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-runner.mjs","names":[],"sources":["../src/capture-runner.ts"],"sourcesContent":["/** Puppeteer capture runner — composition root (factory wiring). */\n\nimport type { BrowserName, CaptureRunner } from \"@storyshelf/core/adapter/capture-runner\";\nimport { activeRuns, closeBrowser } from \"./browser.ts\";\nimport { renderAll } from \"./pipeline.ts\";\nimport type { ActiveRun, PuppeteerRenderInput } from \"./types.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/** Create a CaptureRunner that renders Storybook stories with Puppeteer (Chromium via chrome-headless-shell). */\nexport function createPuppeteerCaptureRunner(\n options: {\n browser?: BrowserName;\n supportedBrowsers?: readonly BrowserName[];\n executablePath?: string;\n args?: string[];\n } = {},\n): CaptureRunner {\n return {\n metadata: {\n name: \"Puppeteer\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"Puppeteer capture runner (chrome-headless-shell)\",\n kind: \"puppeteer\",\n category: \"capture-runner\",\n supportedBrowsers: options.supportedBrowsers ?? [\"chromium\", \"chrome\"],\n },\n async render(input: PuppeteerRenderInput) {\n const active: ActiveRun = { cancelled: false, browser: null };\n activeRuns.set(input.buildId, active);\n try {\n return await renderAll(input, active, options);\n } finally {\n activeRuns.delete(input.buildId);\n }\n },\n async cancel(buildId) {\n const active = activeRuns.get(buildId);\n if (!active) return;\n active.cancelled = true;\n await closeBrowser(active.browser);\n },\n };\n}\n\n// Re-exports for backward compatibility (tests import these from capture-runner).\nexport type { PuppeteerRenderInput, RuntimeParamsState } from \"./types.ts\";\nexport { createRuntimeParamsState, runtimeParametersForStory } from \"./runtime-params.ts\";\n"],"mappings":";;;;;;;;AAUA,SAAgB,6BACd,UAKI,CAAC,GACU;CACf,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;GACV,mBAAmB,QAAQ,qBAAqB,CAAC,YAAY,QAAQ;EACvE;EACA,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAoB;IAAE,WAAW;IAAO,SAAS;GAAK;GAC5D,WAAW,IAAI,MAAM,SAAS,MAAM;GACpC,IAAI;IACF,OAAO,MAAM,UAAU,OAAO,QAAQ,OAAO;GAC/C,UAAU;IACR,WAAW,OAAO,MAAM,OAAO;GACjC;EACF;EACA,MAAM,OAAO,SAAS;GACpB,MAAM,SAAS,WAAW,IAAI,OAAO;GACrC,IAAI,CAAC,QAAQ;GACb,OAAO,YAAY;GACnB,MAAM,aAAa,OAAO,OAAO;EACnC;CACF;AACF"}
@@ -0,0 +1,4 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { createPuppeteerCaptureRunner } from "./capture-runner.mjs";
4
+ export { createPuppeteerCaptureRunner };
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { createPuppeteerCaptureRunner } from "./capture-runner.mjs";
5
+ export { createPuppeteerCaptureRunner };
@@ -0,0 +1,102 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { messageOf, safeCloseBrowser, safeCloseServer } from "./browser.mjs";
5
+ import { createRuntimeParamsState } from "./runtime-params.mjs";
6
+ import { captureScreenshot } from "./screenshot.mjs";
7
+ import { createStaticServer } from "./static-server.mjs";
8
+ import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
9
+ import puppeteer from "puppeteer-core";
10
+ //#region src/pipeline.ts
11
+ /** 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);
15
+ }
16
+ async function runWithBrowser(input, active, browser, server) {
17
+ active.browser = browser;
18
+ const ctx = buildContext(browser, server.url, input);
19
+ const captures = [];
20
+ const failures = [];
21
+ try {
22
+ await runAllCaptures(input, active, ctx, captures, failures);
23
+ return {
24
+ captures,
25
+ failures
26
+ };
27
+ } finally {
28
+ active.browser = null;
29
+ await Promise.all([safeCloseBrowser(browser), safeCloseServer(server)]);
30
+ }
31
+ }
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
+ function buildContext(browser, baseUrl, input) {
40
+ return {
41
+ browser,
42
+ adapter: input.adapter ?? new StorybookAdapter(),
43
+ baseUrl,
44
+ executePlay: input.executePlay,
45
+ playTimeoutMs: input.playTimeoutMs,
46
+ runA11y: input.runA11y,
47
+ runtimeParams: createRuntimeParamsState()
48
+ };
49
+ }
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);
53
+ }
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));
56
+ }
57
+ async function captureOne(input, active, ctx, story, viewport, captures, failures) {
58
+ if (active.cancelled) throw new Error("Capture cancelled");
59
+ try {
60
+ await pushCapture(ctx, story, viewport, captures, failures, input);
61
+ } catch (error) {
62
+ pushRenderFailure(failures, input, story.id, viewport.name, error);
63
+ }
64
+ }
65
+ async function pushCapture(ctx, story, viewport, captures, failures, input) {
66
+ const result = await captureScreenshot(ctx, story, viewport);
67
+ captures.push({
68
+ story: result.story,
69
+ viewportName: viewport.name,
70
+ viewport,
71
+ screenshot: result.screenshot
72
+ });
73
+ if (result.a11yViolations.length > 0) handleA11y(input, failures, story, viewport, result.a11yViolations);
74
+ }
75
+ function handleA11y(input, failures, story, viewport, violations) {
76
+ failures.push({
77
+ storyId: story.id,
78
+ viewportName: viewport.name,
79
+ error: `a11y: ${violations.join("; ")}`
80
+ });
81
+ input.logger?.warn({
82
+ storyId: story.id,
83
+ viewport: viewport.name,
84
+ violations
85
+ }, "a11y violations found");
86
+ }
87
+ function pushRenderFailure(failures, input, storyId, viewportName, error) {
88
+ failures.push({
89
+ storyId,
90
+ viewportName,
91
+ error: messageOf(error)
92
+ });
93
+ input.logger?.error({
94
+ storyId,
95
+ viewport: viewportName,
96
+ err: error
97
+ }, "render failed for story");
98
+ }
99
+ //#endregion
100
+ export { renderAll };
101
+
102
+ //# sourceMappingURL=pipeline.mjs.map
@@ -0,0 +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"}
package/dist/play.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { messageOf } from "./browser.mjs";
5
+ //#region src/play.ts
6
+ /** Execute the story's play function with a timeout race. */
7
+ async function runPlay(page, storyId, timeoutMs) {
8
+ try {
9
+ await page.evaluate(inPagePlay, {
10
+ storyId,
11
+ timeoutMs
12
+ });
13
+ } catch (error) {
14
+ throw new Error(`play failed: ${messageOf(error)}`, { cause: error });
15
+ }
16
+ }
17
+ /**
18
+ * Runs inside the browser. Playwright serializes only this function's source, so it must not
19
+ * reference anything from this module (helpers, imports): split it up and the page throws
20
+ * `ReferenceError`. `play.test.ts` evaluates it in a scope without module bindings to guard that.
21
+ */
22
+ async function inPagePlay({ storyId, timeoutMs }) {
23
+ const preview = globalThis["__STORYBOOK_PREVIEW__"];
24
+ if (!preview) return;
25
+ let playError = null;
26
+ try {
27
+ preview.channel?.on("playFunctionThrewException", (err) => {
28
+ playError = err;
29
+ });
30
+ } catch {}
31
+ const canvasElement = globalThis.document?.querySelector("#storybook-root");
32
+ const work = preview.executePlay ? preview.executePlay(storyId) : preview.storyStore?.fromId?.(storyId)?.play?.({ canvasElement });
33
+ if (!work) return;
34
+ let timer;
35
+ const timeout = new Promise((_resolve, reject) => {
36
+ timer = setTimeout(() => {
37
+ reject(/* @__PURE__ */ new Error(`play timeout after ${timeoutMs}ms`));
38
+ }, timeoutMs);
39
+ });
40
+ try {
41
+ await Promise.race([work, timeout]);
42
+ } finally {
43
+ clearTimeout(timer);
44
+ }
45
+ if (playError) throw playError;
46
+ }
47
+ //#endregion
48
+ export { inPagePlay, runPlay };
49
+
50
+ //# sourceMappingURL=play.mjs.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import "./types.mjs";
4
+ import "@storyshelf/core/capture";
@@ -0,0 +1,60 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { mergeParameters } from "@storyshelf/core/capture";
5
+ //#region src/runtime-params.ts
6
+ /** Per-run runtime-parameter extraction cache (ADR 0017). */
7
+ /** Create the per-run runtime-parameter cache shared across captures. */
8
+ function createRuntimeParamsState() {
9
+ return {};
10
+ }
11
+ /**
12
+ * Read story parameters from the running preview via
13
+ * `__STORYBOOK_PREVIEW__.extract()`, once per run and lazily.
14
+ */
15
+ async function runtimeParametersForStory(state, page, storyId) {
16
+ return (state.extractFailed ? void 0 : await extractedParametersMap(state, page))?.get(storyId);
17
+ }
18
+ async function extractedParametersMap(state, page) {
19
+ state.extractPromise ??= loadExtractedParametersMap(state, page);
20
+ return await state.extractPromise;
21
+ }
22
+ async function loadExtractedParametersMap(state, page) {
23
+ return buildParamsMap(state, await safeReadExtracted(state, page));
24
+ }
25
+ async function safeReadExtracted(state, page) {
26
+ try {
27
+ return await readExtractedParameters(page);
28
+ } catch {
29
+ state.extractFailed = true;
30
+ return null;
31
+ }
32
+ }
33
+ function buildParamsMap(state, raw) {
34
+ const out = /* @__PURE__ */ new Map();
35
+ if (raw) fillParamsMap(out, raw);
36
+ if (out.size === 0) state.extractFailed = true;
37
+ return out;
38
+ }
39
+ function fillParamsMap(out, raw) {
40
+ for (const [id, entry] of Object.entries(raw)) {
41
+ const merged = mergeParameters(entry);
42
+ if (merged) out.set(id, merged);
43
+ }
44
+ }
45
+ async function readExtractedParameters(page) {
46
+ const map = await page.evaluate(() => {
47
+ const preview = globalThis["__STORYBOOK_PREVIEW__"];
48
+ if (!preview || typeof preview.extract !== "function") return null;
49
+ const extracted = preview.extract();
50
+ return extracted && typeof extracted === "object" ? extracted : null;
51
+ });
52
+ return hasEntries(map) ? map : null;
53
+ }
54
+ function hasEntries(map) {
55
+ return Boolean(map && Object.keys(map).length > 0);
56
+ }
57
+ //#endregion
58
+ export { createRuntimeParamsState, runtimeParametersForStory };
59
+
60
+ //# sourceMappingURL=runtime-params.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-params.mjs","names":["win"],"sources":["../src/runtime-params.ts"],"sourcesContent":["/** Per-run runtime-parameter extraction cache (ADR 0017). */\n\nimport { mergeParameters } from \"@storyshelf/core/capture\";\nimport type { StoryParameters } from \"@storyshelf/core/capture\";\nimport type { ExtractedEntry, RuntimeParamsPage, RuntimeParamsState } from \"./types.ts\";\n\n/** Create the per-run runtime-parameter cache shared across captures. */\nexport function createRuntimeParamsState(): RuntimeParamsState {\n return {};\n}\n\n/**\n * Read story parameters from the running preview via\n * `__STORYBOOK_PREVIEW__.extract()`, once per run and lazily.\n */\nexport async function runtimeParametersForStory(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n storyId: string,\n): Promise<StoryParameters | undefined> {\n const map = state.extractFailed ? undefined : await extractedParametersMap(state, page);\n return map?.get(storyId);\n}\n\nasync function extractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n state.extractPromise ??= loadExtractedParametersMap(state, page);\n return await state.extractPromise;\n}\n\nasync function loadExtractedParametersMap(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Map<string, StoryParameters>> {\n const raw = await safeReadExtracted(state, page);\n return buildParamsMap(state, raw);\n}\n\nasync function safeReadExtracted(\n state: RuntimeParamsState,\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n try {\n return await readExtractedParameters(page);\n } catch {\n state.extractFailed = true;\n return null;\n }\n}\n\nfunction buildParamsMap(\n state: RuntimeParamsState,\n raw: Record<string, ExtractedEntry> | null,\n): Map<string, StoryParameters> {\n const out = new Map<string, StoryParameters>();\n if (raw) fillParamsMap(out, raw);\n if (out.size === 0) state.extractFailed = true;\n return out;\n}\n\nfunction fillParamsMap(\n out: Map<string, StoryParameters>,\n raw: Record<string, ExtractedEntry>,\n): void {\n for (const [id, entry] of Object.entries(raw)) {\n const merged = mergeParameters(entry);\n if (merged) out.set(id, merged);\n }\n}\n\nasync function readExtractedParameters(\n page: RuntimeParamsPage,\n): Promise<Record<string, ExtractedEntry> | null> {\n const map = (await page.evaluate(() => {\n const win = globalThis as unknown as {\n __STORYBOOK_PREVIEW__?: { extract?: () => unknown };\n };\n // oxlint-disable-next-line typescript/dot-notation -- __STORYBOOK_PREVIEW__ is a Storybook global\n const preview = win[\"__STORYBOOK_PREVIEW__\"];\n if (!preview || typeof preview.extract !== \"function\") return null;\n const extracted = preview.extract();\n return extracted && typeof extracted === \"object\"\n ? (extracted as Record<string, ExtractedEntry>)\n : null;\n })) as Record<string, ExtractedEntry> | null;\n return hasEntries(map) ? map : null;\n}\n\nfunction hasEntries(map: Record<string, ExtractedEntry> | null): boolean {\n return Boolean(map && Object.keys(map).length > 0);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,2BAA+C;CAC7D,OAAO,CAAC;AACV;;;;;AAMA,eAAsB,0BACpB,OACA,MACA,SACsC;CAEtC,QADY,MAAM,gBAAgB,KAAA,IAAY,MAAM,uBAAuB,OAAO,IAAI,EAAA,EAC1E,IAAI,OAAO;AACzB;AAEA,eAAe,uBACb,OACA,MACuC;CACvC,MAAM,mBAAmB,2BAA2B,OAAO,IAAI;CAC/D,OAAO,MAAM,MAAM;AACrB;AAEA,eAAe,2BACb,OACA,MACuC;CAEvC,OAAO,eAAe,OAAO,MADX,kBAAkB,OAAO,IAAI,CACf;AAClC;AAEA,eAAe,kBACb,OACA,MACgD;CAChD,IAAI;EACF,OAAO,MAAM,wBAAwB,IAAI;CAC3C,QAAQ;EACN,MAAM,gBAAgB;EACtB,OAAO;CACT;AACF;AAEA,SAAS,eACP,OACA,KAC8B;CAC9B,MAAM,sBAAM,IAAI,IAA6B;CAC7C,IAAI,KAAK,cAAc,KAAK,GAAG;CAC/B,IAAI,IAAI,SAAS,GAAG,MAAM,gBAAgB;CAC1C,OAAO;AACT;AAEA,SAAS,cACP,KACA,KACM;CACN,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,GAAG,GAAG;EAC7C,MAAM,SAAS,gBAAgB,KAAK;EACpC,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM;CAChC;AACF;AAEA,eAAe,wBACb,MACgD;CAChD,MAAM,MAAO,MAAM,KAAK,eAAe;EAKrC,MAAM,UAAUA,WAAI;EACpB,IAAI,CAAC,WAAW,OAAO,QAAQ,YAAY,YAAY,OAAO;EAC9D,MAAM,YAAY,QAAQ,QAAQ;EAClC,OAAO,aAAa,OAAO,cAAc,WACpC,YACD;CACN,CAAC;CACD,OAAO,WAAW,GAAG,IAAI,MAAM;AACjC;AAEA,SAAS,WAAW,KAAqD;CACvE,OAAO,QAAQ,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,SAAS,CAAC;AACnD"}
@@ -0,0 +1,133 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { runtimeParametersForStory } from "./runtime-params.mjs";
5
+ import { checkA11y } from "./a11y.mjs";
6
+ import { runPlay } from "./play.mjs";
7
+ import { getScreenshotPlan } from "@storyshelf/core/capture";
8
+ //#region src/screenshot.ts
9
+ /** Capture a screenshot for one story/viewport, returning buffer + a11y violations. */
10
+ async function captureScreenshot(ctx, inputStory, viewport) {
11
+ const page = await ctx.browser.newPage();
12
+ await page.setViewport({
13
+ width: viewport.width,
14
+ height: viewport.height
15
+ });
16
+ try {
17
+ return await runCapture(page, ctx, inputStory, viewport);
18
+ } finally {
19
+ await page.close();
20
+ }
21
+ }
22
+ async function runCapture(page, ctx, inputStory, viewport) {
23
+ const story = await navigateAndEnrich(ctx, page, inputStory);
24
+ await maybeWaitForSelector(ctx, page, story);
25
+ if (ctx.executePlay) await runPlay(page, story.id, ctx.playTimeoutMs ?? 1e4);
26
+ const a11yViolations = await getA11y(ctx, page);
27
+ await settlePage(page, story);
28
+ const plan = await resolvePlan(page, viewport, story);
29
+ if (plan.viewport.width !== viewport.width || plan.viewport.height !== viewport.height) await page.setViewport({
30
+ width: plan.viewport.width,
31
+ height: plan.viewport.height
32
+ });
33
+ return {
34
+ screenshot: await burstShots(page, plan, story),
35
+ a11yViolations,
36
+ story
37
+ };
38
+ }
39
+ async function navigateAndEnrich(ctx, page, story) {
40
+ await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id), { waitUntil: "networkidle0" });
41
+ await ctx.adapter.waitForReady?.(page);
42
+ if (story.parameters) return story;
43
+ const rpPage = page;
44
+ const params = await runtimeParametersForStory(ctx.runtimeParams, rpPage, story.id);
45
+ return params ? {
46
+ ...story,
47
+ parameters: params
48
+ } : story;
49
+ }
50
+ async function maybeWaitForSelector(ctx, page, story) {
51
+ if (!ctx.adapter.screenshotSelector) return;
52
+ await page.waitForSelector(ctx.adapter.screenshotSelector, { visible: false });
53
+ await delay(story.parameters?.delay ?? 500);
54
+ if (story.parameters?.pauseAnimationAtEnd) await page.evaluate(() => {
55
+ const doc = globalThis.document;
56
+ for (const anim of doc.getAnimations()) anim.pause();
57
+ });
58
+ }
59
+ async function getA11y(ctx, page) {
60
+ if (!ctx.runA11y) return [];
61
+ try {
62
+ return await checkA11y(page);
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+ async function settlePage(page, story) {
68
+ const burst = story.parameters?.burst ?? 1;
69
+ if (story.parameters?.waitForFonts ?? burst > 1) try {
70
+ await page.evaluate(async () => {
71
+ await globalThis.document.fonts.ready;
72
+ });
73
+ } catch {}
74
+ try {
75
+ await page.evaluate(() => {
76
+ for (const vid of globalThis.document.querySelectorAll("video")) vid.pause();
77
+ });
78
+ } catch {}
79
+ }
80
+ async function resolvePlan(page, viewport, story) {
81
+ const box = await readBoundingBox(page);
82
+ return getScreenshotPlan(viewport, box, {
83
+ autoCrop: story.parameters?.autoCrop,
84
+ whitespaceThreshold: story.parameters?.autoCropThreshold,
85
+ fullPageOnOverflow: story.parameters?.fullPageOnOverflow,
86
+ minWidth: story.parameters?.minWidth,
87
+ minHeight: story.parameters?.minHeight,
88
+ maxWidth: story.parameters?.maxWidth,
89
+ maxHeight: story.parameters?.maxHeight
90
+ });
91
+ }
92
+ async function readBoundingBox(page) {
93
+ try {
94
+ const el = await page.$("#storybook-root");
95
+ if (!el) return null;
96
+ return await el.boundingBox();
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ async function burstShots(page, plan, story) {
102
+ const count = Math.max(1, Math.min(5, story.parameters?.burst ?? 1));
103
+ const shots = [];
104
+ for (let i = 0; i < count; i += 1) {
105
+ if (i > 0) await delay(100);
106
+ shots.push(await shotOne(page, plan, story));
107
+ }
108
+ const preferred = shots[Math.floor(shots.length / 2)] ?? shots[0];
109
+ if (preferred) return preferred;
110
+ return await page.screenshot({
111
+ type: "png",
112
+ ...plan.fullPage ? { fullPage: true } : {},
113
+ ...plan.clip ? { clip: plan.clip } : {}
114
+ });
115
+ }
116
+ async function shotOne(page, plan, _story) {
117
+ return await page.screenshot({
118
+ type: "png",
119
+ ...plan.fullPage ? { fullPage: true } : {},
120
+ ...plan.clip ? { clip: plan.clip } : {}
121
+ });
122
+ }
123
+ async function delay(ms) {
124
+ await new Promise((resolve) => {
125
+ setTimeout(() => {
126
+ resolve();
127
+ }, ms);
128
+ });
129
+ }
130
+ //#endregion
131
+ export { captureScreenshot };
132
+
133
+ //# sourceMappingURL=screenshot.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.mjs","names":[],"sources":["../src/screenshot.ts"],"sourcesContent":["/** Screenshot pipeline for a single story + viewport (Puppeteer). */\n\nimport type { StoryEntry, Viewport } from \"@storyshelf/core/adapter/capture-runner\";\nimport { getScreenshotPlan } from \"@storyshelf/core/capture\";\nimport type { Page } from \"puppeteer-core\";\nimport { checkA11y } from \"./a11y.ts\";\nimport { runPlay } from \"./play.ts\";\nimport { runtimeParametersForStory } from \"./runtime-params.ts\";\nimport type { CaptureContext, RuntimeParamsPage } from \"./types.ts\";\n\n/** Capture a screenshot for one story/viewport, returning buffer + a11y violations. */\nexport async function captureScreenshot(\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const page = await ctx.browser.newPage();\n await page.setViewport({ width: viewport.width, height: viewport.height });\n try {\n return await runCapture(page, ctx, inputStory, viewport);\n } finally {\n await page.close();\n }\n}\n\nasync function runCapture(\n page: Page,\n ctx: CaptureContext,\n inputStory: StoryEntry,\n viewport: Viewport,\n): Promise<{ screenshot: Buffer; a11yViolations: string[]; story: StoryEntry }> {\n const story = await navigateAndEnrich(ctx, page, inputStory);\n await maybeWaitForSelector(ctx, page, story);\n if (ctx.executePlay) await runPlay(page, story.id, ctx.playTimeoutMs ?? 10_000);\n const a11yViolations = await getA11y(ctx, page);\n await settlePage(page, story);\n const plan = await resolvePlan(page, viewport, story);\n if (plan.viewport.width !== viewport.width || plan.viewport.height !== viewport.height)\n await page.setViewport({ width: plan.viewport.width, height: plan.viewport.height });\n const screenshot = await burstShots(page, plan, story);\n return { screenshot, a11yViolations, story };\n}\n\nasync function navigateAndEnrich(\n ctx: CaptureContext,\n page: Page,\n story: StoryEntry,\n): Promise<StoryEntry> {\n await page.goto(ctx.adapter.buildUrl(ctx.baseUrl, story.id), { waitUntil: \"networkidle0\" });\n await ctx.adapter.waitForReady?.(page);\n if (story.parameters) return story;\n const rpPage = page as unknown as RuntimeParamsPage;\n const params = await runtimeParametersForStory(ctx.runtimeParams, rpPage, story.id);\n return params ? { ...story, parameters: params } : story;\n}\nasync function maybeWaitForSelector(\n ctx: CaptureContext,\n page: Page,\n story: StoryEntry,\n): Promise<void> {\n if (!ctx.adapter.screenshotSelector) return;\n await page.waitForSelector(ctx.adapter.screenshotSelector, { visible: false });\n await delay(story.parameters?.delay ?? 500);\n if (story.parameters?.pauseAnimationAtEnd) {\n await page.evaluate(() => {\n const doc = globalThis.document as unknown as {\n getAnimations: () => { pause: () => void }[];\n };\n for (const anim of doc.getAnimations()) anim.pause();\n });\n }\n}\nasync function getA11y(ctx: CaptureContext, page: Page): Promise<string[]> {\n if (!ctx.runA11y) return [];\n try {\n return await checkA11y(page);\n } catch {\n return [];\n }\n}\nasync function settlePage(page: Page, story: StoryEntry): Promise<void> {\n const burst = story.parameters?.burst ?? 1;\n const waitFonts = story.parameters?.waitForFonts ?? burst > 1;\n if (waitFonts) {\n try {\n await page.evaluate(async () => {\n await (globalThis as unknown as { document: { fonts: { ready: Promise<void> } } }).document\n .fonts.ready;\n });\n } catch {\n // best-effort\n }\n }\n try {\n await page.evaluate(() => {\n for (const vid of (globalThis as unknown as { document: Document }).document.querySelectorAll(\n \"video\",\n ))\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- narrow to HTMLVideoElement\n (vid as unknown as HTMLVideoElement).pause();\n });\n } catch {\n // best-effort\n }\n}\nasync function resolvePlan(\n page: Page,\n viewport: Viewport,\n story: StoryEntry,\n): Promise<ReturnType<typeof getScreenshotPlan>> {\n const box = await readBoundingBox(page);\n return getScreenshotPlan(viewport, box, {\n autoCrop: story.parameters?.autoCrop,\n whitespaceThreshold: story.parameters?.autoCropThreshold,\n fullPageOnOverflow: story.parameters?.fullPageOnOverflow,\n minWidth: story.parameters?.minWidth,\n minHeight: story.parameters?.minHeight,\n maxWidth: story.parameters?.maxWidth,\n maxHeight: story.parameters?.maxHeight,\n });\n}\n\nasync function readBoundingBox(\n page: Page,\n): Promise<{ x: number; y: number; width: number; height: number } | null> {\n try {\n const el = await page.$(\"#storybook-root\");\n if (!el) return null;\n return await el.boundingBox();\n } catch {\n return null;\n }\n}\nasync function burstShots(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n story: StoryEntry,\n): Promise<Buffer> {\n const count = Math.max(1, Math.min(5, story.parameters?.burst ?? 1));\n const shots: Buffer[] = [];\n for (let i = 0; i < count; i += 1) {\n if (i > 0) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n await delay(100);\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential burst\n shots.push(await shotOne(page, plan, story));\n }\n const preferred = shots[Math.floor(shots.length / 2)] ?? shots[0];\n if (preferred) return preferred;\n const fallback: unknown = await page.screenshot({\n type: \"png\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return fallback as unknown as Buffer;\n}\nasync function shotOne(\n page: Page,\n plan: ReturnType<typeof getScreenshotPlan>,\n _story: StoryEntry,\n): Promise<Buffer> {\n const shot: unknown = await page.screenshot({\n type: \"png\",\n ...(plan.fullPage ? { fullPage: true } : {}),\n ...(plan.clip ? { clip: plan.clip } : {}),\n });\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Buffer narrowing\n return shot as unknown as Buffer;\n}\n\nasync function delay(ms: number): Promise<void> {\n await new Promise<void>((resolve) => {\n setTimeout(() => {\n resolve();\n }, ms);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { isPathSafe, mimeFor } from "@storyshelf/core/utils";
5
+ import { createReadStream } from "node:fs";
6
+ import { stat } from "node:fs/promises";
7
+ import { createServer } from "node:http";
8
+ import { join, normalize } from "node:path";
9
+ //#region src/static-server.ts
10
+ /**
11
+ * Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.
12
+ *
13
+ * @param rootDir - Directory containing the built Storybook (`index.html` entry).
14
+ * @returns The server URL and a `close` function to stop it.
15
+ */
16
+ async function createStaticServer(rootDir) {
17
+ const root = normalize(rootDir);
18
+ const info = await stat(root).catch(() => null);
19
+ if (!info || !info.isDirectory()) throw new Error(`Storybook directory not found: ${rootDir}. Upload the built Storybook before capturing.`);
20
+ const server = createServer((req, res) => {
21
+ handleRequest(root, req.url ?? "/", res).catch(() => {
22
+ sendNotFound(res);
23
+ });
24
+ });
25
+ return {
26
+ url: `http://127.0.0.1:${await listen(server)}`,
27
+ close: async () => {
28
+ await closeServer(server);
29
+ }
30
+ };
31
+ }
32
+ function sendNotFound(res) {
33
+ res.writeHead(404, { "content-type": "text/plain" });
34
+ res.end("Not found");
35
+ }
36
+ async function resolveFile(root, urlPath) {
37
+ const clean = (urlPath.split("?")[0] ?? "/").replace(/^\/+/u, "");
38
+ const target = normalize(join(root, clean === "" ? "index.html" : clean));
39
+ if (!isPathSafe(root, target)) return null;
40
+ const info = await stat(target).catch(() => null);
41
+ if (!info) return null;
42
+ if (info.isDirectory()) return resolveFile(root, `${clean}/index.html`);
43
+ return info.isFile() ? target : null;
44
+ }
45
+ async function handleRequest(root, url, res) {
46
+ const file = await resolveFile(root, url);
47
+ if (!file) {
48
+ sendNotFound(res);
49
+ return;
50
+ }
51
+ res.writeHead(200, { "content-type": mimeFor(file) });
52
+ createReadStream(file).pipe(res);
53
+ }
54
+ async function listen(server) {
55
+ return await new Promise((resolve, reject) => {
56
+ server.once("error", reject);
57
+ server.listen(0, "127.0.0.1", () => {
58
+ const address = server.address();
59
+ resolve(typeof address === "object" && address !== null ? address.port : 0);
60
+ });
61
+ });
62
+ }
63
+ async function closeServer(server) {
64
+ await new Promise((resolve, reject) => {
65
+ server.close((error) => {
66
+ if (error) reject(error);
67
+ else resolve();
68
+ });
69
+ });
70
+ }
71
+ //#endregion
72
+ export { createStaticServer };
73
+
74
+ //# sourceMappingURL=static-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-server.mjs","names":[],"sources":["../src/static-server.ts"],"sourcesContent":["import { isPathSafe, mimeFor } from \"@storyshelf/core/utils\";\nimport { createReadStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { createServer, type Server, type ServerResponse } from \"node:http\";\nimport { join, normalize } from \"node:path\";\n\n/** A local HTTP server serving a built Storybook directory for capture. */\nexport interface StaticServer {\n url: string;\n close(): Promise<void>;\n}\n\n/**\n * Serve a built Storybook directory over HTTP on 127.0.0.1 for capture.\n *\n * @param rootDir - Directory containing the built Storybook (`index.html` entry).\n * @returns The server URL and a `close` function to stop it.\n */\nexport async function createStaticServer(rootDir: string): Promise<StaticServer> {\n const root = normalize(rootDir);\n const info = await stat(root).catch(() => null);\n if (!info || !info.isDirectory()) {\n throw new Error(\n `Storybook directory not found: ${rootDir}. Upload the built Storybook before capturing.`,\n );\n }\n\n const server = createServer((req, res) => {\n handleRequest(root, req.url ?? \"/\", res).catch(() => {\n sendNotFound(res);\n });\n });\n\n const port = await listen(server);\n\n return {\n url: `http://127.0.0.1:${port}`,\n close: async () => {\n await closeServer(server);\n },\n };\n}\n\nfunction sendNotFound(res: ServerResponse): void {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"Not found\");\n}\n\nasync function resolveFile(root: string, urlPath: string): Promise<string | null> {\n const clean = (urlPath.split(\"?\")[0] ?? \"/\").replace(/^\\/+/u, \"\");\n const target = normalize(join(root, clean === \"\" ? \"index.html\" : clean));\n if (!isPathSafe(root, target)) {\n return null;\n }\n const info = await stat(target).catch(() => null);\n if (!info) {\n return null;\n }\n if (info.isDirectory()) {\n return resolveFile(root, `${clean}/index.html`);\n }\n return info.isFile() ? target : null;\n}\n\nasync function handleRequest(root: string, url: string, res: ServerResponse): Promise<void> {\n const file = await resolveFile(root, url);\n if (!file) {\n sendNotFound(res);\n return;\n }\n res.writeHead(200, { \"content-type\": mimeFor(file) });\n createReadStream(file).pipe(res);\n}\n\nasync function listen(server: Server): Promise<number> {\n return await new Promise((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n resolve(typeof address === \"object\" && address !== null ? address.port : 0);\n });\n });\n}\n\nasync function closeServer(server: Server): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,eAAsB,mBAAmB,SAAwC;CAC/E,MAAM,OAAO,UAAU,OAAO;CAC9B,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI;CAC9C,IAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,GAC7B,MAAM,IAAI,MACR,kCAAkC,QAAQ,+CAC5C;CAGF,MAAM,SAAS,cAAc,KAAK,QAAQ;EACxC,cAAc,MAAM,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC,YAAY;GACnD,aAAa,GAAG;EAClB,CAAC;CACH,CAAC;CAID,OAAO;EACL,KAAK,oBAAoB,MAHR,OAAO,MAAM;EAI9B,OAAO,YAAY;GACjB,MAAM,YAAY,MAAM;EAC1B;CACF;AACF;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CACnD,IAAI,IAAI,WAAW;AACrB;AAEA,eAAe,YAAY,MAAc,SAAyC;CAChF,MAAM,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,IAAA,CAAK,QAAQ,SAAS,EAAE;CAChE,MAAM,SAAS,UAAU,KAAK,MAAM,UAAU,KAAK,eAAe,KAAK,CAAC;CACxE,IAAI,CAAC,WAAW,MAAM,MAAM,GAC1B,OAAO;CAET,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,YAAY,IAAI;CAChD,IAAI,CAAC,MACH,OAAO;CAET,IAAI,KAAK,YAAY,GACnB,OAAO,YAAY,MAAM,GAAG,MAAM,YAAY;CAEhD,OAAO,KAAK,OAAO,IAAI,SAAS;AAClC;AAEA,eAAe,cAAc,MAAc,KAAa,KAAoC;CAC1F,MAAM,OAAO,MAAM,YAAY,MAAM,GAAG;CACxC,IAAI,CAAC,MAAM;EACT,aAAa,GAAG;EAChB;CACF;CACA,IAAI,UAAU,KAAK,EAAE,gBAAgB,QAAQ,IAAI,EAAE,CAAC;CACpD,iBAAiB,IAAI,CAAC,CAAC,KAAK,GAAG;AACjC;AAEA,eAAe,OAAO,QAAiC;CACrD,OAAO,MAAM,IAAI,SAAS,SAAS,WAAW;EAC5C,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,GAAG,mBAAmB;GAClC,MAAM,UAAU,OAAO,QAAQ;GAC/B,QAAQ,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,CAAC;EAC5E,CAAC;CACH,CAAC;AACH;AAEA,eAAe,YAAY,QAA+B;CACxD,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,OAAO,OAAO,UAAU;GACtB,IAAI,OACF,OAAO,KAAK;QAEZ,QAAQ;EAEZ,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,6 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import "@storyshelf/core/capture";
4
+ import "puppeteer-core";
5
+ import { BrowserName } from "@storyshelf/core/adapter/capture-runner";
6
+ import "@storyshelf/core/logger";
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@storyshelf/runner-puppeteer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.3",
4
+ "description": "Puppeteer CaptureRunner for StoryShelf (Chromium via chrome-headless-shell, smaller image).",
5
+ "keywords": [
6
+ "puppeteer",
7
+ "screenshot",
8
+ "self-hosted",
9
+ "storybook",
10
+ "visual-testing"
11
+ ],
12
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Siddhant Gupta",
19
+ "url": "https://guptasiddhant.com"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
24
+ "directory": "packages/runner-puppeteer"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "types": "./dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "source": "./src/index.ts",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "exports": {
41
+ ".": "./dist/index.mjs",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "dev": "tsdown -w",
49
+ "fmt": "oxfmt ./src",
50
+ "lint": "oxlint --type-aware --type-check ./src",
51
+ "test": "vitest run",
52
+ "prepublishOnly": "nub run build"
53
+ },
54
+ "dependencies": {
55
+ "@storyshelf/core": "0.5.3",
56
+ "puppeteer-core": "^24.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@storyshelf/app": "0.5.3",
60
+ "@storyshelf/db-sqlite": "0.5.3",
61
+ "@storyshelf/storage-local": "0.5.3",
62
+ "@types/node": "^26.0.0",
63
+ "@vitest/coverage-v8": "^4.0.0",
64
+ "adm-zip": "0.6.0",
65
+ "oxfmt": "^0.65.0",
66
+ "oxlint": "^1.80.0",
67
+ "oxlint-tsgolint": "^7.0.2001",
68
+ "tsdown": "^0.22.14",
69
+ "typescript": "^7.0.2",
70
+ "vitest": "^5.0.0"
71
+ },
72
+ "jsr": {
73
+ "runtimeCompat": {
74
+ "node": true,
75
+ "deno": true,
76
+ "browser": false,
77
+ "workerd": false,
78
+ "bun": true
79
+ }
80
+ }
81
+ }