@volter/browser-cdp 0.1.47 → 0.1.49

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.
@@ -0,0 +1,153 @@
1
+ import { createSocketEndpoint } from "@volter/almostcdp/socket";
2
+ import { MessagePortTransport } from "@volter/almostcdp/message-port";
3
+ import { createPreviewBrowserHost } from "./preview-browser-host.js";
4
+ const REATTACH_DELAY_MS = 250;
5
+ /**
6
+ * The preview as a Chrome DevTools Protocol endpoint on a loopback port of the
7
+ * tab. AlmostCDP serves discovery and the DevTools WebSockets on each accepted
8
+ * connection; the preview's surface is lent through the authenticated sandbox
9
+ * bridge and lent again whenever the sandbox document is replaced, which a
10
+ * client sees as its page target closing and a new one appearing.
11
+ */
12
+ export function publishPreviewCdp(options) {
13
+ const { bridge, port } = options;
14
+ let endpoint;
15
+ const host = options.browser ? createPreviewBrowserHost({
16
+ ...options.browser,
17
+ attachSurface: (peer, id, signal) => attachAndAwait(endpoint, peer, id, signal),
18
+ }) : undefined;
19
+ endpoint = createSocketEndpoint({
20
+ address: `127.0.0.1:${port}`,
21
+ ...(host ? { contextHost: host.contextHost, pageHost: host.pageHost } : {}),
22
+ ...(options.downloads ? { downloadHost: { write: (_context, path, bytes) => options.downloads.write(path, bytes) } } : {}),
23
+ ...(options.files ? { fileHost: options.files } : {}),
24
+ });
25
+ const unlisten = options.listen(port, (socket) => endpoint.serve(socket));
26
+ const unpublish = options.portTable ? servePortTable(endpoint, options.portTable)
27
+ : options.discoveryTable ? serveDiscovery(endpoint, options.discoveryTable) : undefined;
28
+ const targetId = options.targetId ?? "preview";
29
+ let closed = false;
30
+ if (bridge)
31
+ void (async () => {
32
+ while (!closed) {
33
+ try {
34
+ const channel = await bridge.connectProtocol(targetId);
35
+ const transport = new MessagePortTransport(channel.port);
36
+ void channel.closed.then((reason) => transport.close(1001, reason));
37
+ if (closed) {
38
+ transport.close(1001, "Preview CDP publication closed");
39
+ return;
40
+ }
41
+ const lost = new Promise((resolve) => transport.on("close", resolve));
42
+ endpoint.attachSurface(transport);
43
+ await lost;
44
+ }
45
+ catch (error) {
46
+ // A disposed bridge never lends its preview again; every other failure
47
+ // (not yet ready, a dropped transfer, a replaced document) is retried.
48
+ if (error instanceof Error && error.message === "Browser sandbox bridge is disposed.")
49
+ return;
50
+ }
51
+ if (!closed)
52
+ await new Promise((resolve) => setTimeout(resolve, REATTACH_DELAY_MS));
53
+ }
54
+ })();
55
+ return {
56
+ port,
57
+ url: `http://127.0.0.1:${port}`,
58
+ attachClient: (peer) => endpoint.attachClient(peer),
59
+ async close() {
60
+ if (closed)
61
+ return;
62
+ closed = true;
63
+ unlisten();
64
+ unpublish?.();
65
+ await endpoint.close();
66
+ await host?.dispose();
67
+ },
68
+ };
69
+ }
70
+ /** Lends a page surface and resolves once the endpoint lists it under its target id. */
71
+ async function attachAndAwait(endpoint, peer, id, signal) {
72
+ let lost = false;
73
+ peer.on("close", () => { lost = true; });
74
+ endpoint.attachSurface(peer);
75
+ while (!endpoint.targets().some((target) => target.id === id)) {
76
+ signal.throwIfAborted();
77
+ if (lost)
78
+ throw new Error("The page surface closed before it registered.");
79
+ await new Promise((resolve) => setTimeout(resolve, 10));
80
+ }
81
+ }
82
+ /**
83
+ * Discovery as the port's HTTP server and DevTools WebSockets as its socket
84
+ * endpoint. Discovery names the address the request came in on: a loopback
85
+ * host plainly, any other host (a live tunnel's) behind TLS.
86
+ */
87
+ /** The discovery documents as a GET-only server of the port table. */
88
+ function serveDiscovery(endpoint, table) {
89
+ const encoder = new TextEncoder();
90
+ table.ports.registerServer({
91
+ listening: true,
92
+ address: () => ({ port: table.port, address: "0.0.0.0", family: "IPv4" }),
93
+ async handleRequest(method, url, headers) {
94
+ if (method !== "GET" && method !== "HEAD")
95
+ return { statusCode: 405, headers: { allow: "GET, HEAD" } };
96
+ const host = headers.host ?? headers.Host ?? `127.0.0.1:${table.port}`;
97
+ const loopback = /^(localhost|127\.0\.0\.1|\[::1\])(:|$)/u.test(host);
98
+ const { status, body } = endpoint.discovery(new URL(url, "http://localhost").pathname, loopback ? host : `wss://${host}`);
99
+ return { statusCode: status, headers: { "content-type": "application/json; charset=UTF-8" }, ...(method === "GET" ? { body: encoder.encode(JSON.stringify(body)) } : {}) };
100
+ },
101
+ }, table.port);
102
+ return () => table.ports.unregisterServer(table.port);
103
+ }
104
+ function servePortTable(endpoint, table) {
105
+ const unregisterDiscovery = serveDiscovery(endpoint, table);
106
+ const unregisterSockets = table.webSockets.register(table.port, {
107
+ open(path, _protocols, handlers) {
108
+ const messageListeners = [];
109
+ const closeListeners = [];
110
+ let state = 1;
111
+ const finish = () => {
112
+ if (state === 3)
113
+ return;
114
+ state = 3;
115
+ for (const listener of closeListeners)
116
+ listener();
117
+ };
118
+ const peer = {
119
+ get readyState() { return state; },
120
+ send: (data) => { if (state === 1)
121
+ handlers.message({ kind: "text", text: data }); },
122
+ close: (code, reason) => { if (state !== 1)
123
+ return; handlers.close(code, reason, true); finish(); },
124
+ on: ((event, listener) => {
125
+ if (event === "message")
126
+ messageListeners.push(listener);
127
+ else
128
+ closeListeners.push(listener);
129
+ }),
130
+ };
131
+ queueMicrotask(() => {
132
+ handlers.open("");
133
+ endpoint.connect(peer, path);
134
+ });
135
+ return {
136
+ send(data) {
137
+ if (data.kind !== "text") {
138
+ peer.close(1003, "CDP messages are text");
139
+ return;
140
+ }
141
+ for (const listener of messageListeners)
142
+ listener(data.text);
143
+ },
144
+ close(code = 1000, reason = "") { handlers.close(code, reason, true); finish(); },
145
+ };
146
+ },
147
+ });
148
+ return () => {
149
+ unregisterSockets();
150
+ unregisterDiscovery();
151
+ };
152
+ }
153
+ //# sourceMappingURL=preview-cdp-endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-cdp-endpoint.js","sourceRoot":"","sources":["../src/preview-cdp-endpoint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sandbox-surface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-surface.d.ts","sourceRoot":"","sources":["../src/sandbox-surface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { connectDomSurface } from "@volter/almostcdp/dom";
2
+ import { MessagePortTransport } from "@volter/almostcdp/message-port";
3
+ import { createCaptureAdapter } from "@volter/almostcdp/capture-stream";
4
+ import { createDomAccessibility } from "@volter/almostcdp/accessibility";
5
+ import { registerPreviewProtocolSurface } from "@volter/browser-runtime/sandbox-preview-inspector.js";
6
+ /**
7
+ * The sandbox page lends its preview as an AlmostCDP DOM surface. Imported by
8
+ * the sandbox bootstrap of an embedding that publishes the tab's CDP
9
+ * endpoint; the runtime itself lends no protocol.
10
+ */
11
+ registerPreviewProtocolSurface((port, id, hooks) => connectDomSurface({
12
+ id,
13
+ target: hooks.window,
14
+ validateDocument: hooks.validateDocument,
15
+ urls: hooks.urls,
16
+ transport: new MessagePortTransport(port),
17
+ navigation: hooks.navigation,
18
+ // The sandbox bridge carries the preview's document requests through the
19
+ // surface's network door, so a navigation answers with its outcome.
20
+ carriedDocuments: true,
21
+ documentStart: hooks.documentStart,
22
+ capture: createCaptureAdapter((params) => hooks.capture(params), "dom-rasterized"),
23
+ // The accessibility tree read from the page's DOM, as a client's
24
+ // Accessibility.getFullAXTree asks (chrome-devtools-mcp's snapshot).
25
+ accessibility: createDomAccessibility(),
26
+ }));
27
+ //# sourceMappingURL=sandbox-surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-surface.js","sourceRoot":"","sources":["../src/sandbox-surface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,8BAA8B,EAAE,MAAM,sDAAsD,CAAC;AAEtG;;;;GAIG;AACH,8BAA8B,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC;IACpE,EAAE;IACF,MAAM,EAAE,KAAK,CAAC,MAAM;IACpB,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;IACxC,IAAI,EAAE,KAAK,CAAC,IAAI;IAChB,SAAS,EAAE,IAAI,oBAAoB,CAAC,IAAI,CAAC;IACzC,UAAU,EAAE,KAAK,CAAC,UAAU;IAC5B,yEAAyE;IACzE,oEAAoE;IACpE,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,KAAK,CAAC,aAAa;IAClC,OAAO,EAAE,oBAAoB,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAClF,iEAAiE;IACjE,qEAAqE;IACrE,aAAa,EAAE,sBAAsB,EAAE;CACxC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { BrowserRuntime } from "@volter/browser-runtime";
2
+ import type { BrowserSandboxBridge } from "@volter/browser-runtime/browser-sandbox-bridge.js";
3
+ import type { PreviewBrowserHostOptions } from "./preview-browser-host.js";
4
+ import { publishPreviewCdp, type PreviewCdpListener, type PreviewCdpPublication } from "./preview-cdp-endpoint.js";
5
+ /** The port a client in the tab passes to `connectOverCDP`, as Chromium's default. */
6
+ export declare const TAB_CDP_PORT = 9222;
7
+ export interface TabChromiumOptions {
8
+ runtime: BrowserRuntime;
9
+ /** The preview's bridge, to publish the preview itself as a page target; a launched browser does not need it. */
10
+ bridge?: BrowserSandboxBridge;
11
+ /** A loopback listener of the tab, such as a Node container's `listenNet`. */
12
+ listen: PreviewCdpListener;
13
+ /** Where browser contexts and pages the endpoint creates are opened. */
14
+ browser: Omit<PreviewBrowserHostOptions, "attachSurface">;
15
+ /** The home Playwright's registry looks under: the `HOME` the tab's programs run with. */
16
+ home: string;
17
+ /** The project roots whose installed playwright-core pins are read for the revisions to place. */
18
+ roots: readonly string[];
19
+ /** The same endpoint in the tab's port table, so a page of the tab or a shared port reaches it. */
20
+ portTable?: Parameters<typeof publishPreviewCdp>[0]["portTable"];
21
+ /** The discovery documents alone in the tab's port table, under the endpoint's port. */
22
+ discoveryTable?: Parameters<typeof publishPreviewCdp>[0]["discoveryTable"];
23
+ port?: number;
24
+ }
25
+ export interface TabChromium {
26
+ readonly publication: PreviewCdpPublication;
27
+ dispose(): Promise<void>;
28
+ }
29
+ /**
30
+ * A Chromium for the tab's own Playwright: the preview published as a CDP
31
+ * endpoint, and the face registered under Chromium's executable names and
32
+ * placed where each installed playwright-core's registry looks, so a project's
33
+ * unchanged `playwright` launches it over its debugging pipe. A page's
34
+ * download is written where the Playwright client in the container reads it.
35
+ */
36
+ export declare function installTabChromium(options: TabChromiumOptions): TabChromium;
37
+ /**
38
+ * Where the tab has no Chromium to give (a page that runs its project in-page,
39
+ * with no sandbox to lend frames from), the face is still placed, and
40
+ * launching it says why and where the Chromium is. Without it a project's
41
+ * Playwright reports a missing executable and advises `npx playwright
42
+ * install`, a download that can never run in a tab.
43
+ */
44
+ export declare function installUnavailableTabChromium(options: {
45
+ runtime: BrowserRuntime;
46
+ home: string;
47
+ roots: readonly string[];
48
+ /** One line naming what to do instead. */
49
+ reason: string;
50
+ }): () => void;
51
+ //# sourceMappingURL=tab-chromium.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-chromium.d.ts","sourceRoot":"","sources":["../src/tab-chromium.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mDAAmD,CAAC;AAE9F,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEnH,sFAAsF;AACtF,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,cAAc,CAAC;IACxB,iHAAiH;IACjH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,8EAA8E;IAC9E,MAAM,EAAE,kBAAkB,CAAC;IAC3B,wEAAwE;IACxE,OAAO,EAAE,IAAI,CAAC,yBAAyB,EAAE,eAAe,CAAC,CAAC;IAC1D,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,mGAAmG;IACnG,SAAS,CAAC,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACjE,wFAAwF;IACxF,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAC5C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAgD3E;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACrD,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,IAAI,CAUb"}
@@ -0,0 +1,113 @@
1
+ import { CHROMIUM_FACE_PROGRAMS, chromiumFaceRunner, placeChromiumFace, playwrightChromiumRevisions } from "./chromium-face.js";
2
+ import { publishPreviewCdp } from "./preview-cdp-endpoint.js";
3
+ /** The port a client in the tab passes to `connectOverCDP`, as Chromium's default. */
4
+ export const TAB_CDP_PORT = 9222;
5
+ /**
6
+ * A Chromium for the tab's own Playwright: the preview published as a CDP
7
+ * endpoint, and the face registered under Chromium's executable names and
8
+ * placed where each installed playwright-core's registry looks, so a project's
9
+ * unchanged `playwright` launches it over its debugging pipe. A page's
10
+ * download is written where the Playwright client in the container reads it.
11
+ */
12
+ export function installTabChromium(options) {
13
+ const { runtime } = options;
14
+ const fs = runtime.filesystem;
15
+ const publication = publishPreviewCdp({
16
+ ...(options.bridge ? { bridge: options.bridge } : {}),
17
+ port: options.port ?? TAB_CDP_PORT,
18
+ listen: options.listen,
19
+ browser: options.browser,
20
+ ...(options.portTable ? { portTable: options.portTable } : {}),
21
+ ...(options.discoveryTable ? { discoveryTable: options.discoveryTable } : {}),
22
+ downloads: {
23
+ async write(path, bytes) {
24
+ fs.mkdirSync(path.slice(0, path.lastIndexOf("/")) || "/", { recursive: true });
25
+ fs.writeFileSync(path, bytes);
26
+ },
27
+ },
28
+ files: {
29
+ async read(path) {
30
+ const stat = fs.statSync(path);
31
+ if (!stat.isFile())
32
+ throw new Error(`Not a file: ${path}`);
33
+ const name = path.slice(path.lastIndexOf("/") + 1);
34
+ return { data: await fs.readFile(path), name, type: mimeTypeOf(name), lastModified: Math.round(stat.mtimeMs) };
35
+ },
36
+ async list(path) {
37
+ if (!fs.statSync(path).isDirectory())
38
+ return undefined;
39
+ const files = [];
40
+ const walk = (directory, prefix) => {
41
+ for (const name of fs.readdirSync(directory).map(String).sort()) {
42
+ const entry = `${directory}/${name}`;
43
+ if (fs.statSync(entry).isDirectory())
44
+ walk(entry, `${prefix}${name}/`);
45
+ else
46
+ files.push(`${prefix}${name}`);
47
+ }
48
+ };
49
+ walk(path.replace(/\/+$/u, ""), "");
50
+ return files;
51
+ },
52
+ },
53
+ });
54
+ const face = chromiumFaceRunner((peer) => publication.attachClient(peer));
55
+ for (const name of CHROMIUM_FACE_PROGRAMS)
56
+ runtime.registerProgram(name, face, { descriptors: true });
57
+ const stopPlacing = keepFacePlaced(runtime, options.home, options.roots);
58
+ return {
59
+ publication,
60
+ async dispose() {
61
+ stopPlacing();
62
+ await publication.close();
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * Where the tab has no Chromium to give (a page that runs its project in-page,
68
+ * with no sandbox to lend frames from), the face is still placed, and
69
+ * launching it says why and where the Chromium is. Without it a project's
70
+ * Playwright reports a missing executable and advises `npx playwright
71
+ * install`, a download that can never run in a tab.
72
+ */
73
+ export function installUnavailableTabChromium(options) {
74
+ const message = `chromium: ${options.reason}\n`;
75
+ const refuse = async (_argv, run) => {
76
+ run.onStderr?.(message);
77
+ return { stdout: "", stderr: message, exitCode: 1 };
78
+ };
79
+ // Declared like the real face: Playwright launches it with its pipe on fds
80
+ // 3 and 4, and a program refusing those would be refused first, generically.
81
+ for (const name of CHROMIUM_FACE_PROGRAMS)
82
+ options.runtime.registerProgram(name, refuse, { descriptors: true });
83
+ return keepFacePlaced(options.runtime, options.home, options.roots);
84
+ }
85
+ /** Places the face for every playwright-core revision the roots install, again whenever that set changes. */
86
+ function keepFacePlaced(runtime, home, roots) {
87
+ const fs = runtime.filesystem;
88
+ let placed = "";
89
+ const place = async () => {
90
+ const revisions = (await playwrightChromiumRevisions(fs, roots)).sort().join(",");
91
+ if (revisions === placed)
92
+ return;
93
+ placed = revisions;
94
+ // Playwright looks under os.homedir(), which is HOME.
95
+ await placeChromiumFace(fs, `${home}/.cache/ms-playwright`, revisions ? revisions.split(",") : []);
96
+ };
97
+ // Playwright can arrive with the image or later; its pin is one file, read when it changes.
98
+ const placing = setInterval(() => { void place().catch((cause) => console.error("Chromium face placement failed:", cause)); }, 2_000);
99
+ void place().catch((cause) => console.error("Chromium face placement failed:", cause));
100
+ return () => clearInterval(placing);
101
+ }
102
+ /** A file's type as Chromium gives a picked file one: by its extension. */
103
+ function mimeTypeOf(name) {
104
+ const extension = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
105
+ return {
106
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
107
+ ico: "image/x-icon", bmp: "image/bmp", avif: "image/avif", txt: "text/plain", html: "text/html", htm: "text/html",
108
+ css: "text/css", js: "text/javascript", mjs: "text/javascript", json: "application/json", xml: "text/xml", csv: "text/csv",
109
+ pdf: "application/pdf", zip: "application/zip", gz: "application/gzip", wasm: "application/wasm", mp3: "audio/mpeg",
110
+ wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4", webm: "video/webm", woff: "font/woff", woff2: "font/woff2",
111
+ }[extension] ?? "";
112
+ }
113
+ //# sourceMappingURL=tab-chromium.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-chromium.js","sourceRoot":"","sources":["../src/tab-chromium.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEhI,OAAO,EAAE,iBAAiB,EAAuD,MAAM,2BAA2B,CAAC;AAEnH,sFAAsF;AACtF,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AA0BjC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA2B;IAC5D,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAC5B,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAC9B,MAAM,WAAW,GAAG,iBAAiB,CAAC;QACpC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,YAAY;QAClC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,SAAS,EAAE;YACT,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK;gBACrB,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC/E,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAChC,CAAC;SACF;QACD,KAAK,EAAE;YACL,KAAK,CAAC,IAAI,CAAC,IAAI;gBACb,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC/B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;oBAAE,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBACnD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjH,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,IAAI;gBACb,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;oBAAE,OAAO,SAAS,CAAC;gBACvD,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,CAAC,SAAiB,EAAE,MAAc,EAAQ,EAAE;oBACvD,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;wBAChE,MAAM,KAAK,GAAG,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC;wBACrC,IAAI,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE;4BAAE,IAAI,CAAC,KAAK,EAAE,GAAG,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC;;4BAClE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC;oBACtC,CAAC;gBACH,CAAC,CAAC;gBACF,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;gBACpC,OAAO,KAAK,CAAC;YACf,CAAC;SACF;KACF,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,kBAAkB,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1E,KAAK,MAAM,IAAI,IAAI,sBAAsB;QAAE,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IACtG,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACzE,OAAO;QACL,WAAW;QACX,KAAK,CAAC,OAAO;YACX,WAAW,EAAE,CAAC;YACd,MAAM,WAAW,CAAC,KAAK,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAAC,OAM7C;IACC,MAAM,OAAO,GAAG,aAAa,OAAO,CAAC,MAAM,IAAI,CAAC;IAChD,MAAM,MAAM,GAAG,KAAK,EAAE,KAAwB,EAAE,GAA0C,EAAE,EAAE;QAC5F,GAAG,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC;QACxB,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACtD,CAAC,CAAC;IACF,2EAA2E;IAC3E,6EAA6E;IAC7E,KAAK,MAAM,IAAI,IAAI,sBAAsB;QAAE,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAChH,OAAO,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC;AAED,6GAA6G;AAC7G,SAAS,cAAc,CAAC,OAAuB,EAAE,IAAY,EAAE,KAAwB;IACrF,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAC9B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,KAAK,GAAG,KAAK,IAAmB,EAAE;QACtC,MAAM,SAAS,GAAG,CAAC,MAAM,2BAA2B,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClF,IAAI,SAAS,KAAK,MAAM;YAAE,OAAO;QACjC,MAAM,GAAG,SAAS,CAAC;QACnB,sDAAsD;QACtD,MAAM,iBAAiB,CAAC,EAAE,EAAE,GAAG,IAAI,uBAAuB,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrG,CAAC,CAAC;IACF,4FAA4F;IAC5F,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/I,KAAK,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChG,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;AAED,2EAA2E;AAC3E,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACtE,OAAQ;QACN,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,eAAe;QACnH,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW;QACjH,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,iBAAiB,EAAE,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU;QAC1H,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,YAAY;QACnH,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY;KACtF,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;AAChD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/browser-cdp",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "The tab's Chrome DevTools Protocol endpoint over AlmostCDP: the preview published inside the tab, browser contexts and pages, and the Chromium face",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,5 +22,9 @@
22
22
  "@volter/almostcdp": "0.1.25",
23
23
  "@volter/browser-runtime": "^0.1.5",
24
24
  "dom-accessibility-api": "0.7.1"
25
- }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "LICENSE"
29
+ ]
26
30
  }
