146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
|
|
/**
|
||
|
|
* 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 * as path from 'path';
|
||
|
|
|
||
|
|
// NOTE: update this to point to playwright/lib when moving this file.
|
||
|
|
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
|
||
|
|
|
||
|
|
type Position = {
|
||
|
|
line: number;
|
||
|
|
column: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
export async function generateSourceMapUrl(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) {
|
||
|
|
return {
|
||
|
|
line: position.line + delta.line,
|
||
|
|
column: delta.column + (delta.line ? 0 : position.column),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getCallerFilePath(): string | null {
|
||
|
|
const error = new Error();
|
||
|
|
const stackFrames = (error.stack || '').split('\n').slice(1);
|
||
|
|
// Find first stackframe that doesn't point to PLAYWRIGHT_LIB_PATH.
|
||
|
|
for (let frame of stackFrames) {
|
||
|
|
frame = frame.trim();
|
||
|
|
if (!frame.startsWith('at '))
|
||
|
|
return null;
|
||
|
|
if (frame.endsWith(')')) {
|
||
|
|
const from = frame.indexOf('(');
|
||
|
|
frame = frame.substring(from + 1, frame.length - 1);
|
||
|
|
} else {
|
||
|
|
frame = frame.substring('at '.length);
|
||
|
|
}
|
||
|
|
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
|
||
|
|
if (!match)
|
||
|
|
return null;
|
||
|
|
const filePath = match[1];
|
||
|
|
if (filePath.startsWith(PLAYWRIGHT_LIB_PATH))
|
||
|
|
continue;
|
||
|
|
return filePath;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|