Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support node-like environments #8490

Merged
merged 1 commit into from Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/common/BrowserConnector.ts
Expand Up @@ -14,18 +14,18 @@
* limitations under the License.
*/

import { ConnectionTransport } from './ConnectionTransport.js';
import { debugError } from '../common/helper.js';
import { isNode } from '../environment.js';
import { assert } from './assert.js';
import {
Browser,
TargetFilterCallback,
IsPageTargetCallback,
TargetFilterCallback,
} from './Browser.js';
import { assert } from './assert.js';
import { debugError } from '../common/helper.js';
import { Connection } from './Connection.js';
import { Viewport } from './PuppeteerViewport.js';
import { isNode } from '../environment.js';
import { ConnectionTransport } from './ConnectionTransport.js';
import { getFetch } from './fetch.js';
import { Viewport } from './PuppeteerViewport.js';

/**
* Generic browser options that can be passed when launching any browser or when
Expand Down
68 changes: 39 additions & 29 deletions src/common/DOMWorld.ts
Expand Up @@ -14,30 +14,29 @@
* limitations under the License.
*/

import { Protocol } from 'devtools-protocol';
import { assert } from './assert.js';
import { helper, debugError } from './helper.js';
import {
LifecycleWatcher,
PuppeteerLifeCycleEvent,
} from './LifecycleWatcher.js';
import { CDPSession } from './Connection.js';
import { TimeoutError } from './Errors.js';
import { JSHandle, ElementHandle } from './JSHandle.js';
import { ExecutionContext } from './ExecutionContext.js';
import { TimeoutSettings } from './TimeoutSettings.js';
import { MouseButton } from './Input.js';
import { FrameManager, Frame } from './FrameManager.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import {
SerializableOrJSHandle,
EvaluateHandleFn,
WrapElementHandle,
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import { isNode } from '../environment.js';
import { Protocol } from 'devtools-protocol';
import { CDPSession } from './Connection.js';
import { ExecutionContext } from './ExecutionContext.js';
import { Frame, FrameManager } from './FrameManager.js';
import { debugError, helper } from './helper.js';
import { MouseButton } from './Input.js';
import { ElementHandle, JSHandle } from './JSHandle.js';
import {
LifecycleWatcher,
PuppeteerLifeCycleEvent,
} from './LifecycleWatcher.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { TimeoutSettings } from './TimeoutSettings.js';

// predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below.
Expand Down Expand Up @@ -330,13 +329,18 @@ export class DOMWorld {
}

