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
+
[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 ';
|