mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(browser): load Playwright runtime on demand (#127049)
* fix(browser): load Playwright runtime on demand * test(browser): use lazy Playwright accessor * fix(worker): bind bundled Playwright runtime * style(worker): format build regression * style(worker): apply repository formatter
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
// Build-visible bridge for playwright-core's private User-Agent helper.
|
||||
import coreBundle from "playwright-core/lib/coreBundle";
|
||||
|
||||
export default coreBundle;
|
||||
@@ -1,15 +1,22 @@
|
||||
/**
|
||||
* Playwright runtime loader.
|
||||
*
|
||||
* Static package imports keep the worker deploy build's executable closure visible
|
||||
* to the bundler while normal package builds may still externalize the dependency.
|
||||
* Loads playwright-core only when browser behavior needs it. The worker deploy
|
||||
* build declares its static dependency closure in worker-deploy-build-plugin.mts.
|
||||
*/
|
||||
import playwrightCoreDefault from "playwright-core";
|
||||
import { createRequire } from "node:module";
|
||||
import type * as PlaywrightCore from "playwright-core";
|
||||
import coreBundle from "./playwright-core-bundle.runtime.mjs";
|
||||
|
||||
/** Runtime playwright-core module instance. */
|
||||
export const playwrightCore = playwrightCoreDefault as typeof PlaywrightCore;
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/** Loads the Playwright runtime on first Browser use. */
|
||||
export function getPlaywrightCore(): typeof PlaywrightCore {
|
||||
return require("playwright-core") as typeof PlaywrightCore;
|
||||
}
|
||||
|
||||
/** Dependency-owned User-Agent used by Playwright's native CDP WebSocket transport. */
|
||||
export const getPlaywrightUserAgent = (coreBundle as { getUserAgent: () => string }).getUserAgent;
|
||||
export function getPlaywrightUserAgent(): string {
|
||||
return (
|
||||
require("playwright-core/lib/coreBundle") as { getUserAgent: () => string }
|
||||
).getUserAgent();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AddressInfo } from "node:net";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test-support.js";
|
||||
import { playwrightCore } from "./playwright-core.runtime.js";
|
||||
import { getPlaywrightCore } from "./playwright-core.runtime.js";
|
||||
import { ensurePageState } from "./pw-session-state.js";
|
||||
import { closePlaywrightBrowserConnection, getPageForTargetId } from "./pw-session.js";
|
||||
import { downloadViaPlaywright, waitForDownloadViaPlaywright } from "./pw-tools-core.downloads.js";
|
||||
@@ -83,7 +83,7 @@ describe.runIf(runChromiumProof)("managed Chromium download cancellation", () =>
|
||||
|
||||
const cdpPort = await getFreePort();
|
||||
const profileDir = path.join(rootDir, "profile");
|
||||
const context = await playwrightCore.chromium.launchPersistentContext(profileDir, {
|
||||
const context = await getPlaywrightCore().chromium.launchPersistentContext(profileDir, {
|
||||
headless: true,
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
args: [`--remote-debugging-port=${cdpPort}`],
|
||||
@@ -222,11 +222,14 @@ describe.runIf(runChromiumProof)("managed Chromium download cancellation", () =>
|
||||
await context.close();
|
||||
|
||||
const restartedCdpPort = await getFreePort();
|
||||
const restartedContext = await playwrightCore.chromium.launchPersistentContext(profileDir, {
|
||||
headless: true,
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
args: [`--remote-debugging-port=${restartedCdpPort}`],
|
||||
});
|
||||
const restartedContext = await getPlaywrightCore().chromium.launchPersistentContext(
|
||||
profileDir,
|
||||
{
|
||||
headless: true,
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
args: [`--remote-debugging-port=${restartedCdpPort}`],
|
||||
},
|
||||
);
|
||||
cleanup.push(async () => await restartedContext.close());
|
||||
const restartedPage = restartedContext.pages()[0] ?? (await restartedContext.newPage());
|
||||
await restartedPage.goto(`http://127.0.0.1:${downloadPort}/`);
|
||||
|
||||
@@ -4,9 +4,7 @@ import type { Browser, ConnectOverCDPTransport } from "playwright-core";
|
||||
import WebSocket from "ws";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { openCdpWebSocket } from "./cdp.helpers.js";
|
||||
import { playwrightCore } from "./playwright-core.runtime.js";
|
||||
|
||||
const { chromium } = playwrightCore;
|
||||
import { getPlaywrightCore } from "./playwright-core.runtime.js";
|
||||
type CdpSocketLookup = typeof dnsLookupCb;
|
||||
|
||||
export async function connectOverCdpPinnedTransport(
|
||||
@@ -130,7 +128,7 @@ export async function connectOverCdpPinnedTransport(
|
||||
ws.on("error", (error) => {
|
||||
scheduleTransportClosed(formatErrorMessage(error));
|
||||
});
|
||||
return await chromium.connectOverCDP(transport, { timeout: opts.timeout });
|
||||
return await getPlaywrightCore().chromium.connectOverCDP(transport, { timeout: opts.timeout });
|
||||
} catch (error) {
|
||||
ws.close();
|
||||
throw error;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./cdp.helpers.js";
|
||||
import { getChromeWebSocketEndpoint } from "./chrome.js";
|
||||
import { BrowserTabNotFoundError } from "./errors.js";
|
||||
import { playwrightCore } from "./playwright-core.runtime.js";
|
||||
import { getPlaywrightCore } from "./playwright-core.runtime.js";
|
||||
import { connectOverCdpPinnedTransport } from "./pw-session-cdp-transport.js";
|
||||
import {
|
||||
blockedPageRefsByCdpUrl,
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
targetKey,
|
||||
} from "./pw-session-state.js";
|
||||
|
||||
const { chromium } = playwrightCore;
|
||||
type CdpEndpointPin = NonNullable<Awaited<ReturnType<typeof assertCdpEndpointAllowed>>>;
|
||||
|
||||
function resolveCdpConnectRetryDelayMs(attempt: number): number {
|
||||
@@ -452,7 +451,10 @@ export async function connectBrowser(
|
||||
lookup,
|
||||
});
|
||||
}
|
||||
return await chromium.connectOverCDP(connectionUrl, { timeout, headers });
|
||||
return await getPlaywrightCore().chromium.connectOverCDP(connectionUrl, {
|
||||
timeout,
|
||||
headers,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,12 +14,12 @@ export const getChromeWebSocketEndpointMock: MockFn = vi.fn();
|
||||
|
||||
vi.mock("./playwright-core.runtime.js", () => ({
|
||||
getPlaywrightUserAgent: () => "Playwright/test",
|
||||
playwrightCore: {
|
||||
getPlaywrightCore: () => ({
|
||||
chromium: {
|
||||
connectOverCDP: (...args: unknown[]) => connectOverCdpMock(...args),
|
||||
},
|
||||
devices: {},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./chrome.js", () => ({
|
||||
|
||||
@@ -35,7 +35,7 @@ const stateMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("./playwright-core.runtime.js", () => ({
|
||||
playwrightCore: { devices: stateMocks.devices },
|
||||
getPlaywrightCore: () => ({ devices: stateMocks.devices }),
|
||||
}));
|
||||
|
||||
vi.mock("./pw-session.js", () => ({
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
*/
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { CDPSession, Page } from "playwright-core";
|
||||
import { playwrightCore } from "./playwright-core.runtime.js";
|
||||
import { getPlaywrightCore } from "./playwright-core.runtime.js";
|
||||
import type { PageState } from "./pw-session-contracts.js";
|
||||
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
|
||||
|
||||
const { devices: playwrightDevices } = playwrightCore;
|
||||
|
||||
type DeviceSize = { width: number; height: number };
|
||||
type PageCdpSend = (method: string, params?: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
@@ -252,7 +250,7 @@ export async function setDeviceViaPlaywright(opts: {
|
||||
if (!name) {
|
||||
throw new Error("device name is required");
|
||||
}
|
||||
const descriptor = (playwrightDevices as Record<string, unknown>)[name] as
|
||||
const descriptor = (getPlaywrightCore().devices as Record<string, unknown>)[name] as
|
||||
| PlaywrightDeviceDescriptor
|
||||
| undefined;
|
||||
if (!descriptor) {
|
||||
|
||||
@@ -11,6 +11,10 @@ const PLAYWRIGHT_BROWSER_REGISTRY_INIT =
|
||||
' registry = new Registry(require(import_path20.default.join(packageRoot, "browsers.json")));';
|
||||
const WORKER_BROWSER_RUNTIME_COMPOSITION = `import { createAttachedBrowserToolRuntime } from "../../extensions/browser/runtime-api.js";
|
||||
export default { createAttachedBrowserToolRuntime };`;
|
||||
const WORKER_PLAYWRIGHT_RUNTIME = `import * as playwrightCore from "playwright-core";
|
||||
import { getUserAgent } from "playwright-core/lib/coreBundle";
|
||||
export function getPlaywrightCore() { return playwrightCore; }
|
||||
export function getPlaywrightUserAgent() { return getUserAgent(); }`;
|
||||
const UNDICI_REQUIRE_BOOTSTRAP = [
|
||||
'import { createRequire } from "node:module";',
|
||||
"const requireUndici = createRequire(import.meta.url);\n",
|
||||
@@ -25,6 +29,9 @@ export function createWorkerDeployBuildPlugin(rootDir = process.cwd()) {
|
||||
const browserRuntimeBridgePath = fs.realpathSync(
|
||||
path.resolve("src/worker/worker-deploy-browser-runtime.ts"),
|
||||
);
|
||||
const playwrightRuntimePath = fs.realpathSync(
|
||||
path.resolve("extensions/browser/src/browser/playwright-core.runtime.ts"),
|
||||
);
|
||||
const undiciDispatcherOptionsPath = fs.realpathSync(
|
||||
path.resolve("src/infra/net/undici-dispatcher-options.ts"),
|
||||
);
|
||||
@@ -55,6 +62,9 @@ export function createWorkerDeployBuildPlugin(rootDir = process.cwd()) {
|
||||
if (resolvedId === browserRuntimeBridgePath) {
|
||||
return WORKER_BROWSER_RUNTIME_COMPOSITION;
|
||||
}
|
||||
if (resolvedId === playwrightRuntimePath) {
|
||||
return WORKER_PLAYWRIGHT_RUNTIME;
|
||||
}
|
||||
if (resolvedId === undiciDispatcherOptionsPath) {
|
||||
if (UNDICI_REQUIRE_BOOTSTRAP.some((fragment) => !code.includes(fragment))) {
|
||||
this.error("undici dispatcher bootstrap changed; update the worker deploy transform");
|
||||
|
||||
@@ -35,6 +35,20 @@ describe("worker deploy build plugin", () => {
|
||||
expect(transformed).not.toContain("was not composed by the build");
|
||||
});
|
||||
|
||||
it("binds the lazy Playwright accessor to bundled modules", () => {
|
||||
const runtimePath = path.resolve("extensions/browser/src/browser/playwright-core.runtime.ts");
|
||||
const source = fs.readFileSync(runtimePath, "utf8");
|
||||
const plugin = createWorkerDeployBuildPlugin();
|
||||
|
||||
const transformed = plugin.transform.call({ error: fail }, source, runtimePath);
|
||||
|
||||
expect(transformed).toContain('import * as playwrightCore from "playwright-core";');
|
||||
expect(transformed).toContain('import { getUserAgent } from "playwright-core/lib/coreBundle";');
|
||||
expect(transformed).toContain("return playwrightCore;");
|
||||
expect(transformed).not.toContain("createRequire");
|
||||
expect(transformed).not.toContain('require("playwright-core")');
|
||||
});
|
||||
|
||||
it("bundles the undici dispatcher dependency without a worker runtime require", () => {
|
||||
const dispatcherPath = path.resolve("src/infra/net/undici-dispatcher-options.ts");
|
||||
const source = fs.readFileSync(dispatcherPath, "utf8");
|
||||
|
||||
Reference in New Issue
Block a user