if (path !== null) {
if (!isNode) {
throw new Error(
'Cannot pass a filepath to addScriptTag in the browser environment.'
);
let fs;
try {
fs = (await import('fs')).promises;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Can only pass a filepath to addScriptTag in a Node-like environment.'
);
}
throw error;
}
const fs = await import('fs');
let contents = await fs.promises.readFile(path, 'utf8');
let contents = await fs.readFile(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
const context = await this.executionContext();
const handle = await context.evaluateHandle(
Expand Down Expand Up @@ -437,13 +441,19 @@ export class DOMWorld {
}

if (path !== null) {
if (!isNode) {
throw new Error(
'Cannot pass a filepath to addStyleTag in the browser environment.'
);
let fs: typeof import('fs').promises;
try {
fs = (await import('fs')).promises;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Cannot pass a filepath to addStyleTag in the browser environment.'
);
}
throw error;
}
const fs = await import('fs');
let contents = await fs.promises.readFile(path, 'utf8');

let contents = await fs.readFile(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
const context = await this.executionContext();
const handle = await context.evaluateHandle(addStyleContent, contents);
Expand Down
40 changes: 20 additions & 20 deletions src/common/JSHandle.ts
Expand Up @@ -14,25 +14,24 @@
* limitations under the License.
*/

import { Protocol } from 'devtools-protocol';
import { assert } from './assert.js';
import { helper, debugError } from './helper.js';
import { ExecutionContext } from './ExecutionContext.js';
import { Page, ScreenshotOptions } from './Page.js';
import { CDPSession } from './Connection.js';
import { KeyInput } from './USKeyboardLayout.js';
import { FrameManager, Frame } from './FrameManager.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { Protocol } from 'devtools-protocol';
import {
EvaluateFn,
SerializableOrJSHandle,
EvaluateFnReturnType,
EvaluateHandleFn,
WrapElementHandle,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import { isNode } from '../environment.js';
import { ExecutionContext } from './ExecutionContext.js';
import { Frame, FrameManager } from './FrameManager.js';
import { debugError, helper } from './helper.js';
import { MouseButton } from './Input.js';
import { Page, ScreenshotOptions } from './Page.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { KeyInput } from './USKeyboardLayout.js';

/**
* @public
Expand Down Expand Up @@ -822,17 +821,18 @@ export class ElementHandle<
'Multiple file uploads only work with <input type=file multiple>'
);

if (!isNode) {
throw new Error(
`JSHandle#uploadFile can only be used in Node environments.`
);
}
/*
This import is only needed for `uploadFile`, so keep it scoped here to
avoid paying the cost unnecessarily.
*/
const path = await import('path');
// Locate all files and confirm that they exist.
let path: typeof import('path');
try {
path = await import('path');
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
`JSHandle#uploadFile can only be used in Node-like environments.`
);
}
throw error;
}
const files = filePaths.map((filePath) => {
if (path.isAbsolute(filePath)) {
return filePath;
Expand Down
17 changes: 10 additions & 7 deletions src/common/Page.ts
Expand Up @@ -16,7 +16,6 @@

import { Protocol } from 'devtools-protocol';
import type { Readable } from 'stream';
import { isNode } from '../environment.js';
import { Accessibility } from './Accessibility.js';
import { assert, assertNever } from './assert.js';
import { Browser, BrowserContext } from './Browser.js';
Expand Down Expand Up @@ -2825,13 +2824,17 @@ export class Page extends EventEmitter {
: Buffer.from(result.data, 'base64');

if (options.path) {
if (!isNode) {
throw new Error(
'Screenshots can only be written to a file path in a Node environment.'
);
try {
const fs = (await import('fs')).promises;
await fs.writeFile(options.path, buffer);
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Screenshots can only be written to a file path in a Node-like environment.'
);
}
throw error;
}
const fs = (await import('fs')).promises;
await fs.writeFile(options.path, buffer);
}
return buffer;

Expand Down
4 changes: 1 addition & 3 deletions src/common/fetch.ts
Expand Up @@ -14,9 +14,7 @@
* limitations under the License.
*/

import { isNode } from '../environment.js';

/* Use the global version if we're in the browser, else load the node-fetch module. */
export const getFetch = async (): Promise<typeof fetch> => {
return isNode ? (await import('cross-fetch')).fetch : globalThis.fetch;
return globalThis.fetch || (await import('cross-fetch')).fetch;
};
60 changes: 31 additions & 29 deletions src/common/helper.ts
Expand Up @@ -14,15 +14,14 @@
* limitations under the License.
*/

import { Protocol } from 'devtools-protocol';
import type { Readable } from 'stream';

import { TimeoutError } from './Errors.js';
import { debug } from './Debug.js';
import { isNode } from '../environment.js';
import { assert } from './assert.js';
import { CDPSession } from './Connection.js';
import { Protocol } from 'devtools-protocol';
import { debug } from './Debug.js';
import { TimeoutError } from './Errors.js';
import { CommonEventEmitter } from './EventEmitter.js';
import { assert } from './assert.js';
import { isNode } from '../environment.js';

export const debugError = debug('puppeteer:error');

Expand Down Expand Up @@ -318,40 +317,43 @@ async function getReadableAsBuffer(
readable: Readable,
path?: string
): Promise<Buffer | null> {
if (!isNode && path) {
throw new Error('Cannot write to a path outside of Node.js environment.');
}

const fs = isNode ? (await import('fs')).promises : null;

let fileHandle: import('fs').promises.FileHandle | undefined;

if (path && fs) {
fileHandle = await fs.open(path, 'w');
}
const buffers = [];
for await (const chunk of readable) {
buffers.push(chunk);
if (fileHandle && fs) {
await fs.writeFile(fileHandle, chunk);
if (path) {
let fs: typeof import('fs').promises;
try {
fs = (await import('fs')).promises;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Cannot write to a path outside of a Node-like environment.'
);
}
throw error;
}
const fileHandle = await fs.open(path, 'w+');
for await (const chunk of readable) {
buffers.push(chunk);
await fileHandle.writeFile(chunk);
}
await fileHandle.close();
} else {
for await (const chunk of readable) {
buffers.push(chunk);
}
}

if (path && fileHandle) await fileHandle.close();
let resultBuffer = null;
try {
resultBuffer = Buffer.concat(buffers);
} finally {
return resultBuffer;
return Buffer.concat(buffers);
} catch (error) {
return null;
}
}

async function getReadableFromProtocolStream(
client: CDPSession,
handle: string
): Promise<Readable> {
// TODO:
// This restriction can be lifted once https://github.com/nodejs/node/pull/39062 has landed
// TODO: Once Node 18 becomes the lowest supported version, we can migrate to
// ReadableStream.
if (!isNode) {
throw new Error('Cannot create a stream outside of Node.js environment.');
}
Expand Down
2 changes: 1 addition & 1 deletion src/node-puppeteer-core.ts
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import { initializePuppeteerNode } from './initialize-node.js';
import { isNode } from './environment.js';
import { initializePuppeteerNode } from './initialize-node.js';

if (!isNode) {
throw new Error('Cannot run puppeteer-core outside of Node.js');
Expand Down
6 changes: 4 additions & 2 deletions src/node.ts
Expand Up @@ -14,10 +14,12 @@
* limitations under the License.
*/

import { initializePuppeteerNode } from './initialize-node.js';
import { isNode } from './environment.js';
import { initializePuppeteerNode } from './initialize-node.js';

if (!isNode) {
throw new Error('Trying to run Puppeteer-Node in a web environment.');
}
export default initializePuppeteerNode('puppeteer');

const puppeteer = initializePuppeteerNode('puppeteer');
export default puppeteer;
5 changes: 3 additions & 2 deletions src/web.ts
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

import { initializePuppeteerWeb } from './initialize-web.js';
import { isNode } from './environment.js';
import { initializePuppeteerWeb } from './initialize-web.js';

if (isNode) {
throw new Error('Trying to run Puppeteer-Web in a Node environment');
}

export default initializePuppeteerWeb('puppeteer');
const puppeteer = initializePuppeteerWeb('puppeteer');
export default puppeteer;