feat: auto-detect expression/function in js server (#5284)

This commit is contained in:
Pavel Feldman 2021-02-03 13:49:25 -08:00 committed by GitHub
parent fa1cf4108b
commit 3d253c4e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 100 additions and 221 deletions

View File

@ -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,

View File

@ -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

View File

@ -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({}));

View File

@ -50,7 +50,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
});
}
async evaluateExpressionInternal(expression: string, isFunction: boolean, ...args: any[]): Promise<any> {
async evaluateExpressionInternal(expression: string, isFunction: boolean | undefined, ...args: any[]): Promise<any> {
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<any> {
async evaluateExpressionHandleInternal(expression: string, isFunction: boolean | undefined, ...args: any[]): Promise<any> {
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<T extends Node = Node> extends js.JSHandle<T> {
return this._page.selectors._queryAll(this._context.frame, selector, this, true /* adoptToMain */);
}
async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
async _$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
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<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
async _$$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const arrayHandle = await this._page.selectors._queryArray(this._context.frame, selector, this);
const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg);
arrayHandle.dispose();

View File

@ -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<void>(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<any> {
async _evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
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<any> {
async _evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
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<any> {
async _$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
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<any> {
async _$$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
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<dom.ElementHandle>(async resolve => {
const actionPromise = new Promise<void>(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<void>(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<R>(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
async _waitForFunctionExpression<R>(expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
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 + ')';

View File

@ -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;
}

View File

@ -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<T = any> {
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<any> {
export async function evaluateExpression(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean | undefined, ...args: any[]): Promise<any> {
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 {

View File

@ -503,7 +503,7 @@ export class Worker extends EventEmitter {
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
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<any> {
async _evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg);
}
async _evaluateExpressionHandle(expression: string, isFunction: boolean, arg: any): Promise<any> {
async _evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
return js.evaluateExpression(await this._executionContextPromise, false /* returnByValue */, expression, isFunction, arg);
}
}

View File

@ -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<string> {
if (!isDebugMode())
return '';
const sourceMapUrl = await innerGenerateSourceMapUrl(functionText, generatedText);
return sourceMapUrl || '';
}
async function innerGenerateSourceMapUrl(functionText: string, generatedText: string): Promise<string | undefined> {
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),
};
}