diff --git a/src/browser.ts b/src/browser.ts index b15b5efa4..63221a3f7 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -14,8 +14,14 @@ import type { ChromeReleaseChannel, LaunchOptions, Target, + BrowsersChromeReleaseChannel, +} from './third_party/index.js'; +import { + puppeteer, + resolveDefaultUserDataDir, + detectBrowserPlatform, + BrowserEnum, } from './third_party/index.js'; -import {puppeteer} from './third_party/index.js'; let browser: Browser | undefined; @@ -49,6 +55,7 @@ export async function ensureBrowserConnected(options: { wsHeaders?: Record; devtools: boolean; channel?: Channel; + userDataDir?: string; }) { const {channel} = options; if (browser?.connected) { @@ -68,15 +75,57 @@ export async function ensureBrowserConnected(options: { } } else if (options.browserURL) { connectOptions.browserURL = options.browserURL; - } else if (channel) { - const puppeteerChannel = - channel !== 'stable' - ? (`chrome-${channel}` as ChromeReleaseChannel) - : 'chrome'; - connectOptions.channel = puppeteerChannel; + } else if (channel || options.userDataDir) { + let userDataDir = options.userDataDir; + if (!userDataDir) { + if (!channel) { + throw new Error('Channel must be provided if userDataDir is missing'); + } + const platform = detectBrowserPlatform(); + if (!platform) { + throw new Error('Could not detect required browser platform'); + } + userDataDir = resolveDefaultUserDataDir( + BrowserEnum.CHROME, + platform, + (channel === 'stable' + ? 'chrome' + : `chrome-${channel}`) as BrowsersChromeReleaseChannel, + ); + } + + // TODO: re-expose this logic via Puppeteer. + const portPath = path.join(userDataDir, 'DevToolsActivePort'); + try { + const fileContent = await fs.promises.readFile(portPath, 'utf8'); + const [rawPort, rawPath] = fileContent + .split('\n') + .map(line => { + return line.trim(); + }) + .filter(line => { + return !!line; + }); + if (!rawPort || !rawPath) { + throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`); + } + const port = parseInt(rawPort, 10); + if (isNaN(port) || port <= 0 || port > 65535) { + throw new Error(`Invalid port '${rawPort}' found`); + } + const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`; + connectOptions.browserWSEndpoint = browserWSEndpoint; + } catch (error) { + throw new Error( + `Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled.`, + { + cause: error, + }, + ); + } } else { throw new Error( - 'Either browserURL, wsEndpoint or channel must be provided', + 'Either browserURL, wsEndpoint, channel or userDataDir must be provided', ); } diff --git a/src/cli.ts b/src/cli.ts index 9da407510..3e25f8d6c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,7 +12,7 @@ export const cliOptions = { type: 'boolean', description: 'If specified, automatically connects to a browser (Chrome 145+) running in the user data directory identified by the channel param.', - conflicts: ['isolated', 'executablePath', 'userDataDir'], + conflicts: ['isolated', 'executablePath'], default: false, coerce: (value: boolean | undefined) => { if (!value) { diff --git a/src/main.ts b/src/main.ts index e785e9c74..ca02a6472 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,6 +61,7 @@ async function getContext(): Promise { wsHeaders: args.wsHeaders, // Important: only pass channel, if autoConnect is true. channel: args.autoConnect ? (args.channel as Channel) : undefined, + userDataDir: args.userDataDir, devtools, }) : await ensureBrowserLaunched({ diff --git a/src/third_party/index.ts b/src/third_party/index.ts index 83194e38a..fc589fcc0 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -29,3 +29,9 @@ export { export {default as puppeteer} from 'puppeteer-core'; export type * from 'puppeteer-core'; export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js'; +export { + resolveDefaultUserDataDir, + detectBrowserPlatform, + Browser as BrowserEnum, + type ChromeReleaseChannel as BrowsersChromeReleaseChannel, +} from '@puppeteer/browsers'; diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 3c8e31ca8..1519a62fd 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -11,7 +11,7 @@ import {describe, it} from 'node:test'; import {executablePath} from 'puppeteer'; -import {launch} from '../src/browser.js'; +import {ensureBrowserConnected, launch} from '../src/browser.js'; describe('browser', () => { it('cannot launch multiple times with the same profile', async () => { @@ -73,4 +73,27 @@ describe('browser', () => { await browser.close(); } }); + it('connects to an existing browser with userDataDir', async () => { + const tmpDir = os.tmpdir(); + const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`); + const browser = await launch({ + headless: true, + isolated: false, + userDataDir: folderPath, + executablePath: executablePath(), + devtools: false, + args: ['--remote-debugging-port=0'], + }); + try { + const connectedBrowser = await ensureBrowserConnected({ + userDataDir: folderPath, + devtools: false, + }); + assert.ok(connectedBrowser); + assert.ok(connectedBrowser.isConnected()); + connectedBrowser.disconnect(); + } finally { + await browser.close(); + } + }); });