utils.ts (6438B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 9 import {PuppeteerURL, evaluationString} from '../common/util.js'; 10 import {assert} from '../util/assert.js'; 11 12 /** 13 * @internal 14 */ 15 export function createEvaluationError( 16 details: Protocol.Runtime.ExceptionDetails, 17 ): unknown { 18 let name: string; 19 let message: string; 20 if (!details.exception) { 21 name = 'Error'; 22 message = details.text; 23 } else if ( 24 (details.exception.type !== 'object' || 25 details.exception.subtype !== 'error') && 26 !details.exception.objectId 27 ) { 28 return valueFromRemoteObject(details.exception); 29 } else { 30 const detail = getErrorDetails(details); 31 name = detail.name; 32 message = detail.message; 33 } 34 const messageHeight = message.split('\n').length; 35 const error = new Error(message); 36 error.name = name; 37 const stackLines = error.stack!.split('\n'); 38 const messageLines = stackLines.splice(0, messageHeight); 39 40 // The first line is this function which we ignore. 41 stackLines.shift(); 42 if (details.stackTrace && stackLines.length < Error.stackTraceLimit) { 43 for (const frame of details.stackTrace.callFrames.reverse()) { 44 if ( 45 PuppeteerURL.isPuppeteerURL(frame.url) && 46 frame.url !== PuppeteerURL.INTERNAL_URL 47 ) { 48 const url = PuppeteerURL.parse(frame.url); 49 stackLines.unshift( 50 ` at ${frame.functionName || url.functionName} (${ 51 url.functionName 52 } at ${url.siteString}, <anonymous>:${frame.lineNumber}:${ 53 frame.columnNumber 54 })`, 55 ); 56 } else { 57 stackLines.push( 58 ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${ 59 frame.lineNumber 60 }:${frame.columnNumber})`, 61 ); 62 } 63 if (stackLines.length >= Error.stackTraceLimit) { 64 break; 65 } 66 } 67 } 68 69 error.stack = [...messageLines, ...stackLines].join('\n'); 70 return error; 71 } 72 73 const getErrorDetails = (details: Protocol.Runtime.ExceptionDetails) => { 74 let name = ''; 75 let message: string; 76 const lines = details.exception?.description?.split('\n at ') ?? []; 77 const size = Math.min( 78 details.stackTrace?.callFrames.length ?? 0, 79 lines.length - 1, 80 ); 81 lines.splice(-size, size); 82 if (details.exception?.className) { 83 name = details.exception.className; 84 } 85 message = lines.join('\n'); 86 if (name && message.startsWith(`${name}: `)) { 87 message = message.slice(name.length + 2); 88 } 89 return {message, name}; 90 }; 91 92 /** 93 * @internal 94 */ 95 export function createClientError( 96 details: Protocol.Runtime.ExceptionDetails, 97 ): Error { 98 let name: string; 99 let message: string; 100 if (!details.exception) { 101 name = 'Error'; 102 message = details.text; 103 } else if ( 104 (details.exception.type !== 'object' || 105 details.exception.subtype !== 'error') && 106 !details.exception.objectId 107 ) { 108 return valueFromRemoteObject(details.exception); 109 } else { 110 const detail = getErrorDetails(details); 111 name = detail.name; 112 message = detail.message; 113 } 114 const error = new Error(message); 115 error.name = name; 116 117 const messageHeight = error.message.split('\n').length; 118 const messageLines = error.stack!.split('\n').splice(0, messageHeight); 119 120 const stackLines = []; 121 if (details.stackTrace) { 122 for (const frame of details.stackTrace.callFrames) { 123 // Note we need to add `1` because the values are 0-indexed. 124 stackLines.push( 125 ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${ 126 frame.lineNumber + 1 127 }:${frame.columnNumber + 1})`, 128 ); 129 if (stackLines.length >= Error.stackTraceLimit) { 130 break; 131 } 132 } 133 } 134 135 error.stack = [...messageLines, ...stackLines].join('\n'); 136 return error; 137 } 138 139 /** 140 * @internal 141 */ 142 export function valueFromRemoteObject( 143 remoteObject: Protocol.Runtime.RemoteObject, 144 ): any { 145 assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); 146 if (remoteObject.unserializableValue) { 147 if (remoteObject.type === 'bigint') { 148 return BigInt(remoteObject.unserializableValue.replace('n', '')); 149 } 150 switch (remoteObject.unserializableValue) { 151 case '-0': 152 return -0; 153 case 'NaN': 154 return NaN; 155 case 'Infinity': 156 return Infinity; 157 case '-Infinity': 158 return -Infinity; 159 default: 160 throw new Error( 161 'Unsupported unserializable value: ' + 162 remoteObject.unserializableValue, 163 ); 164 } 165 } 166 return remoteObject.value; 167 } 168 169 /** 170 * @internal 171 */ 172 export function addPageBinding( 173 type: string, 174 name: string, 175 prefix: string, 176 ): void { 177 // Depending on the frame loading state either Runtime.evaluate or 178 // Page.addScriptToEvaluateOnNewDocument might succeed. Let's check that we 179 // don't re-wrap Puppeteer's binding. 180 // @ts-expect-error: In a different context. 181 if (globalThis[name]) { 182 return; 183 } 184 185 // We replace the CDP binding with a Puppeteer binding. 186 Object.assign(globalThis, { 187 [name](...args: unknown[]): Promise<unknown> { 188 // This is the Puppeteer binding. 189 // @ts-expect-error: In a different context. 190 const callPuppeteer = globalThis[name]; 191 callPuppeteer.args ??= new Map(); 192 callPuppeteer.callbacks ??= new Map(); 193 194 const seq = (callPuppeteer.lastSeq ?? 0) + 1; 195 callPuppeteer.lastSeq = seq; 196 callPuppeteer.args.set(seq, args); 197 198 // @ts-expect-error: In a different context. 199 // Needs to be the same as CDP_BINDING_PREFIX. 200 globalThis[prefix + name]( 201 JSON.stringify({ 202 type, 203 name, 204 seq, 205 args, 206 isTrivial: !args.some(value => { 207 return value instanceof Node; 208 }), 209 }), 210 ); 211 212 return new Promise((resolve, reject) => { 213 callPuppeteer.callbacks.set(seq, { 214 resolve(value: unknown) { 215 callPuppeteer.args.delete(seq); 216 resolve(value); 217 }, 218 reject(value?: unknown) { 219 callPuppeteer.args.delete(seq); 220 reject(value); 221 }, 222 }); 223 }); 224 }, 225 }); 226 } 227 228 /** 229 * @internal 230 */ 231 export const CDP_BINDING_PREFIX = 'puppeteer_'; 232 233 /** 234 * @internal 235 */ 236 export function pageBindingInitString(type: string, name: string): string { 237 return evaluationString(addPageBinding, type, name, CDP_BINDING_PREFIX); 238 }