diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index a142d46d6b..f0a8b4c693 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -1261,11 +1261,11 @@ export type FrameNavigatedEvent = { export type FrameEvalOnSelectorParams = { selector: string, expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type FrameEvalOnSelectorOptions = { - + isFunction?: boolean, }; export type FrameEvalOnSelectorResult = { value: SerializedValue, @@ -1273,11 +1273,11 @@ export type FrameEvalOnSelectorResult = { export type FrameEvalOnSelectorAllParams = { selector: string, expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type FrameEvalOnSelectorAllOptions = { - + isFunction?: boolean, }; export type FrameEvalOnSelectorAllResult = { value: SerializedValue, @@ -1377,11 +1377,12 @@ export type FrameDispatchEventOptions = { export type FrameDispatchEventResult = void; export type FrameEvaluateExpressionParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionOptions = { + isFunction?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionResult = { @@ -1389,11 +1390,12 @@ export type FrameEvaluateExpressionResult = { }; export type FrameEvaluateExpressionHandleParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionHandleOptions = { + isFunction?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionHandleResult = { @@ -1680,12 +1682,13 @@ export type FrameUncheckOptions = { export type FrameUncheckResult = void; export type FrameWaitForFunctionParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, timeout?: number, pollingInterval?: number, }; export type FrameWaitForFunctionOptions = { + isFunction?: boolean, timeout?: number, pollingInterval?: number, }; @@ -1717,22 +1720,22 @@ export interface WorkerChannel extends Channel { export type WorkerCloseEvent = {}; export type WorkerEvaluateExpressionParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type WorkerEvaluateExpressionOptions = { - + isFunction?: boolean, }; export type WorkerEvaluateExpressionResult = { value: SerializedValue, }; export type WorkerEvaluateExpressionHandleParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type WorkerEvaluateExpressionHandleOptions = { - + isFunction?: boolean, }; export type WorkerEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -1759,22 +1762,22 @@ export type JSHandleDisposeOptions = {}; export type JSHandleDisposeResult = void; export type JSHandleEvaluateExpressionParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type JSHandleEvaluateExpressionOptions = { - + isFunction?: boolean, }; export type JSHandleEvaluateExpressionResult = { value: SerializedValue, }; export type JSHandleEvaluateExpressionHandleParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type JSHandleEvaluateExpressionHandleOptions = { - + isFunction?: boolean, }; export type JSHandleEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -1844,11 +1847,11 @@ export interface ElementHandleChannel extends JSHandleChannel { export type ElementHandleEvalOnSelectorParams = { selector: string, expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type ElementHandleEvalOnSelectorOptions = { - + isFunction?: boolean, }; export type ElementHandleEvalOnSelectorResult = { value: SerializedValue, @@ -1856,11 +1859,11 @@ export type ElementHandleEvalOnSelectorResult = { export type ElementHandleEvalOnSelectorAllParams = { selector: string, expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type ElementHandleEvalOnSelectorAllOptions = { - + isFunction?: boolean, }; export type ElementHandleEvalOnSelectorAllResult = { value: SerializedValue, @@ -2499,22 +2502,22 @@ export type ElectronApplicationWindowEvent = { }; export type ElectronApplicationEvaluateExpressionParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type ElectronApplicationEvaluateExpressionOptions = { - + isFunction?: boolean, }; export type ElectronApplicationEvaluateExpressionResult = { value: SerializedValue, }; export type ElectronApplicationEvaluateExpressionHandleParams = { expression: string, - isFunction: boolean, + isFunction?: boolean, arg: SerializedArgument, }; export type ElectronApplicationEvaluateExpressionHandleOptions = { - + isFunction?: boolean, }; export type ElectronApplicationEvaluateExpressionHandleResult = { handle: JSHandleChannel, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 8fb910c40b..0e0ae188cd 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -1050,7 +1050,7 @@ Frame: parameters: selector: string expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -1059,7 +1059,7 @@ Frame: parameters: selector: string expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -1149,7 +1149,7 @@ Frame: evaluateExpression: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument world: type: enum? @@ -1162,7 +1162,7 @@ Frame: evaluateExpressionHandle: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument world: type: enum? @@ -1396,7 +1396,7 @@ Frame: waitForFunction: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument timeout: number? # When present, polls on interval. Otherwise, polls on raf. @@ -1458,7 +1458,7 @@ Worker: evaluateExpression: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -1466,7 +1466,7 @@ Worker: evaluateExpressionHandle: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: handle: JSHandle @@ -1489,7 +1489,7 @@ JSHandle: evaluateExpression: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -1497,7 +1497,7 @@ JSHandle: evaluateExpressionHandle: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: handle: JSHandle @@ -1541,7 +1541,7 @@ ElementHandle: parameters: selector: string expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -1550,7 +1550,7 @@ ElementHandle: parameters: selector: string expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -2113,7 +2113,7 @@ ElectronApplication: evaluateExpression: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: value: SerializedValue @@ -2121,7 +2121,7 @@ ElectronApplication: evaluateExpressionHandle: parameters: expression: string - isFunction: boolean + isFunction: boolean? arg: SerializedArgument returns: handle: JSHandle diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 2252c96ec9..ee09513c60 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -488,13 +488,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.FrameEvalOnSelectorParams = tObject({ selector: tString, expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.FrameEvalOnSelectorAllParams = tObject({ selector: tString, expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.FrameAddScriptTagParams = tObject({ @@ -542,13 +542,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), world: tOptional(tEnum(['main', 'utility'])), }); scheme.FrameEvaluateExpressionHandleParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), world: tOptional(tEnum(['main', 'utility'])), }); @@ -680,7 +680,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.FrameWaitForFunctionParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), timeout: tOptional(tNumber), pollingInterval: tOptional(tNumber), @@ -692,25 +692,25 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.WorkerEvaluateExpressionParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.WorkerEvaluateExpressionHandleParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.JSHandleDisposeParams = tOptional(tObject({})); scheme.ElementHandleDisposeParams = tType('JSHandleDisposeParams'); scheme.JSHandleEvaluateExpressionParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElementHandleEvaluateExpressionParams = tType('JSHandleEvaluateExpressionParams'); scheme.JSHandleEvaluateExpressionHandleParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElementHandleEvaluateExpressionHandleParams = tType('JSHandleEvaluateExpressionHandleParams'); @@ -725,13 +725,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.ElementHandleEvalOnSelectorParams = tObject({ selector: tString, expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElementHandleEvalOnSelectorAllParams = tObject({ selector: tString, expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElementHandleBoundingBoxParams = tOptional(tObject({})); @@ -923,12 +923,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.ElectronApplicationEvaluateExpressionParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElectronApplicationEvaluateExpressionHandleParams = tObject({ expression: tString, - isFunction: tBoolean, + isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.ElectronApplicationCloseParams = tOptional(tObject({})); diff --git a/src/server/dom.ts b/src/server/dom.ts index b22138ed4d..99adfd0df9 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -50,7 +50,7 @@ export class FrameExecutionContext extends js.ExecutionContext { }); } - async evaluateExpressionInternal(expression: string, isFunction: boolean, ...args: any[]): Promise { + async evaluateExpressionInternal(expression: string, isFunction: boolean | undefined, ...args: any[]): Promise { return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => { return js.evaluateExpression(this, true /* returnByValue */, expression, isFunction, ...args); }); @@ -64,7 +64,7 @@ export class FrameExecutionContext extends js.ExecutionContext { }); } - async evaluateExpressionHandleInternal(expression: string, isFunction: boolean, ...args: any[]): Promise { + async evaluateExpressionHandleInternal(expression: string, isFunction: boolean | undefined, ...args: any[]): Promise { return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => { return js.evaluateExpression(this, false /* returnByValue */, expression, isFunction, ...args); }); @@ -628,7 +628,7 @@ export class ElementHandle extends js.JSHandle { return this._page.selectors._queryAll(this._context.frame, selector, this, true /* adoptToMain */); } - async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise { + async _$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const handle = await this._page.selectors._query(this._context.frame, selector, this); if (!handle) throw new Error(`Error: failed to find element matching selector "${selector}"`); @@ -637,7 +637,7 @@ export class ElementHandle extends js.JSHandle { return result; } - async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise { + async _$$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const arrayHandle = await this._page.selectors._queryArray(this._context.frame, selector, this); const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg); arrayHandle.dispose(); diff --git a/src/server/frames.ts b/src/server/frames.ts index 415338056d..eb6036b518 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -139,7 +139,7 @@ export class FrameManager { await barrier.waitFor(); this._signalBarriers.delete(barrier); // Resolve in the next task, after all waitForNavigations. - await new Promise(makeWaitForNextTask()); + await new Promise(makeWaitForNextTask()); return result; } @@ -579,7 +579,7 @@ export class Frame extends EventEmitter { return this._context('utility'); } - async _evaluateExpressionHandle(expression: string, isFunction: boolean, arg: any, world: types.World = 'main'): Promise { + async _evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise { const context = await this._context(world); const handle = await context.evaluateExpressionHandleInternal(expression, isFunction, arg); if (world === 'main') @@ -587,7 +587,7 @@ export class Frame extends EventEmitter { return handle; } - async _evaluateExpression(expression: string, isFunction: boolean, arg: any, world: types.World = 'main'): Promise { + async _evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise { const context = await this._context(world); const value = await context.evaluateExpressionInternal(expression, isFunction, arg); if (world === 'main') @@ -632,7 +632,7 @@ export class Frame extends EventEmitter { await this._page._doSlowMo(); } - async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise { + async _$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const handle = await this.$(selector); if (!handle) throw new Error(`Error: failed to find element matching selector "${selector}"`); @@ -641,7 +641,7 @@ export class Frame extends EventEmitter { return result; } - async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise { + async _$$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const arrayHandle = await this._page.selectors._queryArray(this, selector); const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg); arrayHandle.dispose(); @@ -805,7 +805,7 @@ export class Frame extends EventEmitter { let result: dom.ElementHandle; let error: Error | undefined; let cspMessage: ConsoleMessage | undefined; - const actionPromise = new Promise(async resolve => { + const actionPromise = new Promise(async resolve => { try { result = await func(); } catch (e) { @@ -813,7 +813,7 @@ export class Frame extends EventEmitter { } resolve(); }); - const errorPromise = new Promise(resolve => { + const errorPromise = new Promise(resolve => { listeners.push(helper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => { if (message.type() === 'error' && message.text().includes('Content Security Policy')) { cspMessage = message; @@ -1016,7 +1016,7 @@ export class Frame extends EventEmitter { }, this._page._timeoutSettings.timeout(options)); } - async _waitForFunctionExpression(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { + async _waitForFunctionExpression(expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { if (typeof options.pollingInterval === 'number') assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval); const predicateBody = isFunction ? 'return (' + expression + ')(arg)' : 'return (' + expression + ')'; diff --git a/src/server/injected/utilityScript.ts b/src/server/injected/utilityScript.ts index d04951ea27..a5a1ef0fde 100644 --- a/src/server/injected/utilityScript.ts +++ b/src/server/injected/utilityScript.ts @@ -17,17 +17,23 @@ import { serializeAsCallArgument, parseEvaluationResultValue } from '../common/utilityScriptSerializers'; export default class UtilityScript { - evaluate(returnByValue: boolean, expression: string) { - const result = global.eval(expression); - return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; - } - - callFunction(returnByValue: boolean, functionText: string, argCount: number, ...argsAndHandles: any[]) { + evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = args.map(a => parseEvaluationResultValue(a, handles)); - const func = global.eval('(' + functionText + ')'); - const result = func(...parameters); + expression = expression.trim(); + if (expression.startsWith('function ') || expression.startsWith('async function ')) + expression = '(' + expression + ')'; + let result = global.eval(expression); + if (isFunction === true) { + result = result(...parameters); + } else if (isFunction === false) { + result = result; + } else { + // auto detect. + if (typeof result === 'function') + result = result(...parameters); + } return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; } diff --git a/src/server/javascript.ts b/src/server/javascript.ts index 0c608206a6..df51a3d8e9 100644 --- a/src/server/javascript.ts +++ b/src/server/javascript.ts @@ -16,7 +16,6 @@ import * as dom from './dom'; import * as utilityScriptSource from '../generated/utilityScriptSource'; -import * as sourceMap from '../utils/sourceMap'; import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import type UtilityScript from './injected/utilityScript'; @@ -114,7 +113,7 @@ export class JSHandle { return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); } - async _evaluateExpression(expression: string, isFunction: boolean, returnByValue: boolean, arg: any) { + async _evaluateExpression(expression: string, isFunction: boolean | undefined, returnByValue: boolean, arg: any) { const value = await evaluateExpression(this._context, returnByValue, expression, isFunction, this, arg); await this._context.doSlowMo(); return value; @@ -174,28 +173,25 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean return evaluateExpression(context, returnByValue, String(pageFunction), typeof pageFunction === 'function', ...args); } -export async function evaluateExpression(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean | undefined, ...args: any[]): Promise { const utilityScript = await context.utilityScript(); - if (!isFunction) { - const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; - return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, [returnByValue, expression], []); - } - let functionText = expression; - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; + if (isFunction) { try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); + new Function('(' + expression + ')'); + } catch (e1) { + // This means we might have a function shorthand. Try another + // time prefixing 'function '. + if (expression.startsWith('async ')) + expression = 'async function ' + expression.substring('async '.length); + else + expression = 'function ' + expression; + try { + new Function('(' + expression + ')'); + } catch (e2) { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function is not well-serializable!'); + } } } @@ -228,11 +224,10 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu utilityScriptObjectIds.push(handle._objectId!); } - functionText += await sourceMap.generateSourceMapUrl(expression, functionText); // See UtilityScript for arguments. - const utilityScriptValues = [returnByValue, functionText, args.length, ...args]; + const utilityScriptValues = [isFunction, returnByValue, expression, args.length, ...args]; - const script = `(utilityScript, ...args) => utilityScript.callFunction(...args)`; + const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { return await context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds); } finally { diff --git a/src/server/page.ts b/src/server/page.ts index 4633993ba2..bb548bf61a 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -503,7 +503,7 @@ export class Worker extends EventEmitter { private _url: string; private _executionContextPromise: Promise; - private _executionContextCallback: (value?: js.ExecutionContext) => void; + private _executionContextCallback: (value: js.ExecutionContext) => void; _existingExecutionContext: js.ExecutionContext | null = null; constructor(url: string) { @@ -522,11 +522,11 @@ export class Worker extends EventEmitter { return this._url; } - async _evaluateExpression(expression: string, isFunction: boolean, arg: any): Promise { + async _evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg); } - async _evaluateExpressionHandle(expression: string, isFunction: boolean, arg: any): Promise { + async _evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise { return js.evaluateExpression(await this._executionContextPromise, false /* returnByValue */, expression, isFunction, arg); } } diff --git a/src/utils/sourceMap.ts b/src/utils/sourceMap.ts deleted file mode 100644 index ff4d28b7ba..0000000000 --- a/src/utils/sourceMap.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as fs from 'fs'; -import * as util from 'util'; -import { getCallerFilePath } from './stackTrace'; -import { isDebugMode } from './utils'; - -type Position = { - line: number; - column: number; -}; - -export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise { - if (!isDebugMode()) - return ''; - const sourceMapUrl = await innerGenerateSourceMapUrl(functionText, generatedText); - return sourceMapUrl || ''; -} - -async function innerGenerateSourceMapUrl(functionText: string, generatedText: string): Promise { - const filePath = getCallerFilePath(); - if (!filePath) - return; - try { - const generatedIndex = generatedText.indexOf(functionText); - if (generatedIndex === -1) - return; - const compiledPosition = findPosition(generatedText, generatedIndex); - const source = await util.promisify(fs.readFile)(filePath, 'utf8'); - const sourceIndex = source.indexOf(functionText); - if (sourceIndex === -1) - return; - const sourcePosition = findPosition(source, sourceIndex); - const delta = findPosition(functionText, functionText.length); - const sourceMap = generateSourceMap(filePath, sourcePosition, compiledPosition, delta); - return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(sourceMap).toString('base64')}\n`; - } catch (e) { - } -} - -const VLQ_BASE_SHIFT = 5; -const VLQ_BASE = 1 << VLQ_BASE_SHIFT; -const VLQ_BASE_MASK = VLQ_BASE - 1; -const VLQ_CONTINUATION_BIT = VLQ_BASE; -const BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - -function base64VLQ(value: number): string { - if (value < 0) - value = ((-value) << 1) | 1; - else - value <<= 1; - let result = ''; - do { - let digit = value & VLQ_BASE_MASK; - value >>>= VLQ_BASE_SHIFT; - if (value > 0) - digit |= VLQ_CONTINUATION_BIT; - result += BASE64_DIGITS[digit]; - } while (value > 0); - return result; -} - -function generateSourceMap(filePath: string, sourcePosition: Position, compiledPosition: Position, delta: Position): any { - const mappings = []; - let lastCompiled = { line: 0, column: 0 }; - let lastSource = { line: 0, column: 0 }; - for (let line = 0; line < delta.line; line++) { - // We need at least a mapping per line. This will yield an execution line at the start of each line. - // Note: for more granular mapping, we can do word-by-word. - const source = advancePosition(sourcePosition, { line, column: 0 }); - const compiled = advancePosition(compiledPosition, { line, column: 0 }); - while (lastCompiled.line < compiled.line) { - mappings.push(';'); - lastCompiled.line++; - lastCompiled.column = 0; - } - mappings.push(base64VLQ(compiled.column - lastCompiled.column)); - mappings.push(base64VLQ(0)); // Source index. - mappings.push(base64VLQ(source.line - lastSource.line)); - mappings.push(base64VLQ(source.column - lastSource.column)); - lastCompiled = compiled; - lastSource = source; - } - return JSON.stringify({ - version: 3, - sources: ['file://' + filePath], - names: [], - mappings: mappings.join(''), - }); -} - -function findPosition(source: string, offset: number): Position { - const result: Position = { line: 0, column: 0 }; - let index = 0; - while (true) { - const newline = source.indexOf('\n', index); - if (newline === -1 || newline >= offset) - break; - result.line++; - index = newline + 1; - } - result.column = offset - index; - return result; -} - -function advancePosition(position: Position, delta: Position): Position { - return { - line: position.line + delta.line, - column: delta.column + (delta.line ? 0 : position.column), - }; -}