/** * 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 { 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; }