diff --git a/docs/api/index.md b/docs/api/index.md index 409fecfe82c..751e25992a4 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -215,6 +215,19 @@ The constructor for this class is marked as internal. Third-party code should no +[ExtensionTransport](./puppeteer.extensiontransport.md) + + + +**_(Experimental)_** Experimental ExtensionTransport allows establishing a connection via chrome.debugger API if Puppeteer runs in an extension. Since Chrome DevTools Protocol is restricted for extensions, the transport implements missing commands and events. + +**Remarks:** + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ExtensionTransport` class. + + + + [FileChooser](./puppeteer.filechooser.md) diff --git a/docs/api/puppeteer.extensiontransport.close.md b/docs/api/puppeteer.extensiontransport.close.md new file mode 100644 index 00000000000..51f4bcb5721 --- /dev/null +++ b/docs/api/puppeteer.extensiontransport.close.md @@ -0,0 +1,17 @@ +--- +sidebar_label: ExtensionTransport.close +--- + +# ExtensionTransport.close() method + +#### Signature: + +```typescript +class ExtensionTransport { + close(): void; +} +``` + +**Returns:** + +void diff --git a/docs/api/puppeteer.extensiontransport.connecttab.md b/docs/api/puppeteer.extensiontransport.connecttab.md new file mode 100644 index 00000000000..2cfa5c19d38 --- /dev/null +++ b/docs/api/puppeteer.extensiontransport.connecttab.md @@ -0,0 +1,44 @@ +--- +sidebar_label: ExtensionTransport.connectTab +--- + +# ExtensionTransport.connectTab() method + +#### Signature: + +```typescript +class ExtensionTransport { + static connectTab(tabId: number): Promise; +} +``` + +## Parameters + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +tabId + + + +number + + + +
+**Returns:** + +Promise<[ExtensionTransport](./puppeteer.extensiontransport.md)> diff --git a/docs/api/puppeteer.extensiontransport.md b/docs/api/puppeteer.extensiontransport.md new file mode 100644 index 00000000000..344f962368f --- /dev/null +++ b/docs/api/puppeteer.extensiontransport.md @@ -0,0 +1,116 @@ +--- +sidebar_label: ExtensionTransport +--- + +# ExtensionTransport class + +Experimental ExtensionTransport allows establishing a connection via chrome.debugger API if Puppeteer runs in an extension. Since Chrome DevTools Protocol is restricted for extensions, the transport implements missing commands and events. + +#### Signature: + +```typescript +export declare class ExtensionTransport implements ConnectionTransport +``` + +**Implements:** [ConnectionTransport](./puppeteer.connectiontransport.md) + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ExtensionTransport` class. + +## Properties + + + + +
+ +Property + + + +Modifiers + + + +Type + + + +Description + +
+ +onclose + + + +`optional` + + + +() => void + + + +
+ +onmessage + + + +`optional` + + + +(message: string) => void + + + +
+ +## Methods + + + + + +
+ +Method + + + +Modifiers + + + +Description + +
+ +[close()](./puppeteer.extensiontransport.close.md) + + + + + +
+ +[connectTab(tabId)](./puppeteer.extensiontransport.connecttab.md) + + + +`static` + + + +
+ +[send(message)](./puppeteer.extensiontransport.send.md) + + + + + +
diff --git a/docs/api/puppeteer.extensiontransport.send.md b/docs/api/puppeteer.extensiontransport.send.md new file mode 100644 index 00000000000..f711e9ff07e --- /dev/null +++ b/docs/api/puppeteer.extensiontransport.send.md @@ -0,0 +1,44 @@ +--- +sidebar_label: ExtensionTransport.send +--- + +# ExtensionTransport.send() method + +#### Signature: + +```typescript +class ExtensionTransport { + send(message: string): void; +} +``` + +## Parameters + + + +
+ +Parameter + + + +Type + + + +Description + +
+ +message + + + +string + + + +
+**Returns:** + +void diff --git a/docs/guides/running-puppeteer-in-extensions.md b/docs/guides/running-puppeteer-in-extensions.md new file mode 100644 index 00000000000..61ef7f7986d --- /dev/null +++ b/docs/guides/running-puppeteer-in-extensions.md @@ -0,0 +1,71 @@ +# Running Puppeteer in Chrome extensions + +:::caution + +Chrome extensions environment is significantly different from the usual Node.JS environment, therefore, the support for running Puppeteer in chrome.debugger +is currently experimental. Please submit issues https://github.com/puppeteer/puppeteer/issues/new/choose if you encounted bugs. + +::: + +Chrome Extensions allow accessing Chrome DevTools Protocol via [`chrome.debugger`](https://developer.chrome.com/docs/extensions/reference/api/debugger). +[`chrome.debugger`](https://developer.chrome.com/docs/extensions/reference/api/debugger) provides a restricted access to CDP and allows attaching to one +page at a time. Therefore, Puppeteer requires a different transport to be used and Puppeteer's view is limited to a single page. It means you can +interact with a single page and its frames and workers but cannot create new pages using Puppeteer. To create a new page you need to use the +[`chrome.tabs`](https://developer.chrome.com/docs/extensions/reference/api/tabs) API and establish a new Puppeteer connection. + +## How to run Puppeteer in the browser + +:::note + +See https://github.com/puppeteer/puppeteer/tree/main/examples/puppeteer-in-extension for a complete example. + +::: + +To run Puppeteer in the an extension, first you need to produce a browser-compatible build using a bundler such as rollup or webpack: + +1. When importing Puppeteer use the browser-specific entrypoint from puppeteer-core `puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js'`: + +```ts +import { + connect, + ExtensionTransport, +} from 'puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js'; + +// Create a tab or find a tab to attach to. +const tab = await chrome.tabs.create({ + url, +}); +// Connect Puppeteer using the ExtensionTransport.connectTab. +const browser = await connect({ + transport: await ExtensionTransport.connectTab(tab.id), +}); +// You will have a single page on the browser object, which corresponds +// to the tab you connected the transport to. +const [page] = await browser.pages(); +// Perform the usual operations with Puppeteer page. +console.log(await page.evaluate('document.title')); +browser.disconnect(); +``` + +2. Build your extension using a bundler. For example, the following configuration can be used with rollup: + +```js +import {nodeResolve} from '@rollup/plugin-node-resolve'; + +export default { + input: 'main.mjs', + output: { + format: 'esm', + dir: 'out', + }, + plugins: [ + nodeResolve({ + // Indicate that we target a browser environment. + browser: true, + // Exclude any dependencies except for puppeteer-core. + // `npm install puppeteer-core` # To install puppeteer-core if needed. + resolveOnly: ['puppeteer-core'], + }), + ], +}; +``` diff --git a/docs/guides/running-puppeteer-in-the-browser.md b/docs/guides/running-puppeteer-in-the-browser.md index 5dacb855076..bcf7dd3bfae 100644 --- a/docs/guides/running-puppeteer-in-the-browser.md +++ b/docs/guides/running-puppeteer-in-the-browser.md @@ -53,6 +53,7 @@ export default { // Indicate that we target a browser environment. browser: true, // Exclude any dependencies except for puppeteer-core. + // `npm install puppeteer-core` # To install puppeteer-core if needed. resolveOnly: ['puppeteer-core'], }), ], diff --git a/examples/puppeteer-in-extension/background.js b/examples/puppeteer-in-extension/background.js index b05644dc6b5..2c9f8533c2d 100644 --- a/examples/puppeteer-in-extension/background.js +++ b/examples/puppeteer-in-extension/background.js @@ -12,9 +12,25 @@ globalThis.testConnect = async url => { const tab = await chrome.tabs.create({ url, }); + + // Wait for the new tab to load before connecting. + await new Promise(resolve => { + function listener(tabId, changeInfo) { + if (tabId === tab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + } + chrome.tabs.onUpdated.addListener(listener); + }); + const browser = await connect({ transport: await ExtensionTransport.connectTab(tab.id), }); const [page] = await browser.pages(); - return await page.evaluate('document.title'); + const title = await page.evaluate(() => { + return document.title; + }); + await browser.disconnect(); + return title; }; diff --git a/packages/puppeteer-core/src/cdp/ExtensionTransport.ts b/packages/puppeteer-core/src/cdp/ExtensionTransport.ts index 53c4c607790..9b07c2c5138 100644 --- a/packages/puppeteer-core/src/cdp/ExtensionTransport.ts +++ b/packages/puppeteer-core/src/cdp/ExtensionTransport.ts @@ -30,7 +30,7 @@ const pageTargetInfo = { * implements missing commands and events. * * @experimental - * @internal + * @public */ export class ExtensionTransport implements ConnectionTransport { static async connectTab(tabId: number): Promise { @@ -178,5 +178,6 @@ export class ExtensionTransport implements ConnectionTransport { close(): void { chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler); + void chrome.debugger.detach({tabId: this.#tabId}); } } diff --git a/packages/puppeteer-core/src/util/Function.ts b/packages/puppeteer-core/src/util/Function.ts index 497a31501dd..411fd401f4b 100644 --- a/packages/puppeteer-core/src/util/Function.ts +++ b/packages/puppeteer-core/src/util/Function.ts @@ -31,7 +31,16 @@ export function stringifyFunction(fn: (...args: never) => unknown): string { let value = fn.toString(); try { new Function(`(${value})`); - } catch { + } catch (err) { + if ( + (err as Error).message.includes( + `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive` + ) + ) { + // The content security policy does not allow Function eval. Let's + // assume the value might be valid as is. + return value; + } // This means we might have a function shorthand (e.g. `test(){}`). Let's // try prefixing. let prefix = 'function ';