| /** |
| * @license |
| * Copyright 2017 Google Inc. |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| import type {Protocol} from 'devtools-protocol'; |
| |
| import {PuppeteerURL, evaluationString} from '../common/util.js'; |
| import {assert} from '../util/assert.js'; |
| |
| /** |
| * @internal |
| */ |
| export function createEvaluationError( |
| details: Protocol.Runtime.ExceptionDetails, |
| ): unknown { |
| let name: string; |
| let message: string; |
| if (!details.exception) { |
| name = 'Error'; |
| message = details.text; |
| } else if ( |
| (details.exception.type !== 'object' || |
| details.exception.subtype !== 'error') && |
| !details.exception.objectId |
| ) { |
| return valueFromRemoteObject(details.exception); |
| } else { |
| const detail = getErrorDetails(details); |
| name = detail.name; |
| message = detail.message; |
| } |
| const messageHeight = message.split('\n').length; |
| const error = new Error(message); |
| error.name = name; |
| const stackLines = error.stack!.split('\n'); |
| const messageLines = stackLines.splice(0, messageHeight); |
| |
| // The first line is this function which we ignore. |
| stackLines.shift(); |
| if (details.stackTrace && stackLines.length < Error.stackTraceLimit) { |
| for (const frame of details.stackTrace.callFrames.reverse()) { |
| if ( |
| PuppeteerURL.isPuppeteerURL(frame.url) && |
| frame.url !== PuppeteerURL.INTERNAL_URL |
| ) { |
| const url = PuppeteerURL.parse(frame.url); |
| stackLines.unshift( |
| ` at ${frame.functionName || url.functionName} (${ |
| url.functionName |
| } at ${url.siteString}, <anonymous>:${frame.lineNumber}:${ |
| frame.columnNumber |
| })`, |
| ); |
| } else { |
| stackLines.push( |
| ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${ |
| frame.lineNumber |
| }:${frame.columnNumber})`, |
| ); |
| } |
| if (stackLines.length >= Error.stackTraceLimit) { |
| break; |
| } |
| } |
| } |
| |
| error.stack = [...messageLines, ...stackLines].join('\n'); |
| return error; |
| } |
| |
| const getErrorDetails = (details: Protocol.Runtime.ExceptionDetails) => { |
| let name = ''; |
| let message: string; |
| const lines = details.exception?.description?.split('\n at ') ?? []; |
| const size = Math.min( |
| details.stackTrace?.callFrames.length ?? 0, |
| lines.length - 1, |
| ); |
| lines.splice(-size, size); |
| if (details.exception?.className) { |
| name = details.exception.className; |
| } |
| message = lines.join('\n'); |
| if (name && message.startsWith(`${name}: `)) { |
| message = message.slice(name.length + 2); |
| } |
| return {message, name}; |
| }; |
| |
| /** |
| * @internal |
| */ |
| export function createClientError( |
| details: Protocol.Runtime.ExceptionDetails, |
| ): Error | unknown { |
| let name: string; |
| let message: string; |
| if (!details.exception) { |
| name = 'Error'; |
| message = details.text; |
| } else if ( |
| (details.exception.type !== 'object' || |
| details.exception.subtype !== 'error') && |
| !details.exception.objectId |
| ) { |
| return valueFromRemoteObject(details.exception); |
| } else { |
| const detail = getErrorDetails(details); |
| name = detail.name; |
| message = detail.message; |
| } |
| const error = new Error(message); |
| error.name = name; |
| |
| const messageHeight = error.message.split('\n').length; |
| const messageLines = error.stack!.split('\n').splice(0, messageHeight); |
| |
| const stackLines = []; |
| if (details.stackTrace) { |
| for (const frame of details.stackTrace.callFrames) { |
| // Note we need to add `1` because the values are 0-indexed. |
| stackLines.push( |
| ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${ |
| frame.lineNumber + 1 |
| }:${frame.columnNumber + 1})`, |
| ); |
| if (stackLines.length >= Error.stackTraceLimit) { |
| break; |
| } |
| } |
| } |
| |
| error.stack = [...messageLines, ...stackLines].join('\n'); |
| return error; |
| } |
| |
| /** |
| * @internal |
| */ |
| export function valueFromRemoteObject( |
| remoteObject: Protocol.Runtime.RemoteObject, |
| ): unknown { |
| assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); |
| if (remoteObject.unserializableValue) { |
| if (remoteObject.type === 'bigint') { |
| return BigInt(remoteObject.unserializableValue.replace('n', '')); |
| } |
| switch (remoteObject.unserializableValue) { |
| case '-0': |
| return -0; |
| case 'NaN': |
| return NaN; |
| case 'Infinity': |
| return Infinity; |
| case '-Infinity': |
| return -Infinity; |
| default: |
| throw new Error( |
| 'Unsupported unserializable value: ' + |
| remoteObject.unserializableValue, |
| ); |
| } |
| } |
| return remoteObject.value; |
| } |
| |
| /** |
| * @internal |
| */ |
| export function addPageBinding( |
| type: string, |
| name: string, |
| prefix: string, |
| ): void { |
| // Depending on the frame loading state either Runtime.evaluate or |
| // Page.addScriptToEvaluateOnNewDocument might succeed. Let's check that we |
| // don't re-wrap Puppeteer's binding. |
| // @ts-expect-error: In a different context. |
| if (globalThis[name]) { |
| return; |
| } |
| |
| // We replace the CDP binding with a Puppeteer binding. |
| Object.assign(globalThis, { |
| [name](...args: unknown[]): Promise<unknown> { |
| // This is the Puppeteer binding. |
| // @ts-expect-error: In a different context. |
| const callPuppeteer = globalThis[name]; |
| callPuppeteer.args ??= new Map(); |
| callPuppeteer.callbacks ??= new Map(); |
| |
| const seq = (callPuppeteer.lastSeq ?? 0) + 1; |
| callPuppeteer.lastSeq = seq; |
| callPuppeteer.args.set(seq, args); |
| |
| // @ts-expect-error: In a different context. |
| // Needs to be the same as CDP_BINDING_PREFIX. |
| globalThis[prefix + name]( |
| JSON.stringify({ |
| type, |
| name, |
| seq, |
| args, |
| isTrivial: !args.some(value => { |
| return value instanceof Node; |
| }), |
| }), |
| ); |
| |
| return new Promise((resolve, reject) => { |
| callPuppeteer.callbacks.set(seq, { |
| resolve(value: unknown) { |
| callPuppeteer.args.delete(seq); |
| resolve(value); |
| }, |
| reject(value?: unknown) { |
| callPuppeteer.args.delete(seq); |
| reject(value); |
| }, |
| }); |
| }); |
| }, |
| }); |
| } |
| |
| /** |
| * @internal |
| */ |
| export const CDP_BINDING_PREFIX = 'puppeteer_'; |
| |
| /** |
| * @internal |
| */ |
| export function pageBindingInitString(type: string, name: string): string { |
| return evaluationString(addPageBinding, type, name, CDP_BINDING_PREFIX); |
| } |