@@ -1,130 +0,0 @@
1
- import type { ProtocolPeer } from "@volter/almostcdp/core";
2
- import type { BrowserProgramRunner } from "@volter/browser-runtime/browser-process-host.js";
3
- import type { BrowserFileSystem } from "@volter/browser-runtime/browser-filesystem.js";
4
-
5
- /** Where Playwright's registry keeps each Chromium it launches, under its cache directory. */
6
- const PLACEMENTS = [
7
- { browser: "chromium", directory: "chromium", executable: "chrome-linux64/chrome" },
8
- { browser: "chromium-headless-shell", directory: "chromium_headless_shell", executable: "chrome-headless-shell-linux64/chrome-headless-shell" },
9
- ] as const;
10
-
11
- /** The program names a placed executable resolves to (a path resolves to its basename). */
12
- export const CHROMIUM_FACE_PROGRAMS = ["chrome", "chrome-headless-shell"] as const;
13
-
14
- const encoder = new TextEncoder();
15
-
16
- /**
17
- * Chromium as Playwright launches it: `--remote-debugging-pipe`, CDP as
18
- * NUL-terminated JSON read from fd 3 and written to fd 4. The face owns no
19
- * protocol behaviour of its own; each launch is one browser-level client of
20
- * the tab's CDP endpoint, and `Browser.close` ends the process as it ends
21
- * Chromium's.
22
- */
23
- export function chromiumFaceRunner(attachClient: (peer: ProtocolPeer) => void): BrowserProgramRunner {
24
- return async (argv, options) => {
25
- const name = argv[0]?.slice(argv[0].lastIndexOf("/") + 1) ?? "chrome";
26
- if (!argv.includes("--remote-debugging-pipe")) {
27
- return { stdout: "", stderr: `${name}: the tab's Chromium is driven over --remote-debugging-pipe only\n`, exitCode: 1 };
28
- }
29
- const commands = options.descriptors?.[3];
30
- const replies = options.descriptors?.[4];
31
- if (!commands || !replies) {
32
- return { stdout: "", stderr: `${name}: --remote-debugging-pipe needs pipes on fds 3 and 4\n`, exitCode: 1 };
33
- }
34
- const messageListeners: ((data: { toString(): string }) => void)[] = [];
35
- const closeListeners: (() => void)[] = [];
36
- let open = true;
37
- let finished!: () => void;
38
- const done = new Promise<void>((resolve) => { finished = resolve; });
39
- const end = (): void => {
40
- if (!open) return;
41
- open = false;
42
- replies.end();
43
- for (const listener of closeListeners) listener();
44
- finished();
45
- };
46
- const peer: ProtocolPeer = {
47
- get readyState() { return open ? 1 : 3; },
48
- send: (data) => { if (open) replies.write(encoder.encode(`${data}\0`)); },
49
- close: () => end(),
50
- on: ((event: "message" | "close", listener: ((data: { toString(): string }) => void) | (() => void)) => {
51
- if (event === "message") messageListeners.push(listener as (data: { toString(): string }) => void);
52
- else closeListeners.push(listener as () => void);
53
- }) as ProtocolPeer["on"],
54
- };
55
- options.signal?.addEventListener("abort", end, { once: true });
56
- attachClient(peer);
57
- void (async () => {
58
- const decoder = new TextDecoder();
59
- let buffered = "";
60
- for await (const chunk of commands.input) {
61
- buffered += decoder.decode(chunk, { stream: true });
62
- for (let boundary = buffered.indexOf("\0"); boundary >= 0; boundary = buffered.indexOf("\0")) {
63
- const message = buffered.slice(0, boundary);
64
- buffered = buffered.slice(boundary + 1);
65
- const closing = browserClose(message);
66
- if (closing !== undefined) { peer.send(JSON.stringify({ id: closing, result: {} })); end(); return; }
67
- for (const listener of messageListeners) listener(message);
68
- if (!open) return;
69
- }
70
- }
71
- end();
72
- })().catch(end);
73
- await done;
74
- return { stdout: "", stderr: "", exitCode: 0 };
75
- };
76
- }
77
-
78
- /** The id of a browser-level `Browser.close`, which ends the process rather than reaching the endpoint. */
79
- function browserClose(message: string): number | undefined {
80
- try {
81
- const parsed = JSON.parse(message) as { id?: unknown; method?: unknown; sessionId?: unknown };
82
- return parsed.method === "Browser.close" && parsed.sessionId === undefined && typeof parsed.id === "number" ? parsed.id : undefined;
83
- } catch { return undefined; }
84
- }
85
-
86
- /** The Chromium revisions pinned by each `playwright-core` installed under the given roots. */
87
- export async function playwrightChromiumRevisions(filesystem: BrowserFileSystem, roots: readonly string[]): Promise<string[]> {
88
- const revisions = new Set<string>();
89
- for (const root of roots) {
90
- const path = `${root.replace(/\/$/u, "")}/node_modules/playwright-core/browsers.json`;
91
- if (!filesystem.existsSync(path)) continue;
92
- try {
93
- const pinned = JSON.parse(await filesystem.readFile(path, "utf8")) as { browsers?: { name?: unknown; revision?: unknown }[] };
94
- for (const entry of pinned.browsers ?? []) {
95
- if ((entry.name === "chromium" || entry.name === "chromium-headless-shell") && typeof entry.revision === "string") revisions.add(entry.revision);
96
- }
97
- } catch { /* A browsers.json that does not parse pins nothing. */ }
98
- }
99
- return [...revisions];
100
- }
101
-
102
- /**
103
- * Places the face where Playwright's registry looks for each revision, marked
104
- * installed and host-validated as the registry marks what it downloads, so
105
- * `playwright install` finds it present and a launch resolves the path to the
106
- * registered program: the file is the engine's registered-program stub, which
107
- * it relays to the host rather than running.
108
- */
109
- export async function placeChromiumFace(filesystem: BrowserFileSystem, cacheDirectory: string, revisions: readonly string[]): Promise<void> {
110
- for (const revision of revisions) {
111
- for (const placement of PLACEMENTS) {
112
- const root = `${cacheDirectory}/${placement.directory}-${revision}`;
113
- const executable = `${root}/${placement.executable}`;
114
- filesystem.mkdirSync(executable.slice(0, executable.lastIndexOf("/")), { recursive: true });
115
- // The registered-program stub the engine relays to the host, which maps
116
- // the pathname to the face by its basename.
117
- const name = placement.executable.slice(placement.executable.lastIndexOf("/") + 1);
118
- const stub = `#!/bin/sh\n# ${name}: runtime-registered program\n`;
119
- let current: string | undefined;
120
- try { current = filesystem.existsSync(executable) ? await filesystem.readFile(executable, "utf8") : undefined; } catch { current = undefined; }
121
- if (current !== stub) {
122
- filesystem.writeFileSync(executable, stub);
123
- filesystem.chmodSync?.(executable, 0o755);
124
- }
125
- for (const marker of ["INSTALLATION_COMPLETE", "DEPENDENCIES_VALIDATED"]) {
126
- if (!filesystem.existsSync(`${root}/${marker}`)) filesystem.writeFileSync(`${root}/${marker}`, "");
127
- }
128
- }
129
- }
130
- }