@storyshelf/runner-puppeteer 0.6.2 → 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 +73 -1
- package/dist/a11y.mjs +22 -22
- package/dist/a11y.mjs.map +1 -1
- package/dist/availability.mjs +30 -0
- package/dist/availability.mjs.map +1 -0
- package/dist/browser.mjs +27 -16
- package/dist/browser.mjs.map +1 -1
- package/dist/capture-runner.d.mts +20 -5
- package/dist/capture-runner.mjs +87 -14
- package/dist/capture-runner.mjs.map +1 -1
- package/dist/connect.mjs +45 -0
- package/dist/connect.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/launch.mjs +52 -0
- package/dist/launch.mjs.map +1 -0
- package/dist/pages.d.mts +5 -0
- package/dist/pages.mjs +90 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/pipeline.mjs +178 -39
- package/dist/pipeline.mjs.map +1 -1
- package/dist/play.mjs +16 -4
- package/dist/play.mjs.map +1 -1
- package/dist/remote/cdp.d.mts +54 -0
- package/dist/remote/cdp.mjs +48 -0
- package/dist/remote/cdp.mjs.map +1 -0
- package/dist/remote/index.d.mts +5 -0
- package/dist/remote/index.mjs +6 -0
- package/dist/remote-environment.mjs +59 -0
- package/dist/remote-environment.mjs.map +1 -0
- package/dist/runtime-params.mjs +1 -2
- package/dist/screenshot.mjs +77 -24
- 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 +157 -0
- package/dist/settle.mjs.map +1 -0
- package/dist/static-server.mjs +2 -4
- package/dist/static-server.mjs.map +1 -1
- package/dist/targets.d.mts +23 -0
- package/dist/targets.mjs +79 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/tests.mjs +119 -0
- package/dist/tests.mjs.map +1 -0
- package/dist/types.d.mts +4 -2
- package/package.json +18 -11
package/README.md
CHANGED
|
@@ -19,11 +19,83 @@ npm install @storyshelf/runner-puppeteer
|
|
|
19
19
|
```ts
|
|
20
20
|
import { createPuppeteerCaptureRunner } from "@storyshelf/runner-puppeteer";
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const runner = createPuppeteerCaptureRunner({
|
|
23
|
+
// defaults to the test-bundled chrome-headless-shell; point at your own Chromium when needed
|
|
24
|
+
executablePath: process.env.CHROME_PATH,
|
|
25
|
+
});
|
|
23
26
|
```
|
|
24
27
|
|
|
28
|
+
Hand the runner to the server:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createShelfApp } from "@storyshelf/app";
|
|
32
|
+
import { createPuppeteerCaptureRunner } from "@storyshelf/runner-puppeteer";
|
|
33
|
+
|
|
34
|
+
const app = createShelfApp({
|
|
35
|
+
database,
|
|
36
|
+
storage,
|
|
37
|
+
testing: { runner: createPuppeteerCaptureRunner() },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Browsers
|
|
42
|
+
|
|
43
|
+
`browsers` lists the targets the runner offers, default first (ADR 0007); omitted it is `["chromium"]`.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createPuppeteerCaptureRunner, local } from "@storyshelf/runner-puppeteer";
|
|
47
|
+
|
|
48
|
+
createPuppeteerCaptureRunner({
|
|
49
|
+
executablePath: "/usr/bin/chromium", // what the bare "chromium" entry launches
|
|
50
|
+
browsers: ["chromium", local("chrome", { executablePath: "/usr/bin/google-chrome" })],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`puppeteer-core` ships no browser, so a target is `ready` only when it has an executable that exists (the entry's own, the runner's `executablePath` for `chromium`, or `PUPPETEER_EXECUTABLE_PATH`); otherwise it is `unavailable` with that reason on **System → Capture browsers**. Bare `"chrome"` without its own `executablePath` throws when the runner is created, and `local("firefox")` and `local("webkit")` throw too: this runner drives Chromium-family engines only. A launch that fails marks its target unavailable and fails the job at once.
|
|
55
|
+
|
|
56
|
+
## Remote browsers
|
|
57
|
+
|
|
58
|
+
`cdp({ name, endpoint, headers, sessions })` from `@storyshelf/runner-puppeteer/remote` is a Chrome DevTools Protocol endpoint (Browserless, Cloudflare Browser Rendering, a Chrome with remote debugging). The runner calls `puppeteer.connect` and offers `chromium@<name>` (and `chrome@<name>` with `engines`):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createPuppeteerCaptureRunner } from "@storyshelf/runner-puppeteer";
|
|
62
|
+
import { cdp } from "@storyshelf/runner-puppeteer/remote";
|
|
63
|
+
|
|
64
|
+
createPuppeteerCaptureRunner({
|
|
65
|
+
executablePath: "/usr/bin/chromium",
|
|
66
|
+
browsers: [
|
|
67
|
+
"chromium",
|
|
68
|
+
cdp({
|
|
69
|
+
name: "cloudflare",
|
|
70
|
+
endpoint: "wss://…",
|
|
71
|
+
headers: { authorization: `Bearer ${token}` },
|
|
72
|
+
sessions: 4,
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A service that must be woken or started first passes `connect` as a function (with `release`, `retry` and `timeouts`) instead of `endpoint`: it receives the run's `buildId`, `runId`, `shardIndex`, `purpose` and a cancel `signal` and returns `{ endpoint, headers, lease }`, and `release(lease, { outcome, error })` runs once when the render ends, fails or is cancelled. `RemoteConnectError` and `refCounted` come from the same `./remote` subpath; see "Scale to zero" in [Remote browsers](https://storyshelf.js.org/guides/remote-browsers/).
|
|
79
|
+
|
|
80
|
+
A Puppeteer remote browser cannot route back to the host, so every remote target needs a signed Storybook URL (`reach: "url"`); it stays `unavailable` until a `secret` and a confirmed public origin exist, then the server hands the runner a short-lived signed URL (`<public origin>/_s/<token>/`) for every render pass, and the runner refuses to start one without it. See [Signed Storybook URLs](https://storyshelf.js.org/guides/remote-browsers/#signed-storybook-urls). The target's environment is measured inside the browser, `sessions` is the per-target page cap shared across the process, and connection failures are retried with backoff. Details in [Remote browsers](https://storyshelf.js.org/guides/remote-browsers/).
|
|
81
|
+
|
|
82
|
+
## Concurrency and memory
|
|
83
|
+
|
|
84
|
+
Pages render through a bounded pool instead of all at once. The default is sized from the detected CPU and memory, container limits included (one page on 0.5–1 vCPU hosts, up to 4); override with `createPuppeteerCaptureRunner({ concurrency })` or `STORYSHELF_CAPTURE_CONCURRENCY`. The per-page timeout scales with CPU (30 s, 60 s under 2 CPUs, 90 s under 1 CPU); override with `navigationTimeoutMs` or `STORYSHELF_NAVIGATION_TIMEOUT_MS`. Every 150 captures the local browser is closed and relaunched to bound its slow memory growth (`recycleBrowserEvery`, or `STORYSHELF_BROWSER_RECYCLE_EVERY`; `0` disables). A connected remote target is instead disconnected and reconnected, never sent `Browser.close`, so sibling clients and the provider machine survive; a run closes only the pages and contexts it created and releases any provider lease once with the first outcome. Each render logs a `capture sizing` line with the detected CPUs, memory and resulting settings. When the orchestrator passes `onCapture`, each screenshot is handed over as soon as it is taken instead of being collected in the result, so memory does not grow with the story count. Set `reusePages: true` (or `STORYSHELF_REUSE_PAGES=1`) to reuse a pool of reset pages (own context each; storage, cookies and IndexedDB cleared between captures; failed pages discarded). It is off by default. The default launch args add low-memory flags (`--disable-dev-shm-usage`, `--disable-extensions`, `--disable-background-networking`, `--mute-audio`); passing your own `args` replaces them. See the deployment guide's "Sizing memory for capture" section.
|
|
85
|
+
|
|
25
86
|
See the [StoryShelf documentation](https://github.com/GuptaSiddhant/StoryShelf#readme) for configuration details.
|
|
26
87
|
|
|
88
|
+
## Puppeteer vs Playwright
|
|
89
|
+
|
|
90
|
+
| Aspect | `runner-puppeteer` | `runner-playwright` |
|
|
91
|
+
| ------------ | -------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
92
|
+
| Engine | `puppeteer-core` + `chrome-headless-shell` (Chromium only) | Playwright (`chromium`/`firefox`/`webkit`/`chrome`) |
|
|
93
|
+
| Selected via | `browsers` runner option (Chromium family only); projects pick from what it offers | `browsers` runner option; projects pick from it |
|
|
94
|
+
| Image size | Smaller — no bundled Firefox/WebKit | Larger |
|
|
95
|
+
| Capabilities | Same orchestrator-owned controls: `play`, `delay`, `diffThreshold`, `viewports`, a11y (axe), burst | Identical controls |
|
|
96
|
+
|
|
97
|
+
Both runners implement the same pure `CaptureRunner` contract. The orchestrator (`@storyshelf/core`, ADR 0015) owns build loading, archive extraction, story discovery, and persistence and delegates only the screenshotting to the runner, so switching is a one-line factory swap.
|
|
98
|
+
|
|
27
99
|
## License
|
|
28
100
|
|
|
29
101
|
MIT
|
package/dist/a11y.mjs
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return await page.evaluate(inPageA11y);
|
|
8
|
-
}
|
|
4
|
+
import { axeInjectionScript, inPageReadA11yParameters, inPageRunAxe, loadAxeSource, mapAxeViolations, resolveA11yRun } from "@storyshelf/core/capture";
|
|
5
|
+
// src/a11y.ts
|
|
6
|
+
/** axe-core accessibility checks for captured pages (ADR 0004 §6). */
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Run axe over the story root and return one annotation string per violation (worst first).
|
|
9
|
+
* Honours `parameters.a11y` (`disable`, `test: "off"`, `config`, `options`, `element`/`context`;
|
|
10
|
+
* function values cannot be read from the page and are dropped, with one warning). Results are annotations: nothing
|
|
11
|
+
* here fails a build. If axe cannot be loaded or injected the result is one visible annotation
|
|
12
|
+
* (`axe unavailable: <reason>`) instead of a silent pass.
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
async function checkA11y(page, logger, storyId) {
|
|
15
|
+
try {
|
|
16
|
+
const read = await page.evaluate(inPageReadA11yParameters);
|
|
17
|
+
if (read && read.dropped > 0) logger?.warn({
|
|
18
|
+
storyId,
|
|
19
|
+
dropped: read.dropped
|
|
20
|
+
}, "parameters.a11y: function, symbol or undefined values were omitted; axe uses the remaining configuration");
|
|
21
|
+
const plan = resolveA11yRun(read?.params);
|
|
22
|
+
if (plan.skip) return [];
|
|
23
|
+
await page.evaluate(axeInjectionScript(await loadAxeSource()));
|
|
24
|
+
return mapAxeViolations(await page.evaluate(inPageRunAxe, plan));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return [`axe unavailable: ${error instanceof Error ? error.message : String(error)}`];
|
|
25
27
|
}
|
|
26
|
-
return violations.slice(0, 10);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
export { checkA11y, inPageA11y };
|
|
29
|
+
export { checkA11y };
|
|
30
30
|
|
|
31
31
|
//# sourceMappingURL=a11y.mjs.map
|
package/dist/a11y.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"a11y.mjs","names":[],"sources":["../src/a11y.ts"],"sourcesContent":["/**
|
|
1
|
+
{"version":3,"file":"a11y.mjs","names":[],"sources":["../src/a11y.ts"],"sourcesContent":["/** axe-core accessibility checks for captured pages (ADR 0004 §6). */\n\nimport {\n axeInjectionScript,\n inPageReadA11yParameters,\n inPageRunAxe,\n loadAxeSource,\n mapAxeViolations,\n resolveA11yRun,\n} from \"@storyshelf/core/capture\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { Page } from \"puppeteer-core\";\n\n/**\n * Run axe over the story root and return one annotation string per violation (worst first).\n * Honours `parameters.a11y` (`disable`, `test: \"off\"`, `config`, `options`, `element`/`context`;\n * function values cannot be read from the page and are dropped, with one warning). Results are annotations: nothing\n * here fails a build. If axe cannot be loaded or injected the result is one visible annotation\n * (`axe unavailable: <reason>`) instead of a silent pass.\n */\nexport async function checkA11y(page: Page, logger?: Logger, storyId?: string): Promise<string[]> {\n try {\n const read = await page.evaluate(inPageReadA11yParameters);\n if (read && read.dropped > 0)\n logger?.warn(\n { storyId, dropped: read.dropped },\n \"parameters.a11y: function, symbol or undefined values were omitted; axe uses the remaining configuration\",\n );\n const plan = resolveA11yRun(read?.params);\n if (plan.skip) return [];\n // Source string over CDP `Runtime.evaluate`: not CSP-bound and works on remote browsers.\n await page.evaluate(axeInjectionScript(await loadAxeSource()));\n return mapAxeViolations(await page.evaluate(inPageRunAxe, plan));\n } catch (error) {\n return [`axe unavailable: ${error instanceof Error ? error.message : String(error)}`];\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,eAAsB,UAAU,MAAY,QAAiB,SAAqC;CAChG,IAAI;EACF,MAAM,OAAO,MAAM,KAAK,SAAS,wBAAwB;EACzD,IAAI,QAAQ,KAAK,UAAU,GACzB,QAAQ,KACN;GAAE;GAAS,SAAS,KAAK;EAAQ,GACjC,0GACF;EACF,MAAM,OAAO,eAAe,MAAM,MAAM;EACxC,IAAI,KAAK,MAAM,OAAO,CAAC;EAEvB,MAAM,KAAK,SAAS,mBAAmB,MAAM,cAAc,CAAC,CAAC;EAC7D,OAAO,iBAAiB,MAAM,KAAK,SAAS,cAAc,IAAI,CAAC;CACjE,SAAS,OAAO;EACd,OAAO,CAAC,oBAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;CACtF;AACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
// src/availability.ts
|
|
6
|
+
/**
|
|
7
|
+
* Each local target as a {@link RunnerTarget}. `puppeteer-core` ships no browser, so a target is
|
|
8
|
+
* `ready` only when it has an executable that exists: its own, the runner's, or the one named by
|
|
9
|
+
* `PUPPETEER_EXECUTABLE_PATH`, which `puppeteer-core` itself reads.
|
|
10
|
+
*/
|
|
11
|
+
function verifyLocalTargets(targets, env = process.env, exists = existsSync) {
|
|
12
|
+
return targets.map((entry) => ({
|
|
13
|
+
target: entry.target,
|
|
14
|
+
reach: "loopback",
|
|
15
|
+
status: statusOf(entry, env, exists)
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
function statusOf(entry, env, exists) {
|
|
19
|
+
const path = entry.executablePath ?? envPath(entry, env);
|
|
20
|
+
if (path === void 0) return { unavailable: "no executablePath (puppeteer-core ships no browser): set `executablePath`" };
|
|
21
|
+
return exists(path) ? "ready" : { unavailable: `executable not found at ${path}` };
|
|
22
|
+
}
|
|
23
|
+
/** `PUPPETEER_EXECUTABLE_PATH` is for the bundled engine, never for `chrome`. */
|
|
24
|
+
function envPath(entry, env) {
|
|
25
|
+
const path = env["PUPPETEER_EXECUTABLE_PATH"];
|
|
26
|
+
return entry.target === "chromium" && path ? path : void 0;
|
|
27
|
+
}
|
|
28
|
+
export { verifyLocalTargets };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=availability.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability.mjs","names":[],"sources":["../src/availability.ts"],"sourcesContent":["/** Telling which declared local engines can launch, without launching anything (ADR 0007, decision 7). */\nimport type { RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport { existsSync } from \"node:fs\";\nimport type { LocalTarget } from \"./targets.ts\";\n\n/** Injectable file check, so tests need no browsers installed. */\ntype Exists = (path: string) => boolean;\n\n/**\n * Each local target as a {@link RunnerTarget}. `puppeteer-core` ships no browser, so a target is\n * `ready` only when it has an executable that exists: its own, the runner's, or the one named by\n * `PUPPETEER_EXECUTABLE_PATH`, which `puppeteer-core` itself reads.\n */\nexport function verifyLocalTargets(\n targets: readonly LocalTarget[],\n env: Readonly<Record<string, string | undefined>> = process.env,\n exists: Exists = existsSync,\n): RunnerTarget[] {\n return targets.map((entry) => ({\n target: entry.target,\n reach: \"loopback\",\n status: statusOf(entry, env, exists),\n }));\n}\n\nfunction statusOf(\n entry: LocalTarget,\n env: Readonly<Record<string, string | undefined>>,\n exists: Exists,\n): RunnerTarget[\"status\"] {\n const path = entry.executablePath ?? envPath(entry, env);\n if (path === undefined) {\n return {\n unavailable: \"no executablePath (puppeteer-core ships no browser): set `executablePath`\",\n };\n }\n return exists(path) ? \"ready\" : { unavailable: `executable not found at ${path}` };\n}\n\n/** `PUPPETEER_EXECUTABLE_PATH` is for the bundled engine, never for `chrome`. */\nfunction envPath(\n entry: LocalTarget,\n env: Readonly<Record<string, string | undefined>>,\n): string | undefined {\n const path = env[\"PUPPETEER_EXECUTABLE_PATH\"];\n return entry.target === \"chromium\" && path ? path : undefined;\n}\n"],"mappings":";;;;;;;;;;AAaA,SAAgB,mBACd,SACA,MAAoD,QAAQ,KAC5D,SAAiB,YACD;CAChB,OAAO,QAAQ,KAAK,WAAW;EAC7B,QAAQ,MAAM;EACd,OAAO;EACP,QAAQ,SAAS,OAAO,KAAK,MAAM;CACrC,EAAE;AACJ;AAEA,SAAS,SACP,OACA,KACA,QACwB;CACxB,MAAM,OAAO,MAAM,kBAAkB,QAAQ,OAAO,GAAG;CACvD,IAAI,SAAS,KAAA,GACX,OAAO,EACL,aAAa,4EACf;CAEF,OAAO,OAAO,IAAI,IAAI,UAAU,EAAE,aAAa,2BAA2B,OAAO;AACnF;;AAGA,SAAS,QACP,OACA,KACoB;CACpB,MAAM,OAAO,IAAI;CACjB,OAAO,MAAM,WAAW,cAAc,OAAO,OAAO,KAAA;AACtD"}
|
package/dist/browser.mjs
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import { RenderRuns } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
// src/browser.ts
|
|
6
|
+
/** Active-run tracking and best-effort cleanup helpers for Puppeteer. */
|
|
7
|
+
/** Tracks in-flight renders, each by its own handle, so `cancel` aborts exactly the runs it names. */
|
|
8
|
+
const activeRuns = new RenderRuns();
|
|
9
|
+
/** The outcome a render's `finally` reports: a cancel wins over a failure, a failure over a finish. */
|
|
10
|
+
function outcomeOf(active, failed) {
|
|
11
|
+
if (active.cancelled) return "cancelled";
|
|
12
|
+
return failed ? "failed" : "finished";
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
/** Run `work`, then `finish` with whether it threw, so a browser is released with how its run went. */
|
|
15
|
+
async function finishing(work, finish) {
|
|
16
|
+
let failure;
|
|
13
17
|
try {
|
|
14
|
-
await
|
|
15
|
-
} catch {
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
return await work();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
failure = { error };
|
|
21
|
+
throw error;
|
|
22
|
+
} finally {
|
|
23
|
+
await finish(failure !== void 0, failure?.error);
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
/** Best-effort close for the static server. */
|
|
22
27
|
async function safeCloseServer(server) {
|
|
@@ -24,11 +29,17 @@ async function safeCloseServer(server) {
|
|
|
24
29
|
await server.close();
|
|
25
30
|
} catch {}
|
|
26
31
|
}
|
|
32
|
+
/** Release and throw when the run was cancelled while acquiring the browser. */
|
|
33
|
+
async function abortIfCancelled(active, session) {
|
|
34
|
+
if (!active.cancelled) return;
|
|
35
|
+
await session.release("cancelled");
|
|
36
|
+
if (active.session === session) active.session = null;
|
|
37
|
+
throw new Error("Capture cancelled");
|
|
38
|
+
}
|
|
27
39
|
/** Normalize any thrown value to a string. */
|
|
28
40
|
function messageOf(error) {
|
|
29
41
|
return error instanceof Error ? error.message : String(error);
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
export { activeRuns, closeBrowser, messageOf, safeCloseBrowser, safeCloseServer };
|
|
43
|
+
export { abortIfCancelled, activeRuns, finishing, messageOf, outcomeOf, safeCloseServer };
|
|
33
44
|
|
|
34
45
|
//# sourceMappingURL=browser.mjs.map
|
package/dist/browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["/**
|
|
1
|
+
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["/** Active-run tracking and best-effort cleanup helpers for Puppeteer. */\n\nimport { RenderRuns, type RemoteLeaseOutcome } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { ActiveRun } from \"./types.ts\";\nimport type { BrowserSession } from \"./session.ts\";\n\n/** Tracks in-flight renders, each by its own handle, so `cancel` aborts exactly the runs it names. */\nexport const activeRuns = new RenderRuns<ActiveRun>();\n\n/** The outcome a render's `finally` reports: a cancel wins over a failure, a failure over a finish. */\nexport function outcomeOf(active: { cancelled: boolean }, failed: boolean): RemoteLeaseOutcome {\n if (active.cancelled) return \"cancelled\";\n return failed ? \"failed\" : \"finished\";\n}\n\n/** Run `work`, then `finish` with whether it threw, so a browser is released with how its run went. */\nexport async function finishing<T>(\n work: () => Promise<T>,\n finish: (failed: boolean, error?: unknown) => Promise<void>,\n): Promise<T> {\n let failure: { error: unknown } | undefined;\n try {\n return await work();\n } catch (error) {\n failure = { error };\n throw error;\n } finally {\n await finish(failure !== undefined, failure?.error);\n }\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/** Release and throw when the run was cancelled while acquiring the browser. */\nexport async function abortIfCancelled(\n active: { cancelled: boolean; session: BrowserSession | null },\n session: BrowserSession,\n): Promise<void> {\n if (!active.cancelled) return;\n await session.release(\"cancelled\");\n if (active.session === session) active.session = null;\n throw new Error(\"Capture cancelled\");\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":";;;;;;;AAOA,MAAa,aAAa,IAAI,WAAsB;;AAGpD,SAAgB,UAAU,QAAgC,QAAqC;CAC7F,IAAI,OAAO,WAAW,OAAO;CAC7B,OAAO,SAAS,WAAW;AAC7B;;AAGA,eAAsB,UACpB,MACA,QACY;CACZ,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,UAAU,EAAE,MAAM;EAClB,MAAM;CACR,UAAU;EACR,MAAM,OAAO,YAAY,KAAA,GAAW,SAAS,KAAK;CACpD;AACF;;AAGA,eAAsB,gBAAgB,QAAmD;CACvF,IAAI;EACF,MAAM,OAAO,MAAM;CACrB,QAAQ,CAER;AACF;;AAGA,eAAsB,iBACpB,QACA,SACe;CACf,IAAI,CAAC,OAAO,WAAW;CACvB,MAAM,QAAQ,QAAQ,WAAW;CACjC,IAAI,OAAO,YAAY,SAAS,OAAO,UAAU;CACjD,MAAM,IAAI,MAAM,mBAAmB;AACrC;;AAGA,SAAgB,UAAU,OAAwB;CAChD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserEntry } from "./targets.mjs";
|
|
3
4
|
import "./types.mjs";
|
|
4
5
|
import "./runtime-params.mjs";
|
|
5
|
-
import {
|
|
6
|
-
//#region src/capture-runner.d.ts
|
|
6
|
+
import { CaptureRunner } from "@storyshelf/core/adapter/capture-runner";
|
|
7
7
|
/** Create a CaptureRunner that renders Storybook stories with Puppeteer (Chromium via chrome-headless-shell). */
|
|
8
8
|
declare function createPuppeteerCaptureRunner(options?: {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Every target the runner offers, default first (ADR 0007, decision 1): `"chromium"`,
|
|
11
|
+
* `local(engine, { executablePath })`, or remote CDP services from
|
|
12
|
+
* `@storyshelf/runner-puppeteer/remote` (`cdp(...)`: `<engine>@<name>` targets that need a
|
|
13
|
+
* Storybook URL they can reach). Default `["chromium"]`. Chromium-family engines only; bare
|
|
14
|
+
* `"chrome"` needs `local("chrome", { executablePath })`.
|
|
15
|
+
*/
|
|
16
|
+
browsers?: readonly BrowserEntry[];
|
|
17
|
+
/** The executable bare entries launch (`puppeteer-core` ships no browser). */
|
|
11
18
|
executablePath?: string;
|
|
19
|
+
/** Launch flags for local engines; replaces the default sandbox and low-memory flags. */
|
|
12
20
|
args?: string[];
|
|
21
|
+
/** Pages rendered at once; falls back to `STORYSHELF_CAPTURE_CONCURRENCY`, then CPU/memory detection. */
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
/** Per-page timeout in ms; falls back to `STORYSHELF_NAVIGATION_TIMEOUT_MS`, then CPU detection. */
|
|
24
|
+
navigationTimeoutMs?: number;
|
|
25
|
+
/** Captures between browser restarts (0 disables); falls back to `STORYSHELF_BROWSER_RECYCLE_EVERY`, then 150. */
|
|
26
|
+
recycleBrowserEvery?: number;
|
|
27
|
+
/** Reuse pages between captures (opt-in); falls back to `STORYSHELF_REUSE_PAGES=1`. */
|
|
28
|
+
reusePages?: boolean;
|
|
13
29
|
}): CaptureRunner;
|
|
14
|
-
//#endregion
|
|
15
30
|
export { createPuppeteerCaptureRunner };
|
|
16
31
|
//# sourceMappingURL=capture-runner.d.mts.map
|
package/dist/capture-runner.mjs
CHANGED
|
@@ -1,42 +1,115 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import {
|
|
4
|
+
import { verifyLocalTargets } from "./availability.mjs";
|
|
5
|
+
import { activeRuns } from "./browser.mjs";
|
|
6
|
+
import { connectRemote } from "./connect.mjs";
|
|
7
|
+
import { launchLocal } from "./launch.mjs";
|
|
5
8
|
import "./runtime-params.mjs";
|
|
6
9
|
import { renderAll } from "./pipeline.mjs";
|
|
7
|
-
|
|
10
|
+
import { flattenBrowsers } from "./targets.mjs";
|
|
11
|
+
import { runTestEntries } from "./tests.mjs";
|
|
12
|
+
import { createTargetOffers, shutdownRemote } from "@storyshelf/core/adapter/capture-runner";
|
|
13
|
+
import { recommendedNavigationTimeoutMs } from "@storyshelf/core/utils";
|
|
14
|
+
// src/capture-runner.ts
|
|
15
|
+
/** Puppeteer capture runner — composition root (factory wiring). */
|
|
8
16
|
/** Create a CaptureRunner that renders Storybook stories with Puppeteer (Chromium via chrome-headless-shell). */
|
|
9
17
|
function createPuppeteerCaptureRunner(options = {}) {
|
|
18
|
+
const { declared, locals, remotes } = flattenBrowsers(options.browsers ?? ["chromium"], options.executablePath);
|
|
19
|
+
const offers = createTargetOffers(declared);
|
|
20
|
+
const driver = {
|
|
21
|
+
launch: offers.guard(async (target, logger, context) => {
|
|
22
|
+
const remote = remotes.find((entry) => entry.target === target);
|
|
23
|
+
return remote ? await connectRemote(remote, context, logger) : {
|
|
24
|
+
browser: await launchLocal(locals.find((entry) => entry.target === target), options.args),
|
|
25
|
+
ownership: "launched"
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
remote: (target) => remotes.find((entry) => entry.target === target)
|
|
29
|
+
};
|
|
30
|
+
const concurrency = options.concurrency ?? envPositiveInt("STORYSHELF_CAPTURE_CONCURRENCY");
|
|
31
|
+
const navigationTimeoutMs = options.navigationTimeoutMs ?? envPositiveInt("STORYSHELF_NAVIGATION_TIMEOUT_MS") ?? recommendedNavigationTimeoutMs();
|
|
32
|
+
const recycleBrowserEvery = options.recycleBrowserEvery ?? envInt("STORYSHELF_BROWSER_RECYCLE_EVERY", 0);
|
|
33
|
+
const reusePages = options.reusePages ?? ["1", "true"].includes(process.env["STORYSHELF_REUSE_PAGES"] ?? "");
|
|
10
34
|
return {
|
|
11
35
|
metadata: {
|
|
12
36
|
name: "Puppeteer",
|
|
13
|
-
version:
|
|
37
|
+
version: "0.8.0",
|
|
14
38
|
description: "Puppeteer capture runner (chrome-headless-shell)",
|
|
15
39
|
kind: "puppeteer",
|
|
16
40
|
category: "capture-runner",
|
|
17
|
-
|
|
41
|
+
get targets() {
|
|
42
|
+
return offers.current();
|
|
43
|
+
}
|
|
18
44
|
},
|
|
45
|
+
lifecycle: withShutdown(offers.lifecycle(async () => await Promise.resolve(verifyTargets(declared, locals)))),
|
|
19
46
|
async render(input) {
|
|
20
47
|
const active = {
|
|
21
48
|
cancelled: false,
|
|
22
|
-
|
|
49
|
+
session: null,
|
|
50
|
+
abort: new AbortController()
|
|
23
51
|
};
|
|
24
|
-
activeRuns.
|
|
52
|
+
const remove = activeRuns.add(input.buildId, input.runId, active);
|
|
25
53
|
try {
|
|
26
|
-
return await renderAll(
|
|
54
|
+
return await renderAll({
|
|
55
|
+
concurrency,
|
|
56
|
+
navigationTimeoutMs,
|
|
57
|
+
recycleBrowserEvery,
|
|
58
|
+
reusePages,
|
|
59
|
+
...input
|
|
60
|
+
}, active, driver, offers.defaultTarget);
|
|
27
61
|
} finally {
|
|
28
|
-
|
|
62
|
+
remove();
|
|
29
63
|
}
|
|
30
64
|
},
|
|
31
|
-
async
|
|
32
|
-
const active =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
async runTests(input) {
|
|
66
|
+
const active = {
|
|
67
|
+
cancelled: false,
|
|
68
|
+
session: null,
|
|
69
|
+
abort: new AbortController()
|
|
70
|
+
};
|
|
71
|
+
const remove = activeRuns.add(input.buildId, input.runId, active);
|
|
72
|
+
try {
|
|
73
|
+
return await runTestEntries({
|
|
74
|
+
concurrency,
|
|
75
|
+
navigationTimeoutMs,
|
|
76
|
+
...input
|
|
77
|
+
}, active, driver, offers.defaultTarget);
|
|
78
|
+
} finally {
|
|
79
|
+
remove();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async cancel(buildId, runId) {
|
|
83
|
+
await Promise.all(activeRuns.matching(buildId, runId).map(async (active) => {
|
|
84
|
+
active.cancelled = true;
|
|
85
|
+
active.abort?.abort();
|
|
86
|
+
await active.session?.release("cancelled");
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Local targets are checked for an executable; a remote one is not probed (that would open a session). */
|
|
92
|
+
function verifyTargets(declared, locals) {
|
|
93
|
+
const checked = new Map(verifyLocalTargets(locals).map((entry) => [entry.target, entry]));
|
|
94
|
+
return declared.map((entry) => checked.get(entry.target) ?? entry);
|
|
95
|
+
}
|
|
96
|
+
/** Teardown also releases every lease still held and closes every pool, so a stopped process leaves no server running. */
|
|
97
|
+
function withShutdown(lifecycle) {
|
|
98
|
+
return {
|
|
99
|
+
...lifecycle,
|
|
100
|
+
teardown: async () => {
|
|
101
|
+
await shutdownRemote();
|
|
102
|
+
await lifecycle.teardown();
|
|
36
103
|
}
|
|
37
104
|
};
|
|
38
105
|
}
|
|
39
|
-
|
|
106
|
+
function envPositiveInt(name) {
|
|
107
|
+
return envInt(name, 1);
|
|
108
|
+
}
|
|
109
|
+
function envInt(name, min) {
|
|
110
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
111
|
+
return parsed >= min ? parsed : void 0;
|
|
112
|
+
}
|
|
40
113
|
export { createPuppeteerCaptureRunner };
|
|
41
114
|
|
|
42
115
|
//# sourceMappingURL=capture-runner.mjs.map
|
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"capture-runner.mjs","names":[],"sources":["../src/capture-runner.ts"],"sourcesContent":["/** Puppeteer capture runner — composition root (factory wiring). */\n\nimport {\n createTargetOffers,\n shutdownRemote,\n type CaptureRunner,\n type RunnerTarget,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport type { AdapterLifecycle } from \"@storyshelf/core/adapter/metadata\";\nimport { recommendedNavigationTimeoutMs } from \"@storyshelf/core/utils\";\nimport { verifyLocalTargets } from \"./availability.ts\";\nimport { activeRuns } from \"./browser.ts\";\nimport { connectRemote } from \"./connect.ts\";\nimport { launchLocal } from \"./launch.ts\";\nimport { renderAll } from \"./pipeline.ts\";\nimport { flattenBrowsers, type BrowserEntry, type LocalTarget } from \"./targets.ts\";\nimport { runTestEntries } from \"./tests.ts\";\nimport type {\n ActiveRun,\n PuppeteerRenderInput,\n PuppeteerTestsInput,\n TargetDriver,\n} 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 /**\n * Every target the runner offers, default first (ADR 0007, decision 1): `\"chromium\"`,\n * `local(engine, { executablePath })`, or remote CDP services from\n * `@storyshelf/runner-puppeteer/remote` (`cdp(...)`: `<engine>@<name>` targets that need a\n * Storybook URL they can reach). Default `[\"chromium\"]`. Chromium-family engines only; bare\n * `\"chrome\"` needs `local(\"chrome\", { executablePath })`.\n */\n browsers?: readonly BrowserEntry[];\n /** The executable bare entries launch (`puppeteer-core` ships no browser). */\n executablePath?: string;\n /** Launch flags for local engines; replaces the default sandbox and low-memory flags. */\n args?: string[];\n /** Pages rendered at once; falls back to `STORYSHELF_CAPTURE_CONCURRENCY`, then CPU/memory detection. */\n concurrency?: number;\n /** Per-page timeout in ms; falls back to `STORYSHELF_NAVIGATION_TIMEOUT_MS`, then CPU detection. */\n navigationTimeoutMs?: number;\n /** Captures between browser restarts (0 disables); falls back to `STORYSHELF_BROWSER_RECYCLE_EVERY`, then 150. */\n recycleBrowserEvery?: number;\n /** Reuse pages between captures (opt-in); falls back to `STORYSHELF_REUSE_PAGES=1`. */\n reusePages?: boolean;\n } = {},\n): CaptureRunner {\n const { declared, locals, remotes } = flattenBrowsers(\n options.browsers ?? [\"chromium\"],\n options.executablePath,\n );\n // Until `lifecycle.setup` verifies them, every declared target reads as ready.\n const offers = createTargetOffers(declared);\n const driver: TargetDriver = {\n launch: offers.guard(async (target, logger, context) => {\n const remote = remotes.find((entry) => entry.target === target);\n return remote\n ? await connectRemote(remote, context, logger)\n : {\n browser: await launchLocal(\n locals.find((entry) => entry.target === target) as LocalTarget,\n options.args,\n ),\n ownership: \"launched\",\n };\n }),\n remote: (target) => remotes.find((entry) => entry.target === target),\n };\n const concurrency = options.concurrency ?? envPositiveInt(\"STORYSHELF_CAPTURE_CONCURRENCY\");\n const navigationTimeoutMs =\n options.navigationTimeoutMs ??\n envPositiveInt(\"STORYSHELF_NAVIGATION_TIMEOUT_MS\") ??\n recommendedNavigationTimeoutMs();\n const recycleBrowserEvery =\n options.recycleBrowserEvery ?? envInt(\"STORYSHELF_BROWSER_RECYCLE_EVERY\", 0);\n const reusePages =\n options.reusePages ?? [\"1\", \"true\"].includes(process.env[\"STORYSHELF_REUSE_PAGES\"] ?? \"\");\n return {\n metadata: {\n name: \"Puppeteer\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Puppeteer capture runner (chrome-headless-shell)\",\n kind: \"puppeteer\",\n category: \"capture-runner\",\n get targets() {\n return offers.current();\n },\n },\n lifecycle: withShutdown(\n offers.lifecycle(async () => await Promise.resolve(verifyTargets(declared, locals))),\n ),\n async render(input: PuppeteerRenderInput) {\n const active: ActiveRun = { cancelled: false, session: null, abort: new AbortController() };\n const remove = activeRuns.add(input.buildId, input.runId, active);\n try {\n return await renderAll(\n { concurrency, navigationTimeoutMs, recycleBrowserEvery, reusePages, ...input },\n active,\n driver,\n offers.defaultTarget,\n );\n } finally {\n remove();\n }\n },\n async runTests(input: PuppeteerTestsInput) {\n const active: ActiveRun = { cancelled: false, session: null, abort: new AbortController() };\n const remove = activeRuns.add(input.buildId, input.runId, active);\n try {\n return await runTestEntries(\n { concurrency, navigationTimeoutMs, ...input },\n active,\n driver,\n offers.defaultTarget,\n );\n } finally {\n remove();\n }\n },\n async cancel(buildId, runId) {\n await Promise.all(\n activeRuns.matching(buildId, runId).map(async (active) => {\n active.cancelled = true;\n active.abort?.abort();\n await active.session?.release(\"cancelled\");\n }),\n );\n },\n };\n}\n\n/** Local targets are checked for an executable; a remote one is not probed (that would open a session). */\nfunction verifyTargets(\n declared: readonly RunnerTarget[],\n locals: readonly LocalTarget[],\n): RunnerTarget[] {\n const checked = new Map(verifyLocalTargets(locals).map((entry) => [entry.target, entry]));\n return declared.map((entry) => checked.get(entry.target) ?? entry);\n}\n\n/** Teardown also releases every lease still held and closes every pool, so a stopped process leaves no server running. */\nfunction withShutdown(lifecycle: AdapterLifecycle): AdapterLifecycle {\n return {\n ...lifecycle,\n teardown: async () => {\n await shutdownRemote();\n await lifecycle.teardown();\n },\n };\n}\n\nfunction envPositiveInt(name: string): number | undefined {\n return envInt(name, 1);\n}\n\nfunction envInt(name: string, min: number): number | undefined {\n const parsed = Number.parseInt(process.env[name] ?? \"\", 10);\n return parsed >= min ? parsed : undefined;\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":";;;;;;;;;;;;;;;;AA2BA,SAAgB,6BACd,UAqBI,CAAC,GACU;CACf,MAAM,EAAE,UAAU,QAAQ,YAAY,gBACpC,QAAQ,YAAY,CAAC,UAAU,GAC/B,QAAQ,cACV;CAEA,MAAM,SAAS,mBAAmB,QAAQ;CAC1C,MAAM,SAAuB;EAC3B,QAAQ,OAAO,MAAM,OAAO,QAAQ,QAAQ,YAAY;GACtD,MAAM,SAAS,QAAQ,MAAM,UAAU,MAAM,WAAW,MAAM;GAC9D,OAAO,SACH,MAAM,cAAc,QAAQ,SAAS,MAAM,IAC3C;IACE,SAAS,MAAM,YACb,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM,GAC9C,QAAQ,IACV;IACA,WAAW;GACb;EACN,CAAC;EACD,SAAS,WAAW,QAAQ,MAAM,UAAU,MAAM,WAAW,MAAM;CACrE;CACA,MAAM,cAAc,QAAQ,eAAe,eAAe,gCAAgC;CAC1F,MAAM,sBACJ,QAAQ,uBACR,eAAe,kCAAkC,KACjD,+BAA+B;CACjC,MAAM,sBACJ,QAAQ,uBAAuB,OAAO,oCAAoC,CAAC;CAC7E,MAAM,aACJ,QAAQ,cAAc,CAAC,KAAK,MAAM,CAAC,CAAC,SAAS,QAAQ,IAAI,6BAA6B,EAAE;CAC1F,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;GACN,UAAU;GACV,IAAI,UAAU;IACZ,OAAO,OAAO,QAAQ;GACxB;EACF;EACA,WAAW,aACT,OAAO,UAAU,YAAY,MAAM,QAAQ,QAAQ,cAAc,UAAU,MAAM,CAAC,CAAC,CACrF;EACA,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAoB;IAAE,WAAW;IAAO,SAAS;IAAM,OAAO,IAAI,gBAAgB;GAAE;GAC1F,MAAM,SAAS,WAAW,IAAI,MAAM,SAAS,MAAM,OAAO,MAAM;GAChE,IAAI;IACF,OAAO,MAAM,UACX;KAAE;KAAa;KAAqB;KAAqB;KAAY,GAAG;IAAM,GAC9E,QACA,QACA,OAAO,aACT;GACF,UAAU;IACR,OAAO;GACT;EACF;EACA,MAAM,SAAS,OAA4B;GACzC,MAAM,SAAoB;IAAE,WAAW;IAAO,SAAS;IAAM,OAAO,IAAI,gBAAgB;GAAE;GAC1F,MAAM,SAAS,WAAW,IAAI,MAAM,SAAS,MAAM,OAAO,MAAM;GAChE,IAAI;IACF,OAAO,MAAM,eACX;KAAE;KAAa;KAAqB,GAAG;IAAM,GAC7C,QACA,QACA,OAAO,aACT;GACF,UAAU;IACR,OAAO;GACT;EACF;EACA,MAAM,OAAO,SAAS,OAAO;GAC3B,MAAM,QAAQ,IACZ,WAAW,SAAS,SAAS,KAAK,CAAC,CAAC,IAAI,OAAO,WAAW;IACxD,OAAO,YAAY;IACnB,OAAO,OAAO,MAAM;IACpB,MAAM,OAAO,SAAS,QAAQ,WAAW;GAC3C,CAAC,CACH;EACF;CACF;AACF;;AAGA,SAAS,cACP,UACA,QACgB;CAChB,MAAM,UAAU,IAAI,IAAI,mBAAmB,MAAM,CAAC,CAAC,KAAK,UAAU,CAAC,MAAM,QAAQ,KAAK,CAAC,CAAC;CACxF,OAAO,SAAS,KAAK,UAAU,QAAQ,IAAI,MAAM,MAAM,KAAK,KAAK;AACnE;;AAGA,SAAS,aAAa,WAA+C;CACnE,OAAO;EACL,GAAG;EACH,UAAU,YAAY;GACpB,MAAM,eAAe;GACrB,MAAM,UAAU,SAAS;EAC3B;CACF;AACF;AAEA,SAAS,eAAe,MAAkC;CACxD,OAAO,OAAO,MAAM,CAAC;AACvB;AAEA,SAAS,OAAO,MAAc,KAAiC;CAC7D,MAAM,SAAS,OAAO,SAAS,QAAQ,IAAI,SAAS,IAAI,EAAE;CAC1D,OAAO,UAAU,MAAM,SAAS,KAAA;AAClC"}
|
package/dist/connect.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { connectLeased, scrubConnection } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
import puppeteer from "puppeteer-core";
|
|
6
|
+
// src/connect.ts
|
|
7
|
+
/** Connecting to a remote browser: retried when the service is unreachable or full, a clear error when it is not. */
|
|
8
|
+
/**
|
|
9
|
+
* Connect to `entry`'s service and return the browser with its ownership: the run must disconnect
|
|
10
|
+
* it, never close it, and releases the service's lease (a function `connect`'s `lease`) exactly
|
|
11
|
+
* once when the session ends. Connection-level failures are retried with backoff, so a provider
|
|
12
|
+
* outage or a full plan costs time and not a story crash; what is left is a `TargetLaunchError`,
|
|
13
|
+
* which fails the job at once (a refused credential also marks the target unavailable, a service
|
|
14
|
+
* that is merely down does not). No message carries the endpoint, its credentials or the lease.
|
|
15
|
+
*/
|
|
16
|
+
async function connectRemote(entry, context, logger) {
|
|
17
|
+
const { value, lease } = await connectLeased(entry, context, {
|
|
18
|
+
logger,
|
|
19
|
+
open: async (connection) => await connectOnce(connection, entry.remote.timeouts.connectMs),
|
|
20
|
+
reasonOf
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
browser: value,
|
|
24
|
+
ownership: "connected",
|
|
25
|
+
releaseLease: async (outcome, error) => {
|
|
26
|
+
await lease.end(outcome, error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function connectOnce(connection, timeoutMs) {
|
|
31
|
+
const isHttp = connection.endpoint.startsWith("http");
|
|
32
|
+
return await puppeteer.connect({
|
|
33
|
+
...isHttp ? { browserURL: connection.endpoint } : { browserWSEndpoint: connection.endpoint },
|
|
34
|
+
protocolTimeout: timeoutMs,
|
|
35
|
+
...connection.headers === void 0 ? {} : { headers: connection.headers }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** What failed, in one line, with the endpoint and credentials removed. */
|
|
39
|
+
function reasonOf(error, connection) {
|
|
40
|
+
const line = (error instanceof Error ? error.message : String(error)).split("\n").find((candidate) => candidate.trim() !== "")?.trim() ?? "the connection failed";
|
|
41
|
+
return scrubConnection(line, connection);
|
|
42
|
+
}
|
|
43
|
+
export { connectRemote };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=connect.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.mjs","names":[],"sources":["../src/connect.ts"],"sourcesContent":["/** Connecting to a remote browser: retried when the service is unreachable or full, a clear error when it is not. */\nimport {\n connectLeased,\n scrubConnection,\n type ConnectLeasedContext,\n type RemoteConnection,\n type RemoteTarget,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { Browser } from \"puppeteer-core\";\nimport puppeteer from \"puppeteer-core\";\nimport type { BrowserAcquisition } from \"./session.ts\";\n\n/**\n * Connect to `entry`'s service and return the browser with its ownership: the run must disconnect\n * it, never close it, and releases the service's lease (a function `connect`'s `lease`) exactly\n * once when the session ends. Connection-level failures are retried with backoff, so a provider\n * outage or a full plan costs time and not a story crash; what is left is a `TargetLaunchError`,\n * which fails the job at once (a refused credential also marks the target unavailable, a service\n * that is merely down does not). No message carries the endpoint, its credentials or the lease.\n */\nexport async function connectRemote(\n entry: RemoteTarget,\n context: ConnectLeasedContext,\n logger?: Logger,\n): Promise<BrowserAcquisition> {\n const { value, lease } = await connectLeased(entry, context, {\n logger,\n open: async (connection) => await connectOnce(connection, entry.remote.timeouts.connectMs),\n reasonOf,\n });\n return {\n browser: value,\n ownership: \"connected\",\n releaseLease: async (outcome, error) => {\n await lease.end(outcome, error);\n },\n };\n}\n\nasync function connectOnce(connection: RemoteConnection, timeoutMs: number): Promise<Browser> {\n const isHttp = connection.endpoint.startsWith(\"http\");\n return await puppeteer.connect({\n ...(isHttp ? { browserURL: connection.endpoint } : { browserWSEndpoint: connection.endpoint }),\n protocolTimeout: timeoutMs,\n ...(connection.headers === undefined ? {} : { headers: connection.headers }),\n });\n}\n\n/** What failed, in one line, with the endpoint and credentials removed. */\nfunction reasonOf(error: unknown, connection: RemoteConnection & { lease?: unknown }): string {\n const text = error instanceof Error ? error.message : String(error);\n const line =\n text\n .split(\"\\n\")\n .find((candidate) => candidate.trim() !== \"\")\n ?.trim() ?? \"the connection failed\";\n return scrubConnection(line, connection);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,eAAsB,cACpB,OACA,SACA,QAC6B;CAC7B,MAAM,EAAE,OAAO,UAAU,MAAM,cAAc,OAAO,SAAS;EAC3D;EACA,MAAM,OAAO,eAAe,MAAM,YAAY,YAAY,MAAM,OAAO,SAAS,SAAS;EACzF;CACF,CAAC;CACD,OAAO;EACL,SAAS;EACT,WAAW;EACX,cAAc,OAAO,SAAS,UAAU;GACtC,MAAM,MAAM,IAAI,SAAS,KAAK;EAChC;CACF;AACF;AAEA,eAAe,YAAY,YAA8B,WAAqC;CAC5F,MAAM,SAAS,WAAW,SAAS,WAAW,MAAM;CACpD,OAAO,MAAM,UAAU,QAAQ;EAC7B,GAAI,SAAS,EAAE,YAAY,WAAW,SAAS,IAAI,EAAE,mBAAmB,WAAW,SAAS;EAC5F,iBAAiB;EACjB,GAAI,WAAW,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,WAAW,QAAQ;CAC5E,CAAC;AACH;;AAGA,SAAS,SAAS,OAAgB,YAA4D;CAE5F,MAAM,QADO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAA,CAG7D,MAAM,IAAI,CAAC,CACX,MAAM,cAAc,UAAU,KAAK,MAAM,EAAE,CAAC,EAC3C,KAAK,KAAK;CAChB,OAAO,gBAAgB,MAAM,UAAU;AACzC"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BrowserEntry, LocalBrowser, LocalOptions, local } from "./targets.mjs";
|
|
3
4
|
import { createPuppeteerCaptureRunner } from "./capture-runner.mjs";
|
|
4
|
-
export { createPuppeteerCaptureRunner };
|
|
5
|
+
export { type BrowserEntry, type LocalBrowser, type LocalOptions, createPuppeteerCaptureRunner, local };
|
package/dist/index.mjs
CHANGED
package/dist/launch.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { TargetLaunchError } from "@storyshelf/core/adapter/capture-runner";
|
|
5
|
+
import puppeteer from "puppeteer-core";
|
|
6
|
+
// src/launch.ts
|
|
7
|
+
/** Starting a local browser, and telling one that would not start from one that crashed. */
|
|
8
|
+
/**
|
|
9
|
+
* Sandbox flags for containers plus low-memory flags. Rendering-affecting flags
|
|
10
|
+
* (`--disable-gpu`, `--single-process`) are excluded: they can shift pixels.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_ARGS = [
|
|
13
|
+
"--no-sandbox",
|
|
14
|
+
"--disable-setuid-sandbox",
|
|
15
|
+
"--disable-dev-shm-usage",
|
|
16
|
+
"--disable-extensions",
|
|
17
|
+
"--disable-background-networking",
|
|
18
|
+
"--mute-audio"
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Puppeteer closes its browser on SIGINT/SIGTERM/SIGHUP by default, which fails a render that a
|
|
22
|
+
* graceful drain is still finishing. The host owns shutdown: it drains, each render releases the
|
|
23
|
+
* browser it launched (or disconnects the one it connected to), then `teardown` releases leases.
|
|
24
|
+
*/
|
|
25
|
+
const HOST_OWNS_SIGNALS = {
|
|
26
|
+
handleSIGINT: false,
|
|
27
|
+
handleSIGTERM: false,
|
|
28
|
+
handleSIGHUP: false
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Launch `entry`'s browser. A failure is a {@link TargetLaunchError}: the browser never started, so
|
|
32
|
+
* no story is at fault and the job fails at once instead of going through crash isolation.
|
|
33
|
+
*/
|
|
34
|
+
async function launchLocal(entry, args) {
|
|
35
|
+
try {
|
|
36
|
+
return await puppeteer.launch({
|
|
37
|
+
...HOST_OWNS_SIGNALS,
|
|
38
|
+
headless: true,
|
|
39
|
+
executablePath: entry.executablePath,
|
|
40
|
+
args: args ?? DEFAULT_ARGS
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new TargetLaunchError(entry.target, firstLine(error), { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Launch errors carry a long log tail; the first line says what failed. */
|
|
47
|
+
function firstLine(error) {
|
|
48
|
+
return (error instanceof Error ? error.message : String(error)).split("\n").find((line) => line.trim() !== "")?.trim() ?? "the browser did not start";
|
|
49
|
+
}
|
|
50
|
+
export { launchLocal };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=launch.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch.mjs","names":[],"sources":["../src/launch.ts"],"sourcesContent":["/** Starting a local browser, and telling one that would not start from one that crashed. */\nimport { TargetLaunchError } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Browser } from \"puppeteer-core\";\nimport puppeteer from \"puppeteer-core\";\nimport type { LocalTarget } from \"./targets.ts\";\n\n/**\n * Sandbox flags for containers plus low-memory flags. Rendering-affecting flags\n * (`--disable-gpu`, `--single-process`) are excluded: they can shift pixels.\n */\nconst DEFAULT_ARGS = [\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-extensions\",\n \"--disable-background-networking\",\n \"--mute-audio\",\n];\n\n/**\n * Puppeteer closes its browser on SIGINT/SIGTERM/SIGHUP by default, which fails a render that a\n * graceful drain is still finishing. The host owns shutdown: it drains, each render releases the\n * browser it launched (or disconnects the one it connected to), then `teardown` releases leases.\n */\nconst HOST_OWNS_SIGNALS = {\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n} as const;\n\n/**\n * Launch `entry`'s browser. A failure is a {@link TargetLaunchError}: the browser never started, so\n * no story is at fault and the job fails at once instead of going through crash isolation.\n */\nexport async function launchLocal(\n entry: LocalTarget,\n args: string[] | undefined,\n): Promise<Browser> {\n try {\n return await puppeteer.launch({\n ...HOST_OWNS_SIGNALS,\n headless: true,\n executablePath: entry.executablePath,\n args: args ?? DEFAULT_ARGS,\n });\n } catch (error) {\n throw new TargetLaunchError(entry.target, firstLine(error), { cause: error });\n }\n}\n\n/** Launch errors carry a long log tail; the first line says what failed. */\nfunction firstLine(error: unknown): string {\n const text = error instanceof Error ? error.message : String(error);\n return (\n text\n .split(\"\\n\")\n .find((line) => line.trim() !== \"\")\n ?.trim() ?? \"the browser did not start\"\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;AAOA,MAAM,oBAAoB;CACxB,cAAc;CACd,eAAe;CACf,cAAc;AAChB;;;;;AAMA,eAAsB,YACpB,OACA,MACkB;CAClB,IAAI;EACF,OAAO,MAAM,UAAU,OAAO;GAC5B,GAAG;GACH,UAAU;GACV,gBAAgB,MAAM;GACtB,MAAM,QAAQ;EAChB,CAAC;CACH,SAAS,OAAO;EACd,MAAM,IAAI,kBAAkB,MAAM,QAAQ,UAAU,KAAK,GAAG,EAAE,OAAO,MAAM,CAAC;CAC9E;AACF;;AAGA,SAAS,UAAU,OAAwB;CAEzC,QADa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAA,CAG7D,MAAM,IAAI,CAAC,CACX,MAAM,SAAS,KAAK,KAAK,MAAM,EAAE,CAAC,EACjC,KAAK,KAAK;AAElB"}
|