feat: support running Puppeteer in extensions (#12459)

This commit is contained in:
Alex Rudenko 2024-05-21 12:41:15 +02:00 committed by GitHub
parent c8a64c0f79
commit 3c6f01a31d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 335 additions and 3 deletions

View File

@ -215,6 +215,19 @@ The constructor for this class is marked as internal. Third-party code should no
</td></tr> </td></tr>
<tr><td> <tr><td>
<span id="extensiontransport">[ExtensionTransport](./puppeteer.extensiontransport.md)</span>
</td><td>
**_(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.
</td></tr>
<tr><td>
<span id="filechooser">[FileChooser](./puppeteer.filechooser.md)</span> <span id="filechooser">[FileChooser](./puppeteer.filechooser.md)</span>
</td><td> </td><td>

View File

@ -0,0 +1,17 @@
---
sidebar_label: ExtensionTransport.close
---
# ExtensionTransport.close() method
#### Signature:
```typescript
class ExtensionTransport {
close(): void;
}
```
**Returns:**
void

View File

@ -0,0 +1,44 @@
---
sidebar_label: ExtensionTransport.connectTab
---
# ExtensionTransport.connectTab() method
#### Signature:
```typescript
class ExtensionTransport {
static connectTab(tabId: number): Promise<ExtensionTransport>;
}
```
## Parameters
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
tabId
</td><td>
number
</td><td>
</td></tr>
</tbody></table>
**Returns:**
Promise&lt;[ExtensionTransport](./puppeteer.extensiontransport.md)&gt;

View File

@ -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
<table><thead><tr><th>
Property
</th><th>
Modifiers
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
<span id="onclose">onclose</span>
</td><td>
`optional`
</td><td>
() =&gt; void
</td><td>
</td></tr>
<tr><td>
<span id="onmessage">onmessage</span>
</td><td>
`optional`
</td><td>
(message: string) =&gt; void
</td><td>
</td></tr>
</tbody></table>
## Methods
<table><thead><tr><th>
Method
</th><th>
Modifiers
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
<span id="close">[close()](./puppeteer.extensiontransport.close.md)</span>
</td><td>
</td><td>
</td></tr>
<tr><td>
<span id="connecttab">[connectTab(tabId)](./puppeteer.extensiontransport.connecttab.md)</span>
</td><td>
`static`
</td><td>
</td></tr>
<tr><td>
<span id="send">[send(message)](./puppeteer.extensiontransport.send.md)</span>
</td><td>
</td><td>
</td></tr>
</tbody></table>

View File

@ -0,0 +1,44 @@
---
sidebar_label: ExtensionTransport.send
---
# ExtensionTransport.send() method
#### Signature:
```typescript
class ExtensionTransport {
send(message: string): void;
}
```
## Parameters
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
message
</td><td>
string
</td><td>
</td></tr>
</tbody></table>
**Returns:**
void

View File

@ -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'],
}),
],
};
```

View File

@ -53,6 +53,7 @@ export default {
// Indicate that we target a browser environment. // Indicate that we target a browser environment.
browser: true, browser: true,
// Exclude any dependencies except for puppeteer-core. // Exclude any dependencies except for puppeteer-core.
// `npm install puppeteer-core` # To install puppeteer-core if needed.
resolveOnly: ['puppeteer-core'], resolveOnly: ['puppeteer-core'],
}), }),
], ],

View File

@ -12,9 +12,25 @@ globalThis.testConnect = async url => {
const tab = await chrome.tabs.create({ const tab = await chrome.tabs.create({
url, 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({ const browser = await connect({
transport: await ExtensionTransport.connectTab(tab.id), transport: await ExtensionTransport.connectTab(tab.id),
}); });
const [page] = await browser.pages(); const [page] = await browser.pages();
return await page.evaluate('document.title'); const title = await page.evaluate(() => {
return document.title;
});
await browser.disconnect();
return title;
}; };

View File

@ -30,7 +30,7 @@ const pageTargetInfo = {
* implements missing commands and events. * implements missing commands and events.
* *
* @experimental * @experimental
* @internal * @public
*/ */
export class ExtensionTransport implements ConnectionTransport { export class ExtensionTransport implements ConnectionTransport {
static async connectTab(tabId: number): Promise<ExtensionTransport> { static async connectTab(tabId: number): Promise<ExtensionTransport> {
@ -178,5 +178,6 @@ export class ExtensionTransport implements ConnectionTransport {
close(): void { close(): void {
chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler); chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler);
void chrome.debugger.detach({tabId: this.#tabId});
} }
} }

View File

@ -31,7 +31,16 @@ export function stringifyFunction(fn: (...args: never) => unknown): string {
let value = fn.toString(); let value = fn.toString();
try { try {
new Function(`(${value})`); 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 // This means we might have a function shorthand (e.g. `test(){}`). Let's
// try prefixing. // try prefixing.
let prefix = 'function '; let prefix = 'function ';