@storyshelf/runner-puppeteer 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/a11y.mjs +7 -5
- package/dist/a11y.mjs.map +1 -1
- package/dist/availability.mjs +1 -2
- package/dist/browser.mjs +24 -15
- package/dist/browser.mjs.map +1 -1
- package/dist/capture-runner.d.mts +0 -2
- package/dist/capture-runner.mjs +26 -11
- package/dist/capture-runner.mjs.map +1 -1
- package/dist/connect.mjs +21 -31
- package/dist/connect.mjs.map +1 -1
- package/dist/launch.mjs +3 -4
- package/dist/launch.mjs.map +1 -1
- package/dist/pages.d.mts +1 -0
- package/dist/pages.mjs +14 -13
- package/dist/pages.mjs.map +1 -1
- package/dist/pipeline.mjs +85 -43
- package/dist/pipeline.mjs.map +1 -1
- package/dist/play.mjs +1 -2
- package/dist/remote/cdp.d.mts +38 -13
- package/dist/remote/cdp.mjs +23 -9
- package/dist/remote/cdp.mjs.map +1 -1
- package/dist/remote/index.d.mts +3 -3
- package/dist/remote/index.mjs +2 -1
- package/dist/remote-environment.mjs +37 -21
- package/dist/remote-environment.mjs.map +1 -1
- package/dist/runtime-params.mjs +1 -2
- package/dist/screenshot.mjs +38 -6
- package/dist/screenshot.mjs.map +1 -1
- package/dist/session.d.mts +3 -0
- package/dist/session.mjs +141 -0
- package/dist/session.mjs.map +1 -0
- package/dist/settle.mjs +15 -8
- package/dist/settle.mjs.map +1 -1
- package/dist/static-server.mjs +2 -4
- package/dist/static-server.mjs.map +1 -1
- package/dist/targets.d.mts +0 -2
- package/dist/targets.mjs +1 -2
- package/dist/tests.mjs +23 -18
- package/dist/tests.mjs.map +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +8 -8
package/dist/pipeline.mjs
CHANGED
|
@@ -1,40 +1,73 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { messageOf,
|
|
4
|
+
import { abortIfCancelled, finishing, messageOf, outcomeOf, safeCloseServer } from "./browser.mjs";
|
|
5
5
|
import { PagePool } from "./pages.mjs";
|
|
6
6
|
import { measureRemote } from "./remote-environment.mjs";
|
|
7
7
|
import { createRuntimeParamsState } from "./runtime-params.mjs";
|
|
8
8
|
import { captureScreenshot } from "./screenshot.mjs";
|
|
9
|
+
import { createBrowserSession } from "./session.mjs";
|
|
9
10
|
import { createStaticServer } from "./static-server.mjs";
|
|
10
11
|
import { TargetLaunchError, engineOf } from "@storyshelf/core/adapter/capture-runner";
|
|
11
12
|
import { DEFAULT_RECYCLE_BROWSER_EVERY, chunk, createPageLimiter, describeSizing, detectCaptureEnvironment, isTimeoutError, sessionGateFor, storybookBase } from "@storyshelf/core/utils";
|
|
12
13
|
import { StorybookAdapter, resolveStoryViewports } from "@storyshelf/core/capture";
|
|
13
|
-
|
|
14
|
+
// src/pipeline.ts
|
|
14
15
|
/** Render orchestration: static server, browser, and per-story captures. */
|
|
15
16
|
/** Render all story/viewport combinations for a build. */
|
|
16
17
|
async function renderAll(rawInput, active, driver, defaultTarget) {
|
|
17
18
|
const { input, name, remote } = planRun(rawInput, driver, defaultTarget);
|
|
18
19
|
assertReachable(name, remote, input.storybookUrl);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const relaunch = async () => createBrowserSession(await driver.launch(name, input.logger, launchContextOf(input, "render", active.abort?.signal)));
|
|
21
|
+
const session = await relaunch();
|
|
22
|
+
active.session = session;
|
|
23
|
+
await abortIfCancelled(active, session);
|
|
24
|
+
let server;
|
|
25
|
+
let ctx;
|
|
26
|
+
return await finishing(async () => {
|
|
27
|
+
server = await serve(input, session);
|
|
28
|
+
input.logger?.info(describeSizing({
|
|
29
|
+
...input,
|
|
30
|
+
browser: engineOf(name)
|
|
31
|
+
}), "capture sizing");
|
|
32
|
+
const environment = await measureEnvironment(session, remote, input, active.abort?.signal);
|
|
33
|
+
ctx = buildContext(session, server.url, input);
|
|
34
|
+
const { captures, failures } = await runCaptures(input, active, ctx, relaunch);
|
|
35
|
+
for (const row of [...captures, ...failures]) row.browser = name;
|
|
36
|
+
return {
|
|
37
|
+
captures,
|
|
38
|
+
failures,
|
|
39
|
+
environment
|
|
40
|
+
};
|
|
41
|
+
}, async (failed, error) => {
|
|
42
|
+
await ctx?.pagePool?.dispose();
|
|
43
|
+
const current = active.session ?? session;
|
|
44
|
+
active.session = null;
|
|
45
|
+
await Promise.all([current.release(outcomeOf(active, failed), error), closeServer(server)]);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function closeServer(server) {
|
|
49
|
+
if (server !== void 0) await safeCloseServer(server);
|
|
50
|
+
}
|
|
51
|
+
/** What a remote service's `connect` is told about the run a browser is for. */
|
|
52
|
+
function launchContextOf(input, purpose, signal) {
|
|
28
53
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
buildId: input.buildId,
|
|
55
|
+
runId: input.runId ?? input.buildId,
|
|
56
|
+
shardIndex: input.shardIndex,
|
|
57
|
+
purpose,
|
|
58
|
+
signal
|
|
31
59
|
};
|
|
32
60
|
}
|
|
33
61
|
/** The input with its target resolved, and the session gate when the target is remote. */
|
|
34
62
|
function planRun(rawInput, driver, defaultTarget) {
|
|
35
63
|
const name = rawInput.target ?? defaultTarget;
|
|
36
64
|
const remote = driver.remote(name);
|
|
37
|
-
const gate = remote && sessionGateFor(name, remote.remote.sessions)
|
|
65
|
+
const gate = remote && sessionGateFor(name, remote.remote.sessions, (value) => {
|
|
66
|
+
rawInput.logger?.warn({
|
|
67
|
+
target: name,
|
|
68
|
+
value: String(value)
|
|
69
|
+
}, "sessions() returned an invalid value; keeping the last good one");
|
|
70
|
+
});
|
|
38
71
|
return {
|
|
39
72
|
input: {
|
|
40
73
|
...rawInput,
|
|
@@ -58,8 +91,8 @@ function assertStorybookRoot(target, storybookUrl) {
|
|
|
58
91
|
throw new TargetLaunchError(target, error instanceof Error ? error.message : String(error));
|
|
59
92
|
}
|
|
60
93
|
}
|
|
61
|
-
/** The loopback server, or the URL given; a server that cannot start
|
|
62
|
-
async function serve(input,
|
|
94
|
+
/** The loopback server, or the URL given; a server that cannot start releases the browser it was for. */
|
|
95
|
+
async function serve(input, session) {
|
|
63
96
|
if (input.storybookUrl !== void 0) return {
|
|
64
97
|
url: storybookBase(input.storybookUrl),
|
|
65
98
|
close: async () => {
|
|
@@ -67,35 +100,32 @@ async function serve(input, browser) {
|
|
|
67
100
|
}
|
|
68
101
|
};
|
|
69
102
|
return await createStaticServer(input.storybookDir).catch(async (error) => {
|
|
70
|
-
await
|
|
103
|
+
await session.release("failed", error);
|
|
71
104
|
throw error;
|
|
72
105
|
});
|
|
73
106
|
}
|
|
74
107
|
/** This host's environment: a local browser renders on the machine the runner runs on. */
|
|
75
|
-
async function
|
|
108
|
+
async function measureEnvironment(session, remote, input, signal) {
|
|
109
|
+
if (remote) return await measureRemote(session, remote.remote, {
|
|
110
|
+
logger: input.logger,
|
|
111
|
+
signal
|
|
112
|
+
});
|
|
113
|
+
const browser = session.browser;
|
|
76
114
|
const version = typeof browser.version === "function" ? await browser.version() : void 0;
|
|
77
115
|
return await detectCaptureEnvironment(version);
|
|
78
116
|
}
|
|
79
|
-
async function
|
|
80
|
-
active.browser = browser;
|
|
81
|
-
const ctx = buildContext(browser, server.url, input);
|
|
117
|
+
async function runCaptures(input, active, ctx, relaunch) {
|
|
82
118
|
const captures = [];
|
|
83
119
|
const failures = [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
} finally {
|
|
91
|
-
active.browser = null;
|
|
92
|
-
await ctx.pagePool?.dispose();
|
|
93
|
-
await Promise.all([safeCloseBrowser(ctx.browser), safeCloseServer(server)]);
|
|
94
|
-
}
|
|
120
|
+
await runAllCaptures(input, active, ctx, captures, failures, relaunch);
|
|
121
|
+
return {
|
|
122
|
+
captures,
|
|
123
|
+
failures
|
|
124
|
+
};
|
|
95
125
|
}
|
|
96
|
-
function buildContext(
|
|
126
|
+
function buildContext(session, baseUrl, input) {
|
|
97
127
|
return {
|
|
98
|
-
|
|
128
|
+
session,
|
|
99
129
|
adapter: input.adapter ?? new StorybookAdapter(),
|
|
100
130
|
baseUrl,
|
|
101
131
|
navigationTimeoutMs: input.navigationTimeoutMs,
|
|
@@ -104,7 +134,7 @@ function buildContext(browser, baseUrl, input) {
|
|
|
104
134
|
runA11y: input.runA11y,
|
|
105
135
|
...input.logger ? { logger: input.logger } : {},
|
|
106
136
|
runtimeParams: createRuntimeParamsState(),
|
|
107
|
-
...input.reusePages ? { pagePool: new PagePool(
|
|
137
|
+
...input.reusePages ? { pagePool: new PagePool(session) } : {}
|
|
108
138
|
};
|
|
109
139
|
}
|
|
110
140
|
async function runAllCaptures(input, active, ctx, captures, failures, relaunch) {
|
|
@@ -135,16 +165,29 @@ async function runBatches(batches, pages, input, active, ctx, out) {
|
|
|
135
165
|
await pages.each(batch, async (job) => await captureOne(input, active, ctx, job.story, job.viewport, out));
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
|
-
/** Swap in a fresh browser; the old one is
|
|
168
|
+
/** Swap in a fresh browser; the old one is released first so two never coexist. */
|
|
139
169
|
async function recycleBrowser(active, ctx, relaunch, logger) {
|
|
140
|
-
|
|
170
|
+
assertNotCancelled(active);
|
|
141
171
|
await ctx.pagePool?.dispose();
|
|
142
|
-
await
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
active.browser = ctx.browser;
|
|
172
|
+
await ctx.session.release(active.cancelled ? "cancelled" : "finished");
|
|
173
|
+
assertNotCancelled(active);
|
|
174
|
+
await acquireReplacement(active, ctx, relaunch);
|
|
146
175
|
logger?.info({}, "browser recycled");
|
|
147
176
|
}
|
|
177
|
+
function assertNotCancelled(active) {
|
|
178
|
+
if (active.cancelled) throw new Error("Capture cancelled");
|
|
179
|
+
}
|
|
180
|
+
async function acquireReplacement(active, ctx, relaunch) {
|
|
181
|
+
const next = await relaunch();
|
|
182
|
+
if (active.cancelled) {
|
|
183
|
+
await next.release("cancelled");
|
|
184
|
+
throw new Error("Capture cancelled");
|
|
185
|
+
}
|
|
186
|
+
ctx.session = next;
|
|
187
|
+
if (ctx.pagePool) ctx.pagePool = new PagePool(next);
|
|
188
|
+
active.session = next;
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
148
191
|
/** Capture one story x viewport; resolves to whether it timed out (failures are recorded, not thrown). */
|
|
149
192
|
async function captureOne(input, active, ctx, story, viewport, out) {
|
|
150
193
|
if (active.cancelled) throw new Error("Capture cancelled");
|
|
@@ -193,7 +236,6 @@ function pushRenderFailure(failures, input, storyId, viewportName, error) {
|
|
|
193
236
|
err: error
|
|
194
237
|
}, "render failed for story");
|
|
195
238
|
}
|
|
196
|
-
|
|
197
|
-
export { assertReachable, planRun, renderAll, serve };
|
|
239
|
+
export { assertReachable, launchContextOf, planRun, renderAll, serve };
|
|
198
240
|
|
|
199
241
|
//# sourceMappingURL=pipeline.mjs.map
|
package/dist/pipeline.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["/** Render orchestration: static server, browser, and per-story captures. */\n\nimport {\n TargetLaunchError,\n engineOf,\n type CaptureEnvironment,\n type CaptureTarget,\n type RemoteTarget,\n type RenderResult,\n type RenderedSnapshot,\n type StoryEntry,\n type 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":";;;;;;;;;;;;;;;AA+CA,eAAsB,UACpB,UACA,QACA,QACA,eACuB;CACvB,MAAM,EAAE,OAAO,MAAM,WAAW,QAAQ,UAAU,QAAQ,aAAa;CACvE,gBAAgB,MAAM,QAAQ,MAAM,YAAY;CAEhD,MAAM,UAAU,MAAM,OAAO,OAAO,MAAM,MAAM,MAAM;CACtD,MAAM,SAAS,MAAM,MAAM,OAAO,OAAO;CACzC,MAAM,QAAQ,KAAK,eAAe;EAAE,GAAG;EAAO,SAAS,SAAS,IAAI;CAAE,CAAC,GAAG,gBAAgB;CAE1F,MAAM,cAAc,SAChB,MAAM,cAAc,SAAS,OAAO,QAAQ,MAAM,MAAM,IACxD,MAAM,iBAAiB,OAAO;CAClC,MAAM,SAAS,MAAM,eACnB,OACA,QACA,SACA,QACA,YAAY,MAAM,OAAO,OAAO,MAAM,MAAM,MAAM,CACpD;CAEA,KAAK,MAAM,OAAO,CAAC,GAAG,OAAO,UAAU,GAAG,OAAO,QAAQ,GAAG,IAAI,UAAU;CAC1E,OAAO;EAAE,GAAG;EAAQ;CAAY;AAClC;;AAGA,SAAgB,QACd,UACA,QACA,eAKA;CACA,MAAM,OAAO,SAAS,UAAU;CAChC,MAAM,SAAS,OAAO,OAAO,IAAI;CACjC,MAAM,OAAO,UAAU,eAAe,MAAM,OAAO,OAAO,QAAQ;CAClE,OAAO;EAAE,OAAO;GAAE,GAAG;GAAU,QAAQ;GAAM;EAAK;EAAG;EAAM;CAAO;AACpE;;AAGA,SAAgB,gBACd,QACA,QACA,cACM;CACN,IAAI,QAAQ,UAAU,SAAS,iBAAiB,KAAA,GAC9C,MAAM,IAAI,kBACR,QACA,gIACA,EAAE,WAAW,KAAK,CACpB;CAEF,IAAI,iBAAiB,KAAA,GACnB,oBAAoB,QAAQ,YAAY;AAE5C;;AAGA,SAAS,oBAAoB,QAAuB,cAA4B;CAC9E,IAAI;EACF,cAAc,YAAY;CAC5B,SAAS,OAAO;EACd,MAAM,IAAI,kBAAkB,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;CAC5F;AACF;;AAGA,eAAsB,MACpB,OACA,SACkB;CAClB,IAAI,MAAM,iBAAiB,KAAA,GACzB,OAAO;EACL,KAAK,cAAc,MAAM,YAAY;EACrC,OAAO,YAAY;GACjB,MAAM,QAAQ,QAAQ;EACxB;CACF;CAEF,OAAO,MAAM,mBAAmB,MAAM,YAAY,CAAC,CAAC,MAAM,OAAO,UAAmB;EAClF,MAAM,iBAAiB,OAAO;EAC9B,MAAM;CACR,CAAC;AACH;;AAGA,eAAe,iBAAiB,SAA+C;CAC7E,MAAM,UAAU,OAAO,QAAQ,YAAY,aAAa,MAAM,QAAQ,QAAQ,IAAI,KAAA;CAClF,OAAO,MAAM,yBAAyB,OAAO;AAC/C;AAEA,eAAe,eACb,OACA,QACA,SACA,QACA,UACuB;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,UAAU,QAAQ;EACrE,OAAO;GAAE;GAAU;EAAS;CAC9B,UAAU;EACR,OAAO,UAAU;EACjB,MAAM,IAAI,UAAU,QAAQ;EAE5B,MAAM,QAAQ,IAAI,CAAC,iBAAiB,IAAI,OAAO,GAAG,gBAAgB,MAAM,CAAC,CAAC;CAC5E;AACF;AAEA,SAAS,aACP,SACA,SACA,OACgB;CAChB,OAAO;EACL;EACA,SAAS,MAAM,WAAW,IAAI,iBAAiB;EAC/C;EACA,qBAAqB,MAAM;EAC3B,aAAa,MAAM;EACnB,eAAe,MAAM;EACrB,SAAS,MAAM;EACf,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C,eAAe,yBAAyB;EACxC,GAAI,MAAM,aAAa,EAAE,UAAU,IAAI,SAAS,OAAO,EAAE,IAAI,CAAC;CAChE;AACF;AAEA,eAAe,eACb,OACA,QACA,KACA,UACA,UACA,UACe;CAEf,MAAM,OAAO,MAAM,QAAQ,SAAS,UAClC,sBAAsB,OAAO,MAAM,SAAS,CAAC,CAAC,KAAK,cAAc;EAAE;EAAO;CAAS,EAAE,CACvF;CAEA,MAAM,UAAU,MAAM,MAAM,MAAM,uBAAuB,6BAA6B;CACtF,MAAM,QAAQ,kBAAkB;EAC9B,aAAa,MAAM;EACnB,GAAI,MAAM,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,MAAM,KAAK;EACvD,SAAS,SAAS,MAAM,UAAU,UAAU;EAC5C,QAAQ,MAAM;CAChB,CAAC;CACD,IAAI;EACF,MAAM,WAAW,SAAS,OAAO,OAAO,QAAQ,KAAK;GAAE;GAAU;GAAU;EAAS,CAAC;CACvF,UAAU;EACR,MAAM,QAAQ;CAChB;AACF;AAEA,eAAe,WACb,SACA,OACA,OACA,QACA,KACA,KAKe;CACf,KAAK,MAAM,CAAC,OAAO,UAAU,QAAQ,QAAQ,GAAG;EAC9C,IAAI,QAAQ,GAEV,MAAM,eAAe,QAAQ,KAAK,IAAI,UAAU,MAAM,MAAM;EAG9D,MAAM,MAAM,KACV,OACA,OAAO,QAAQ,MAAM,WAAW,OAAO,QAAQ,KAAK,IAAI,OAAO,IAAI,UAAU,GAAG,CAClF;CACF;AACF;;AAGA,eAAe,eACb,QACA,KACA,UACA,QACe;CACf,IAAI,OAAO,WAAW,MAAM,IAAI,MAAM,mBAAmB;CACzD,MAAM,IAAI,UAAU,QAAQ;CAC5B,MAAM,iBAAiB,IAAI,OAAO;CAClC,IAAI,UAAU,MAAM,SAAS;CAC7B,IAAI,IAAI,UAAU,IAAI,WAAW,IAAI,SAAS,IAAI,OAAO;CACzD,OAAO,UAAU,IAAI;CACrB,QAAQ,KAAK,CAAC,GAAG,kBAAkB;AACrC;;AAGA,eAAe,WACb,OACA,QACA,KACA,OACA,UACA,KACkB;CAClB,IAAI,OAAO,WAAW,MAAM,IAAI,MAAM,mBAAmB;CACzD,IAAI;EACF,MAAM,YAAY,KAAK,OAAO,UAAU,IAAI,UAAU,IAAI,UAAU,KAAK;EACzE,OAAO;CACT,SAAS,OAAO;EACd,kBAAkB,IAAI,UAAU,OAAO,MAAM,IAAI,SAAS,MAAM,KAAK;EACrE,OAAO,eAAe,KAAK;CAC7B;AACF;AAEA,eAAe,YACb,KACA,OACA,UACA,UACA,UACA,OACe;CACf,MAAM,SAAS,MAAM,kBAAkB,KAAK,OAAO,QAAQ;CAC3D,MAAM,WAA6B;EACjC,OAAO,OAAO;EACd,cAAc,SAAS;EACvB;EACA,YAAY,OAAO;EACnB,GAAI,OAAO,eAAe,SAAS,IAAI,EAAE,MAAM,OAAO,eAAe,IAAI,CAAC;CAC5E;CAEA,IAAI,MAAM,WAAW,MAAM,MAAM,UAAU,QAAQ;MAC9C,SAAS,KAAK,QAAQ;CAC3B,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 { abortIfCancelled, finishing, messageOf, outcomeOf, 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 { createBrowserSession, type BrowserSession } from \"./session.ts\";\nimport { createStaticServer } from \"./static-server.ts\";\nimport type {\n ActiveRun,\n CaptureContext,\n LaunchContext,\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 const relaunch = async (): Promise<BrowserSession> =>\n createBrowserSession(\n await driver.launch(\n name,\n input.logger,\n launchContextOf(input, \"render\", active.abort?.signal),\n ),\n );\n // Launch first: a target that will not start must fail before a static server is bound.\n const session = await relaunch();\n active.session = session;\n await abortIfCancelled(active, session);\n let server: Serving | undefined;\n let ctx: CaptureContext | undefined;\n return await finishing(\n async () => {\n server = await serve(input, session);\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 = await measureEnvironment(session, remote, input, active.abort?.signal);\n ctx = buildContext(session, server.url, input);\n const { captures, failures } = await runCaptures(input, active, ctx, relaunch);\n // Echo the target so persisted rows are attributable without the request.\n for (const row of [...captures, ...failures]) row.browser = name;\n return { captures, failures, environment };\n },\n async (failed, error) => {\n await ctx?.pagePool?.dispose();\n const current = active.session ?? session;\n active.session = null;\n await Promise.all([current.release(outcomeOf(active, failed), error), closeServer(server)]);\n },\n );\n}\n\nasync function closeServer(server: Serving | undefined): Promise<void> {\n if (server !== undefined) await safeCloseServer(server);\n}\n\n/** What a remote service's `connect` is told about the run a browser is for. */\nexport function launchContextOf(\n input: { buildId: string; runId?: string | undefined; shardIndex?: number | undefined },\n purpose: LaunchContext[\"purpose\"],\n signal?: AbortSignal,\n): LaunchContext {\n return {\n buildId: input.buildId,\n runId: input.runId ?? input.buildId,\n shardIndex: input.shardIndex,\n purpose,\n signal,\n };\n}\n\n/** The input with its target resolved, and the session gate when the target is remote. */\nexport function planRun<\n T extends { target?: CaptureTarget | undefined; logger?: PuppeteerRenderInput[\"logger\"] },\n>(\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 =\n remote &&\n sessionGateFor(name, remote.remote.sessions, (value) => {\n rawInput.logger?.warn(\n { target: name, value: String(value) },\n \"sessions() returned an invalid value; keeping the last good one\",\n );\n });\n return { 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 releases the browser it was for. */\nexport async function serve(\n input: Pick<RunInput, \"storybookDir\" | \"storybookUrl\">,\n session: BrowserSession,\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 session.release(\"failed\", error);\n throw error;\n });\n}\n\n/** This host's environment: a local browser renders on the machine the runner runs on. */\nasync function measureEnvironment(\n session: BrowserSession,\n remote: RemoteTarget | undefined,\n input: RunInput,\n signal: AbortSignal | undefined,\n): Promise<CaptureEnvironment> {\n if (remote) return await measureRemote(session, remote.remote, { logger: input.logger, signal });\n const browser: Browser = session.browser;\n const version = typeof browser.version === \"function\" ? await browser.version() : undefined;\n return await detectCaptureEnvironment(version);\n}\n\nasync function runCaptures(\n input: RunInput,\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<BrowserSession>,\n): Promise<Pick<RenderResult, \"captures\" | \"failures\">> {\n const captures: RenderedSnapshot[] = [];\n const failures: RenderResult[\"failures\"] = [];\n await runAllCaptures(input, active, ctx, captures, failures, relaunch);\n return { captures, failures };\n}\n\nfunction buildContext(\n session: BrowserSession,\n baseUrl: string,\n input: PuppeteerRenderInput,\n): CaptureContext {\n return {\n session,\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(session) } : {}),\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<BrowserSession>,\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<BrowserSession>;\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 released first so two never coexist. */\nasync function recycleBrowser(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<BrowserSession>,\n logger: PuppeteerRenderInput[\"logger\"],\n): Promise<void> {\n assertNotCancelled(active);\n await ctx.pagePool?.dispose();\n const previous = ctx.session;\n await previous.release(active.cancelled ? \"cancelled\" : \"finished\");\n assertNotCancelled(active);\n await acquireReplacement(active, ctx, relaunch);\n logger?.info({}, \"browser recycled\");\n}\n\nfunction assertNotCancelled(active: ActiveRun): void {\n if (active.cancelled) throw new Error(\"Capture cancelled\");\n}\n\nasync function acquireReplacement(\n active: ActiveRun,\n ctx: CaptureContext,\n relaunch: () => Promise<BrowserSession>,\n): Promise<BrowserSession> {\n const next = await relaunch();\n if (active.cancelled) {\n await next.release(\"cancelled\");\n throw new Error(\"Capture cancelled\");\n }\n ctx.session = next;\n if (ctx.pagePool) ctx.pagePool = new PagePool(next);\n active.session = next;\n return next;\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
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { messageOf } from "./browser.mjs";
|
|
5
|
-
|
|
5
|
+
// src/play.ts
|
|
6
6
|
/** Execute the story's play function with a timeout race. */
|
|
7
7
|
async function runPlay(page, storyId, timeoutMs) {
|
|
8
8
|
try {
|
|
@@ -57,7 +57,6 @@ async function inPagePlay({ storyId, timeoutMs }) {
|
|
|
57
57
|
}
|
|
58
58
|
if (playError) throw playError;
|
|
59
59
|
}
|
|
60
|
-
//#endregion
|
|
61
60
|
export { inPagePlay, runPlay };
|
|
62
61
|
|
|
63
62
|
//# sourceMappingURL=play.mjs.map
|
package/dist/remote/cdp.d.mts
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { BrowserName, RemoteBrowser } from "@storyshelf/core/adapter/capture-runner";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
interface CdpOptions {
|
|
3
|
+
import { BrowserName, RemoteBrowser, RemoteOptions } from "@storyshelf/core/adapter/capture-runner";
|
|
4
|
+
/** What both forms of {@link cdp} share. */
|
|
5
|
+
interface CdpBase {
|
|
7
6
|
/** The target name half (`chromium@<name>`): lowercase letters, digits and hyphens, at most 32. */
|
|
8
7
|
name: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
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;
|
|
8
|
+
/** Parallel pages the service allows (default 1: set it to the plan's cap). A function is read live. */
|
|
9
|
+
sessions?: number | (() => number);
|
|
15
10
|
/** Engines to offer (default `chromium`; `chrome` when the service is real Chrome). */
|
|
16
11
|
engines?: readonly BrowserName[];
|
|
17
12
|
/** OS the service runs on, reported when the page cannot tell. */
|
|
18
13
|
platform?: string;
|
|
19
14
|
/** How long one connect attempt waits in ms (default 30 000). */
|
|
20
15
|
connectTimeoutMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Maximum connections this process holds to the service at once (default: no cap), separate from
|
|
18
|
+
* the pages in `sessions`. Renders over the cap wait; per process, not per deployment.
|
|
19
|
+
*/
|
|
20
|
+
maxConnections?: number;
|
|
21
|
+
}
|
|
22
|
+
/** A service at a fixed address. */
|
|
23
|
+
interface CdpFixedOptions extends CdpBase {
|
|
24
|
+
/** `ws(s)://…`, or `http(s)://…` of a Chrome with remote debugging (its WebSocket is looked up). May carry a key: it is never logged. */
|
|
25
|
+
endpoint: string;
|
|
26
|
+
/** Handshake headers, such as `authorization`. Never logged. */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
connect?: undefined;
|
|
29
|
+
release?: undefined;
|
|
21
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A service that must be woken, started or chosen per connection: `connect` receives the run's
|
|
33
|
+
* context (`buildId`, `runId`, `shardIndex`, `purpose`, `attempt`, `signal`) and returns
|
|
34
|
+
* `{ endpoint, headers, lease }`; `release` gets the lease back once, when the render ends.
|
|
35
|
+
*/
|
|
36
|
+
interface CdpLeasedOptions<TLease> extends CdpBase {
|
|
37
|
+
connect: Extract<RemoteOptions<TLease>["connect"], (...args: never[]) => unknown>;
|
|
38
|
+
release?: RemoteOptions<TLease>["release"];
|
|
39
|
+
/** Retries after the first connect attempt (default 3) and the first backoff in ms (default 500). */
|
|
40
|
+
retry?: RemoteOptions<TLease>["retry"];
|
|
41
|
+
/** `acquireMs` bounds `connect`, `connectMs` the handshake, `releaseMs` `release` (all default 30 000). */
|
|
42
|
+
timeouts?: RemoteOptions<TLease>["timeouts"];
|
|
43
|
+
endpoint?: undefined;
|
|
44
|
+
headers?: undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Options of {@link cdp}. */
|
|
47
|
+
type CdpOptions<TLease = unknown> = CdpFixedOptions | CdpLeasedOptions<TLease>;
|
|
22
48
|
/**
|
|
23
49
|
* Targets on a CDP service, for example `chromium@cloudflare`. The browser cannot route back to
|
|
24
50
|
* this host, so every target needs a signed Storybook URL (ADR 0007, decision 5).
|
|
25
51
|
*/
|
|
26
|
-
declare function cdp(options: CdpOptions): RemoteBrowser;
|
|
27
|
-
|
|
28
|
-
export { CdpOptions, cdp };
|
|
52
|
+
declare function cdp<TLease = unknown>(options: CdpOptions<TLease>): RemoteBrowser;
|
|
53
|
+
export { CdpFixedOptions, CdpLeasedOptions, CdpOptions, cdp };
|
|
29
54
|
//# sourceMappingURL=cdp.d.mts.map
|
package/dist/remote/cdp.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { defineRemote } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
-
|
|
5
|
+
// src/remote/cdp.ts
|
|
6
6
|
/** A Chrome DevTools Protocol endpoint: Browserless, Cloudflare Browser Rendering, a bare Chrome, … */
|
|
7
7
|
/** Puppeteer drives the Chromium family, over CDP only (ADR 0007, decision 1). */
|
|
8
8
|
const SUPPORT = {
|
|
@@ -15,20 +15,34 @@ const SUPPORT = {
|
|
|
15
15
|
* this host, so every target needs a signed Storybook URL (ADR 0007, decision 5).
|
|
16
16
|
*/
|
|
17
17
|
function cdp(options) {
|
|
18
|
-
|
|
18
|
+
const base = {
|
|
19
19
|
name: options.name,
|
|
20
20
|
engines: options.engines ?? ["chromium"],
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
protocol: "cdp",
|
|
22
|
+
reach: "url",
|
|
23
|
+
sessions: options.sessions ?? 1,
|
|
24
|
+
maxConnections: options.maxConnections,
|
|
25
|
+
platform: options.platform
|
|
26
|
+
};
|
|
27
|
+
if (typeof options.connect === "function") return defineRemote({
|
|
28
|
+
...base,
|
|
29
|
+
connect: options.connect,
|
|
30
|
+
release: options.release,
|
|
31
|
+
retry: options.retry,
|
|
32
|
+
timeouts: {
|
|
33
|
+
...options.timeouts,
|
|
34
|
+
connectMs: options.timeouts?.connectMs ?? options.connectTimeoutMs
|
|
35
|
+
}
|
|
36
|
+
}, SUPPORT);
|
|
37
|
+
return defineRemote({
|
|
38
|
+
...base,
|
|
39
|
+
connect: {
|
|
23
40
|
endpoint: options.endpoint,
|
|
24
41
|
...options.headers === void 0 ? {} : { headers: options.headers }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
platform: options.platform,
|
|
28
|
-
connectTimeoutMs: options.connectTimeoutMs
|
|
42
|
+
},
|
|
43
|
+
timeouts: { connectMs: options.connectTimeoutMs }
|
|
29
44
|
}, SUPPORT);
|
|
30
45
|
}
|
|
31
|
-
//#endregion
|
|
32
46
|
export { cdp };
|
|
33
47
|
|
|
34
48
|
//# sourceMappingURL=cdp.mjs.map
|
package/dist/remote/cdp.mjs.map
CHANGED
|
@@ -1 +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/**
|
|
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 RemoteOptions,\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/** What both forms of {@link cdp} share. */\ninterface CdpBase {\n /** The target name half (`chromium@<name>`): lowercase letters, digits and hyphens, at most 32. */\n name: string;\n /** Parallel pages the service allows (default 1: set it to the plan's cap). A function is read live. */\n sessions?: number | (() => 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 * Maximum connections this process holds to the service at once (default: no cap), separate from\n * the pages in `sessions`. Renders over the cap wait; per process, not per deployment.\n */\n maxConnections?: number;\n}\n\n/** A service at a fixed address. */\nexport interface CdpFixedOptions extends CdpBase {\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 connect?: undefined;\n release?: undefined;\n}\n\n/**\n * A service that must be woken, started or chosen per connection: `connect` receives the run's\n * context (`buildId`, `runId`, `shardIndex`, `purpose`, `attempt`, `signal`) and returns\n * `{ endpoint, headers, lease }`; `release` gets the lease back once, when the render ends.\n */\nexport interface CdpLeasedOptions<TLease> extends CdpBase {\n connect: Extract<RemoteOptions<TLease>[\"connect\"], (...args: never[]) => unknown>;\n release?: RemoteOptions<TLease>[\"release\"];\n /** Retries after the first connect attempt (default 3) and the first backoff in ms (default 500). */\n retry?: RemoteOptions<TLease>[\"retry\"];\n /** `acquireMs` bounds `connect`, `connectMs` the handshake, `releaseMs` `release` (all default 30 000). */\n timeouts?: RemoteOptions<TLease>[\"timeouts\"];\n endpoint?: undefined;\n headers?: undefined;\n}\n\n/** Options of {@link cdp}. */\nexport type CdpOptions<TLease = unknown> = CdpFixedOptions | CdpLeasedOptions<TLease>;\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<TLease = unknown>(options: CdpOptions<TLease>): RemoteBrowser {\n const base = {\n name: options.name,\n engines: options.engines ?? [\"chromium\"],\n protocol: \"cdp\",\n reach: \"url\",\n sessions: options.sessions ?? 1,\n maxConnections: options.maxConnections,\n platform: options.platform,\n } as const;\n if (typeof options.connect === \"function\") {\n return defineRemote<TLease>(\n {\n ...base,\n connect: options.connect,\n release: options.release,\n retry: options.retry,\n timeouts: {\n ...options.timeouts,\n connectMs: options.timeouts?.connectMs ?? options.connectTimeoutMs,\n },\n },\n SUPPORT,\n );\n }\n return defineRemote(\n {\n ...base,\n connect: {\n endpoint: options.endpoint,\n ...(options.headers === undefined ? {} : { headers: options.headers }),\n },\n timeouts: { connectMs: options.connectTimeoutMs },\n },\n SUPPORT,\n );\n}\n"],"mappings":";;;;;;;AAUA,MAAM,UAAyB;CAC7B,SAAS,CAAC,YAAY,QAAQ;CAC9B,WAAW,CAAC,KAAK;CACjB,OAAO;AACT;;;;;AAsDA,SAAgB,IAAsB,SAA4C;CAChF,MAAM,OAAO;EACX,MAAM,QAAQ;EACd,SAAS,QAAQ,WAAW,CAAC,UAAU;EACvC,UAAU;EACV,OAAO;EACP,UAAU,QAAQ,YAAY;EAC9B,gBAAgB,QAAQ;EACxB,UAAU,QAAQ;CACpB;CACA,IAAI,OAAO,QAAQ,YAAY,YAC7B,OAAO,aACL;EACE,GAAG;EACH,SAAS,QAAQ;EACjB,SAAS,QAAQ;EACjB,OAAO,QAAQ;EACf,UAAU;GACR,GAAG,QAAQ;GACX,WAAW,QAAQ,UAAU,aAAa,QAAQ;EACpD;CACF,GACA,OACF;CAEF,OAAO,aACL;EACE,GAAG;EACH,SAAS;GACP,UAAU,QAAQ;GAClB,GAAI,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;EACtE;EACA,UAAU,EAAE,WAAW,QAAQ,iBAAiB;CAClD,GACA,OACF;AACF"}
|
package/dist/remote/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
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 };
|
|
3
|
+
import { CdpFixedOptions, CdpLeasedOptions, CdpOptions, cdp } from "./cdp.mjs";
|
|
4
|
+
import { PoolLease, RefCounted, RefCountedOptions, RemoteBrowser, RemoteConnectContext, RemoteConnectError, RemoteReleaseInfo, refCounted } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
export { type CdpFixedOptions, type CdpLeasedOptions, type CdpOptions, type PoolLease, type RefCounted, type RefCountedOptions, type RemoteBrowser, type RemoteConnectContext, RemoteConnectError, type RemoteReleaseInfo, cdp, refCounted };
|
package/dist/remote/index.mjs
CHANGED
|
@@ -2,21 +2,51 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { messageOf } from "./browser.mjs";
|
|
5
|
-
import { FONT_PROBE_FAMILIES, inPageProbe, remoteEnvironment } from "@storyshelf/core/utils";
|
|
6
|
-
|
|
5
|
+
import { FONT_PROBE_FAMILIES, boundedProbe, inPageProbe, remoteEnvironment } from "@storyshelf/core/utils";
|
|
6
|
+
// src/remote-environment.ts
|
|
7
7
|
/**
|
|
8
8
|
* The fingerprint of the browser a render drove: its version, and what a blank page in it reports
|
|
9
9
|
* (OS, pixel ratio, installed fonts). Never `process.platform` or `fc-list`, which describe this
|
|
10
|
-
* host.
|
|
11
|
-
* change (ADR 0005), so a
|
|
10
|
+
* host. The whole measurement is bounded by `remote.timeouts.probeMs`: whatever could not be read
|
|
11
|
+
* in time is left out with a warning, and an unknown component is not a change (ADR 0005), so a
|
|
12
|
+
* service outage here cannot rewrite baselines. A cancel is not an outage: it throws
|
|
13
|
+
* `Capture cancelled` so the run stops and is never stored as an unknown environment.
|
|
12
14
|
*/
|
|
13
|
-
async function measureRemote(
|
|
15
|
+
async function measureRemote(session, remote, options = {}) {
|
|
16
|
+
const seen = {};
|
|
17
|
+
let page;
|
|
18
|
+
const outcome = await boundedProbe({
|
|
19
|
+
timeoutMs: remote.timeouts.probeMs,
|
|
20
|
+
signal: options.signal,
|
|
21
|
+
run: async ({ stopped }) => {
|
|
22
|
+
seen.version = await versionOf(session.browser);
|
|
23
|
+
const opened = await session.newPage();
|
|
24
|
+
page = opened;
|
|
25
|
+
if (stopped()) {
|
|
26
|
+
await closePage(session, opened);
|
|
27
|
+
throw new Error("the measurement stopped before its page opened");
|
|
28
|
+
}
|
|
29
|
+
seen.probe = await opened.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
await closePage(session, page);
|
|
33
|
+
if (outcome.status === "cancelled") throw new Error("Capture cancelled");
|
|
34
|
+
warnUnlessDone(outcome, remote.timeouts.probeMs, options.logger);
|
|
14
35
|
return remoteEnvironment({
|
|
15
|
-
browserVersion:
|
|
16
|
-
probe:
|
|
36
|
+
browserVersion: seen.version,
|
|
37
|
+
probe: seen.probe,
|
|
17
38
|
declaredPlatform: remote.platform
|
|
18
39
|
});
|
|
19
40
|
}
|
|
41
|
+
function warnUnlessDone(outcome, timeoutMs, logger) {
|
|
42
|
+
if (outcome.status === "timeout") logger?.warn({ timeoutMs }, "measuring the remote browser timed out; its environment is unknown");
|
|
43
|
+
else if (outcome.status === "failed") logger?.warn({ reason: messageOf(outcome.error) }, "could not measure the remote browser");
|
|
44
|
+
}
|
|
45
|
+
async function closePage(session, page) {
|
|
46
|
+
if (page === void 0) return;
|
|
47
|
+
await page.close().catch(() => {});
|
|
48
|
+
session.forgetPage(page);
|
|
49
|
+
}
|
|
20
50
|
async function versionOf(browser) {
|
|
21
51
|
try {
|
|
22
52
|
return typeof browser.version === "function" ? await browser.version() : void 0;
|
|
@@ -24,20 +54,6 @@ async function versionOf(browser) {
|
|
|
24
54
|
return;
|
|
25
55
|
}
|
|
26
56
|
}
|
|
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
57
|
export { measureRemote };
|
|
42
58
|
|
|
43
59
|
//# sourceMappingURL=remote-environment.mjs.map
|
|
@@ -1 +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.
|
|
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 boundedProbe,\n inPageProbe,\n remoteEnvironment,\n type BrowserProbe,\n} from \"@storyshelf/core/utils\";\nimport type { Browser, Page } from \"puppeteer-core\";\nimport { messageOf } from \"./browser.ts\";\nimport type { BrowserSession } from \"./session.ts\";\n\n/** What stops a measurement: the run's cancel signal, and where warnings go. */\nexport interface MeasureOptions {\n logger?: Logger | undefined;\n /** Aborted when the run is cancelled; the measurement then stops the run. */\n signal?: AbortSignal | undefined;\n}\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. The whole measurement is bounded by `remote.timeouts.probeMs`: whatever could not be read\n * in time is left out with a warning, and an unknown component is not a change (ADR 0005), so a\n * service outage here cannot rewrite baselines. A cancel is not an outage: it throws\n * `Capture cancelled` so the run stops and is never stored as an unknown environment.\n */\nexport async function measureRemote(\n session: BrowserSession,\n remote: RemoteBrowser,\n options: MeasureOptions = {},\n): Promise<CaptureEnvironment> {\n const seen: { version?: string | undefined; probe?: BrowserProbe | undefined } = {};\n let page: Page | undefined;\n const outcome = await boundedProbe({\n timeoutMs: remote.timeouts.probeMs,\n signal: options.signal,\n run: async ({ stopped }) => {\n seen.version = await versionOf(session.browser);\n const opened = await session.newPage();\n page = opened;\n if (stopped()) {\n await closePage(session, opened);\n throw new Error(\"the measurement stopped before its page opened\");\n }\n seen.probe = await opened.evaluate(inPageProbe, [...FONT_PROBE_FAMILIES]);\n },\n });\n // Closed on every path; a page that opens after the limit is closed here too, once it is known.\n await closePage(session, page);\n if (outcome.status === \"cancelled\") {\n throw new Error(\"Capture cancelled\");\n }\n warnUnlessDone(outcome, remote.timeouts.probeMs, options.logger);\n return remoteEnvironment({\n browserVersion: seen.version,\n probe: seen.probe,\n declaredPlatform: remote.platform,\n });\n}\n\nfunction warnUnlessDone(\n outcome: { status: \"done\" | \"timeout\" | \"failed\"; error?: unknown },\n timeoutMs: number,\n logger: Logger | undefined,\n): void {\n if (outcome.status === \"timeout\") {\n logger?.warn(\n { timeoutMs },\n \"measuring the remote browser timed out; its environment is unknown\",\n );\n } else if (outcome.status === \"failed\") {\n logger?.warn({ reason: messageOf(outcome.error) }, \"could not measure the remote browser\");\n }\n}\n\nasync function closePage(session: BrowserSession, page: Page | undefined): Promise<void> {\n if (page === undefined) return;\n await page.close().catch(() => {});\n session.forgetPage(page);\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"],"mappings":";;;;;;;;;;;;;;AA6BA,eAAsB,cACpB,SACA,QACA,UAA0B,CAAC,GACE;CAC7B,MAAM,OAA2E,CAAC;CAClF,IAAI;CACJ,MAAM,UAAU,MAAM,aAAa;EACjC,WAAW,OAAO,SAAS;EAC3B,QAAQ,QAAQ;EAChB,KAAK,OAAO,EAAE,cAAc;GAC1B,KAAK,UAAU,MAAM,UAAU,QAAQ,OAAO;GAC9C,MAAM,SAAS,MAAM,QAAQ,QAAQ;GACrC,OAAO;GACP,IAAI,QAAQ,GAAG;IACb,MAAM,UAAU,SAAS,MAAM;IAC/B,MAAM,IAAI,MAAM,gDAAgD;GAClE;GACA,KAAK,QAAQ,MAAM,OAAO,SAAS,aAAa,CAAC,GAAG,mBAAmB,CAAC;EAC1E;CACF,CAAC;CAED,MAAM,UAAU,SAAS,IAAI;CAC7B,IAAI,QAAQ,WAAW,aACrB,MAAM,IAAI,MAAM,mBAAmB;CAErC,eAAe,SAAS,OAAO,SAAS,SAAS,QAAQ,MAAM;CAC/D,OAAO,kBAAkB;EACvB,gBAAgB,KAAK;EACrB,OAAO,KAAK;EACZ,kBAAkB,OAAO;CAC3B,CAAC;AACH;AAEA,SAAS,eACP,SACA,WACA,QACM;CACN,IAAI,QAAQ,WAAW,WACrB,QAAQ,KACN,EAAE,UAAU,GACZ,oEACF;MACK,IAAI,QAAQ,WAAW,UAC5B,QAAQ,KAAK,EAAE,QAAQ,UAAU,QAAQ,KAAK,EAAE,GAAG,sCAAsC;AAE7F;AAEA,eAAe,UAAU,SAAyB,MAAuC;CACvF,IAAI,SAAS,KAAA,GAAW;CACxB,MAAM,KAAK,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;CACjC,QAAQ,WAAW,IAAI;AACzB;AAEA,eAAe,UAAU,SAA+C;CACtE,IAAI;EACF,OAAO,OAAO,QAAQ,YAAY,aAAa,MAAM,QAAQ,QAAQ,IAAI,KAAA;CAC3E,QAAQ;EACN;CACF;AACF"}
|
package/dist/runtime-params.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { mergeParameters } from "@storyshelf/core/capture";
|
|
5
|
-
|
|
5
|
+
// src/runtime-params.ts
|
|
6
6
|
/** Per-run runtime-parameter extraction cache (ADR 0017). */
|
|
7
7
|
/** Create the per-run runtime-parameter cache shared across captures. */
|
|
8
8
|
function createRuntimeParamsState() {
|
|
@@ -54,7 +54,6 @@ async function readExtractedParameters(page) {
|
|
|
54
54
|
function hasEntries(map) {
|
|
55
55
|
return Boolean(map && Object.keys(map).length > 0);
|
|
56
56
|
}
|
|
57
|
-
//#endregion
|
|
58
57
|
export { createRuntimeParamsState, runtimeParametersForStory };
|
|
59
58
|
|
|
60
59
|
//# sourceMappingURL=runtime-params.mjs.map
|