Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
311 lines
No EOL
14 KiB
Text
311 lines
No EOL
14 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2017 Google Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import { existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { Browser as InstalledBrowser, CDP_WEBSOCKET_ENDPOINT_REGEX, launch, TimeoutError as BrowsersTimeoutError, WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, computeExecutablePath, } from '@puppeteer/browsers';
|
|
import { firstValueFrom, from, map, race, timer, } from '../../third_party/rxjs/rxjs.js';
|
|
import { CdpBrowser } from '../cdp/Browser.js';
|
|
import { Connection } from '../cdp/Connection.js';
|
|
import { TimeoutError } from '../common/Errors.js';
|
|
import { debugError, DEFAULT_VIEWPORT } from '../common/util.js';
|
|
import { createIncrementalIdGenerator, } from '../util/incremental-id-generator.js';
|
|
import { NodeWebSocketTransport as WebSocketTransport } from './NodeWebSocketTransport.js';
|
|
import { PipeTransport } from './PipeTransport.js';
|
|
/**
|
|
* Describes a launcher - a class that is able to create and launch a browser instance.
|
|
*
|
|
* @public
|
|
*/
|
|
export class BrowserLauncher {
|
|
#browser;
|
|
/**
|
|
* @internal
|
|
*/
|
|
puppeteer;
|
|
/**
|
|
* @internal
|
|
*/
|
|
constructor(puppeteer, browser) {
|
|
this.puppeteer = puppeteer;
|
|
this.#browser = browser;
|
|
}
|
|
get browser() {
|
|
return this.#browser;
|
|
}
|
|
async launch(options = {}) {
|
|
const { dumpio = false, enableExtensions = false, env = process.env, handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, acceptInsecureCerts = false, networkEnabled = true, defaultViewport = DEFAULT_VIEWPORT, downloadBehavior, slowMo = 0, timeout = 30000, waitForInitialPage = true, protocolTimeout, handleDevToolsAsPage, idGenerator = createIncrementalIdGenerator(), } = options;
|
|
let { protocol } = options;
|
|
// Default to 'webDriverBiDi' for Firefox.
|
|
if (this.#browser === 'firefox' && protocol === undefined) {
|
|
protocol = 'webDriverBiDi';
|
|
}
|
|
if (this.#browser === 'firefox' && protocol === 'cdp') {
|
|
throw new Error('Connecting to Firefox using CDP is no longer supported');
|
|
}
|
|
const launchArgs = await this.computeLaunchArguments({
|
|
...options,
|
|
protocol,
|
|
});
|
|
if (!existsSync(launchArgs.executablePath)) {
|
|
throw new Error(`Browser was not found at the configured executablePath (${launchArgs.executablePath})`);
|
|
}
|
|
const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
|
|
const onProcessExit = async () => {
|
|
await this.cleanUserDataDir(launchArgs.userDataDir, {
|
|
isTemp: launchArgs.isTempUserDataDir,
|
|
});
|
|
};
|
|
if (this.#browser === 'firefox' &&
|
|
protocol === 'webDriverBiDi' &&
|
|
usePipe) {
|
|
throw new Error('Pipe connections are not supported with Firefox and WebDriver BiDi');
|
|
}
|
|
const browserProcess = launch({
|
|
executablePath: launchArgs.executablePath,
|
|
args: launchArgs.args,
|
|
handleSIGHUP,
|
|
handleSIGTERM,
|
|
handleSIGINT,
|
|
dumpio,
|
|
env,
|
|
pipe: usePipe,
|
|
onExit: onProcessExit,
|
|
});
|
|
let browser;
|
|
let cdpConnection;
|
|
let closing = false;
|
|
const browserCloseCallback = async () => {
|
|
if (closing) {
|
|
return;
|
|
}
|
|
closing = true;
|
|
await this.closeBrowser(browserProcess, cdpConnection);
|
|
};
|
|
try {
|
|
if (this.#browser === 'firefox') {
|
|
browser = await this.createBiDiBrowser(browserProcess, browserCloseCallback, {
|
|
timeout,
|
|
protocolTimeout,
|
|
slowMo,
|
|
defaultViewport,
|
|
acceptInsecureCerts,
|
|
networkEnabled,
|
|
idGenerator,
|
|
});
|
|
}
|
|
else {
|
|
if (usePipe) {
|
|
cdpConnection = await this.createCdpPipeConnection(browserProcess, {
|
|
timeout,
|
|
protocolTimeout,
|
|
slowMo,
|
|
idGenerator,
|
|
});
|
|
}
|
|
else {
|
|
cdpConnection = await this.createCdpSocketConnection(browserProcess, {
|
|
timeout,
|
|
protocolTimeout,
|
|
slowMo,
|
|
idGenerator,
|
|
});
|
|
}
|
|
if (protocol === 'webDriverBiDi') {
|
|
browser = await this.createBiDiOverCdpBrowser(browserProcess, cdpConnection, browserCloseCallback, {
|
|
defaultViewport,
|
|
acceptInsecureCerts,
|
|
networkEnabled,
|
|
});
|
|
}
|
|
else {
|
|
browser = await CdpBrowser._create(cdpConnection, [], acceptInsecureCerts, defaultViewport, downloadBehavior, browserProcess.nodeProcess, browserCloseCallback, options.targetFilter, undefined, undefined, networkEnabled, handleDevToolsAsPage);
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
void browserCloseCallback();
|
|
const logs = browserProcess.getRecentLogs().join('\n');
|
|
if (logs.includes('Failed to create a ProcessSingleton for your profile directory') ||
|
|
// On Windows we will not get logs due to the singleton process
|
|
// handover. See
|
|
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/process_singleton_win.cc;l=46;drc=fc7952f0422b5073515a205a04ec9c3a1ae81658
|
|
(process.platform === 'win32' &&
|
|
existsSync(join(launchArgs.userDataDir, 'lockfile')))) {
|
|
throw new Error(`The browser is already running for ${launchArgs.userDataDir}. Use a different \`userDataDir\` or stop the running browser first.`);
|
|
}
|
|
if (logs.includes('Missing X server') && options.headless === false) {
|
|
throw new Error(`Missing X server to start the headful browser. Either set headless to true or use xvfb-run to run your Puppeteer script.`);
|
|
}
|
|
if (error instanceof BrowsersTimeoutError) {
|
|
throw new TimeoutError(error.message);
|
|
}
|
|
throw error;
|
|
}
|
|
if (Array.isArray(enableExtensions)) {
|
|
if (this.#browser === 'chrome' && !usePipe) {
|
|
throw new Error('To use `enableExtensions` with a list of paths in Chrome, you must be connected with `--remote-debugging-pipe` (`pipe: true`).');
|
|
}
|
|
await Promise.all([
|
|
enableExtensions.map(path => {
|
|
return browser.installExtension(path);
|
|
}),
|
|
]);
|
|
}
|
|
if (waitForInitialPage) {
|
|
await this.waitForPageTarget(browser, timeout);
|
|
}
|
|
return browser;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async closeBrowser(browserProcess, cdpConnection) {
|
|
if (cdpConnection) {
|
|
// Attempt to close the browser gracefully
|
|
try {
|
|
await cdpConnection.closeBrowser();
|
|
await browserProcess.hasClosed();
|
|
}
|
|
catch (error) {
|
|
debugError(error);
|
|
await browserProcess.close();
|
|
}
|
|
}
|
|
else {
|
|
// Wait for a possible graceful shutdown.
|
|
await firstValueFrom(race(from(browserProcess.hasClosed()), timer(5000).pipe(map(() => {
|
|
return from(browserProcess.close());
|
|
}))));
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async waitForPageTarget(browser, timeout) {
|
|
try {
|
|
await browser.waitForTarget(t => {
|
|
return t.type() === 'page';
|
|
}, { timeout });
|
|
}
|
|
catch (error) {
|
|
await browser.close();
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async createCdpSocketConnection(browserProcess, opts) {
|
|
const browserWSEndpoint = await browserProcess.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX, opts.timeout);
|
|
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
|
return new Connection(browserWSEndpoint, transport, opts.slowMo, opts.protocolTimeout,
|
|
/* rawErrors */ false, opts.idGenerator);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async createCdpPipeConnection(browserProcess, opts) {
|
|
// stdio was assigned during start(), and the 'pipe' option there adds the
|
|
// 4th and 5th items to stdio array
|
|
const { 3: pipeWrite, 4: pipeRead } = browserProcess.nodeProcess.stdio;
|
|
const transport = new PipeTransport(pipeWrite, pipeRead);
|
|
return new Connection('', transport, opts.slowMo, opts.protocolTimeout,
|
|
/* rawErrors */ false, opts.idGenerator);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async createBiDiOverCdpBrowser(browserProcess, cdpConnection, closeCallback, opts) {
|
|
const bidiOnly = process.env['PUPPETEER_WEBDRIVER_BIDI_ONLY'] === 'true';
|
|
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
|
|
const bidiConnection = await BiDi.connectBidiOverCdp(cdpConnection);
|
|
return await BiDi.BidiBrowser.create({
|
|
connection: bidiConnection,
|
|
// Do not provide CDP connection to Browser, if BiDi-only mode is enabled. This
|
|
// would restrict Browser to use only BiDi endpoint.
|
|
cdpConnection: bidiOnly ? undefined : cdpConnection,
|
|
closeCallback,
|
|
process: browserProcess.nodeProcess,
|
|
defaultViewport: opts.defaultViewport,
|
|
acceptInsecureCerts: opts.acceptInsecureCerts,
|
|
networkEnabled: opts.networkEnabled,
|
|
});
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async createBiDiBrowser(browserProcess, closeCallback, opts) {
|
|
const browserWSEndpoint = (await browserProcess.waitForLineOutput(WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, opts.timeout)) + '/session';
|
|
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
|
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
|
|
const bidiConnection = new BiDi.BidiConnection(browserWSEndpoint, transport, opts.idGenerator, opts.slowMo, opts.protocolTimeout);
|
|
return await BiDi.BidiBrowser.create({
|
|
connection: bidiConnection,
|
|
closeCallback,
|
|
process: browserProcess.nodeProcess,
|
|
defaultViewport: opts.defaultViewport,
|
|
acceptInsecureCerts: opts.acceptInsecureCerts,
|
|
networkEnabled: opts.networkEnabled ?? true,
|
|
});
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getProfilePath() {
|
|
return join(this.puppeteer.configuration.temporaryDirectory ?? tmpdir(), `puppeteer_dev_${this.browser}_profile-`);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
resolveExecutablePath(headless, validatePath = true) {
|
|
let executablePath = this.puppeteer.configuration.executablePath;
|
|
if (executablePath) {
|
|
if (validatePath && !existsSync(executablePath)) {
|
|
throw new Error(`Tried to find the browser at the configured path (${executablePath}), but no executable was found.`);
|
|
}
|
|
return executablePath;
|
|
}
|
|
function puppeteerBrowserToInstalledBrowser(browser, headless) {
|
|
switch (browser) {
|
|
case 'chrome':
|
|
if (headless === 'shell') {
|
|
return InstalledBrowser.CHROMEHEADLESSSHELL;
|
|
}
|
|
return InstalledBrowser.CHROME;
|
|
case 'firefox':
|
|
return InstalledBrowser.FIREFOX;
|
|
}
|
|
return InstalledBrowser.CHROME;
|
|
}
|
|
const browserType = puppeteerBrowserToInstalledBrowser(this.browser, headless);
|
|
executablePath = computeExecutablePath({
|
|
cacheDir: this.puppeteer.defaultDownloadPath,
|
|
browser: browserType,
|
|
buildId: this.puppeteer.browserVersion,
|
|
});
|
|
if (validatePath && !existsSync(executablePath)) {
|
|
const configVersion = this.puppeteer.configuration?.[this.browser]?.version;
|
|
if (configVersion) {
|
|
throw new Error(`Tried to find the browser at the configured path (${executablePath}) for version ${configVersion}, but no executable was found.`);
|
|
}
|
|
switch (this.browser) {
|
|
case 'chrome':
|
|
throw new Error(`Could not find Chrome (ver. ${this.puppeteer.browserVersion}). This can occur if either\n` +
|
|
` 1. you did not perform an installation before running the script (e.g. \`npx puppeteer browsers install ${browserType}\`) or\n` +
|
|
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
|
|
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.');
|
|
case 'firefox':
|
|
throw new Error(`Could not find Firefox (rev. ${this.puppeteer.browserVersion}). This can occur if either\n` +
|
|
' 1. you did not perform an installation for Firefox before running the script (e.g. `npx puppeteer browsers install firefox`) or\n' +
|
|
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
|
|
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.');
|
|
}
|
|
}
|
|
return executablePath;
|
|
}
|
|
}
|
|
//# sourceMappingURL=BrowserLauncher.js.map |