mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
344 lines
9.4 KiB
TypeScript
344 lines
9.4 KiB
TypeScript
// Accelerate VLQ decoding with a lookup table
|
|
const vlqTable = new Uint8Array(128);
|
|
const vlqChars =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
vlqTable.fill(0xff);
|
|
for (let i = 0; i < vlqChars.length; i++) vlqTable[vlqChars.charCodeAt(i)] = i;
|
|
|
|
export function parseSourceMap(json) {
|
|
if (json.version !== 3) {
|
|
throw new Error("Invalid source map");
|
|
}
|
|
|
|
if (
|
|
!(json.sources instanceof Array) ||
|
|
json.sources.some((x) => typeof x !== "string")
|
|
) {
|
|
throw new Error("Invalid source map");
|
|
}
|
|
|
|
if (typeof json.mappings !== "string") {
|
|
throw new Error("Invalid source map");
|
|
}
|
|
|
|
const { sources, sourcesContent, names, mappings } = json;
|
|
const emptyData = new Int32Array(0);
|
|
for (let i = 0; i < sources.length; i++) {
|
|
sources[i] = {
|
|
name: sources[i],
|
|
content: (sourcesContent && sourcesContent[i]) || "",
|
|
data: emptyData,
|
|
dataLength: 0,
|
|
};
|
|
}
|
|
const data = decodeMappings(mappings, sources.length);
|
|
return { sources, names, data };
|
|
}
|
|
|
|
// ripped from https://github.com/evanw/source-map-visualization/blob/gh-pages/code.js#L179
|
|
export function decodeMappings(mappings, sourcesCount) {
|
|
const n = mappings.length;
|
|
let data = new Int32Array(1024);
|
|
let dataLength = 0;
|
|
let generatedLine = 0;
|
|
let generatedLineStart = 0;
|
|
let generatedColumn = 0;
|
|
let originalSource = 0;
|
|
let originalLine = 0;
|
|
let originalColumn = 0;
|
|
let originalName = 0;
|
|
let needToSortGeneratedColumns = false;
|
|
let i = 0;
|
|
|
|
function decodeError(text) {
|
|
const error = `Invalid VLQ data at index ${i}: ${text}`;
|
|
throw new Error(error);
|
|
}
|
|
|
|
function decodeVLQ() {
|
|
let shift = 0;
|
|
let vlq = 0;
|
|
|
|
// Scan over the input
|
|
while (true) {
|
|
// Read a byte
|
|
if (i >= mappings.length) decodeError("Expected extra data");
|
|
const c = mappings.charCodeAt(i);
|
|
if ((c & 0x7f) !== c) decodeError("Invalid character");
|
|
const index = vlqTable[c & 0x7f];
|
|
if (index === 0xff) decodeError("Invalid character");
|
|
i++;
|
|
|
|
// Decode the byte
|
|
vlq |= (index & 31) << shift;
|
|
shift += 5;
|
|
|
|
// Stop if there's no continuation bit
|
|
if ((index & 32) === 0) break;
|
|
}
|
|
|
|
// Recover the signed value
|
|
return vlq & 1 ? -(vlq >> 1) : vlq >> 1;
|
|
}
|
|
|
|
while (i < n) {
|
|
let c = mappings.charCodeAt(i);
|
|
|
|
// Handle a line break
|
|
if (c === 59 /* ; */) {
|
|
// The generated columns are very rarely out of order. In that case,
|
|
// sort them with insertion since they are very likely almost ordered.
|
|
if (needToSortGeneratedColumns) {
|
|
for (let j = generatedLineStart + 6; j < dataLength; j += 6) {
|
|
const genL = data[j];
|
|
const genC = data[j + 1];
|
|
const origS = data[j + 2];
|
|
const origL = data[j + 3];
|
|
const origC = data[j + 4];
|
|
const origN = data[j + 5];
|
|
let k = j - 6;
|
|
for (; k >= generatedLineStart && data[k + 1] > genC; k -= 6) {
|
|
data[k + 6] = data[k];
|
|
data[k + 7] = data[k + 1];
|
|
data[k + 8] = data[k + 2];
|
|
data[k + 9] = data[k + 3];
|
|
data[k + 10] = data[k + 4];
|
|
data[k + 11] = data[k + 5];
|
|
}
|
|
data[k + 6] = genL;
|
|
data[k + 7] = genC;
|
|
data[k + 8] = origS;
|
|
data[k + 9] = origL;
|
|
data[k + 10] = origC;
|
|
data[k + 11] = origN;
|
|
}
|
|
}
|
|
|
|
generatedLine++;
|
|
generatedColumn = 0;
|
|
generatedLineStart = dataLength;
|
|
needToSortGeneratedColumns = false;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Ignore stray commas
|
|
if (c === 44 /* , */) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Read the generated column
|
|
const generatedColumnDelta = decodeVLQ();
|
|
if (generatedColumnDelta < 0) needToSortGeneratedColumns = true;
|
|
generatedColumn += generatedColumnDelta;
|
|
if (generatedColumn < 0) decodeError("Invalid generated column");
|
|
|
|
// It's valid for a mapping to have 1, 4, or 5 variable-length fields
|
|
let isOriginalSourceMissing = true;
|
|
let isOriginalNameMissing = true;
|
|
if (i < n) {
|
|
c = mappings.charCodeAt(i);
|
|
if (c === 44 /* , */) {
|
|
i++;
|
|
} else if (c !== 59 /* ; */) {
|
|
isOriginalSourceMissing = false;
|
|
|
|
// Read the original source
|
|
const originalSourceDelta = decodeVLQ();
|
|
originalSource += originalSourceDelta;
|
|
if (originalSource < 0 || originalSource >= sourcesCount)
|
|
decodeError("Invalid original source");
|
|
|
|
// Read the original line
|
|
const originalLineDelta = decodeVLQ();
|
|
originalLine += originalLineDelta;
|
|
if (originalLine < 0) decodeError("Invalid original line");
|
|
|
|
// Read the original column
|
|
const originalColumnDelta = decodeVLQ();
|
|
originalColumn += originalColumnDelta;
|
|
if (originalColumn < 0) decodeError("Invalid original column");
|
|
|
|
// Check for the optional name index
|
|
if (i < n) {
|
|
c = mappings.charCodeAt(i);
|
|
if (c === 44 /* , */) {
|
|
i++;
|
|
} else if (c !== 59 /* ; */) {
|
|
isOriginalNameMissing = false;
|
|
|
|
// Read the optional name index
|
|
const originalNameDelta = decodeVLQ();
|
|
originalName += originalNameDelta;
|
|
if (originalName < 0) decodeError("Invalid original name");
|
|
|
|
// Handle the next character
|
|
if (i < n) {
|
|
c = mappings.charCodeAt(i);
|
|
if (c === 44 /* , */) {
|
|
i++;
|
|
} else if (c !== 59 /* ; */) {
|
|
decodeError("Invalid character after mapping");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append the mapping to the typed array
|
|
if (dataLength + 6 > data.length) {
|
|
const newData = new Int32Array(data.length << 1);
|
|
newData.set(data);
|
|
data = newData;
|
|
}
|
|
data[dataLength] = generatedLine;
|
|
data[dataLength + 1] = generatedColumn;
|
|
if (isOriginalSourceMissing) {
|
|
data[dataLength + 2] = -1;
|
|
data[dataLength + 3] = -1;
|
|
data[dataLength + 4] = -1;
|
|
} else {
|
|
data[dataLength + 2] = originalSource;
|
|
data[dataLength + 3] = originalLine;
|
|
data[dataLength + 4] = originalColumn;
|
|
}
|
|
data[dataLength + 5] = isOriginalNameMissing ? -1 : originalName;
|
|
dataLength += 6;
|
|
}
|
|
|
|
return data.subarray(0, dataLength);
|
|
}
|
|
|
|
export function remapPosition(
|
|
decodedMappings: Int32Array,
|
|
line: number,
|
|
column: number
|
|
) {
|
|
if (!(decodedMappings instanceof Int32Array)) {
|
|
throw new Error("decodedMappings must be an Int32Array");
|
|
}
|
|
|
|
if (!Number.isFinite(line)) {
|
|
throw new Error("line must be a finite number");
|
|
}
|
|
|
|
if (!Number.isFinite(column)) {
|
|
throw new Error("column must be a finite number");
|
|
}
|
|
|
|
if (decodedMappings.length === 0 || line < 0 || column < 0) return null;
|
|
|
|
const index = indexOfMapping(decodedMappings, line, column);
|
|
if (index === -1) return null;
|
|
|
|
return [decodedMappings[index + 3] + 1, decodedMappings[index + 4]];
|
|
}
|
|
|
|
async function fetchRemoteSourceMap(file: string, signal) {
|
|
const response = await globalThis.fetch(file + ".map", {
|
|
signal,
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Mappings-Only": "1",
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export var sourceMappings = new Map();
|
|
|
|
export function fetchMappings(file, signal) {
|
|
if (file.includes(".bun")) return null;
|
|
if (sourceMappings.has(file)) {
|
|
return sourceMappings.get(file);
|
|
}
|
|
|
|
return fetchRemoteSourceMap(file, signal).then((json) => {
|
|
if (!json) return null;
|
|
const { data } = parseSourceMap(json);
|
|
sourceMappings.set(file, data);
|
|
return data;
|
|
});
|
|
}
|
|
|
|
// this batches duplicate requests
|
|
export function fetchAllMappings(files, signal) {
|
|
var results = new Array(files.length);
|
|
var map = new Map();
|
|
for (var i = 0; i < files.length; i++) {
|
|
const existing = map.get(files[i]);
|
|
if (existing) {
|
|
existing.push(i);
|
|
} else map.set(files[i], [i]);
|
|
}
|
|
|
|
for (const [file, indices] of [...map.entries()]) {
|
|
const mapped = fetchMappings(file, signal);
|
|
if (mapped?.then) {
|
|
var resolvers = [];
|
|
for (let i = 0; i < indices.length; i++) {
|
|
results[indices[i]] = new Promise((resolve, reject) => {
|
|
resolvers[i] = (res) => resolve(res ? [res, i] : null);
|
|
});
|
|
}
|
|
|
|
mapped.finally((a) => {
|
|
for (let resolve of resolvers) {
|
|
try {
|
|
resolve(a);
|
|
} catch {
|
|
} finally {
|
|
}
|
|
}
|
|
resolvers.length = 0;
|
|
resolvers = null;
|
|
});
|
|
} else {
|
|
for (let i = 0; i < indices.length; i++) {
|
|
results[indices[i]] = mapped ? [mapped, indices[i]] : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function indexOfMapping(mappings: Int32Array, line: number, column: number) {
|
|
// the array is [generatedLine, generatedColumn, sourceIndex, sourceLine, sourceColumn, nameIndex]
|
|
// 0 - generated line
|
|
var count = mappings.length / 6;
|
|
var index = 0;
|
|
while (count > 0) {
|
|
var step = (count / 2) | 0;
|
|
var i = index + step;
|
|
// this multiply is slow but it's okay for now
|
|
var j = i * 6;
|
|
if (
|
|
mappings[j] < line ||
|
|
(mappings[j] == line && mappings[j + 1] <= column)
|
|
) {
|
|
index = i + 1;
|
|
count -= step + 1;
|
|
} else {
|
|
count = step;
|
|
}
|
|
}
|
|
|
|
index = index | 0;
|
|
|
|
if (index > 0) {
|
|
if (mappings[(index - 1) * 6] == line) {
|
|
return (index - 1) * 6;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|