Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ const IGNORE_LIST_ID = 'x_google_ignoreList';
*/
const NODE_MODULE_BYTES = Buffer.from('node_modules/', 'utf-8');

/**
* The UTF-8 bytes for the "sources" property key used to locate the sources array.
*/
const SOURCES_KEY_BYTES = Buffer.from('"sources"', 'utf-8');

/**
* The UTF-8 bytes for the ignore list identifier to check if already present.
*/
const IGNORE_LIST_BYTES = Buffer.from(`"${IGNORE_LIST_ID}"`, 'utf-8');

/**
* Minimal sourcemap object required to create the ignore list.
*/
Expand All @@ -29,6 +39,83 @@ interface SourceMap {
[IGNORE_LIST_ID]?: number[];
}

function extractSources(contents: Buffer): string[] | undefined {
const sourcesKeyIndex = contents.indexOf(SOURCES_KEY_BYTES);
if (sourcesKeyIndex === -1) {
return undefined;
}

// Find the ':' after "sources"
let colonIndex = sourcesKeyIndex + SOURCES_KEY_BYTES.length;
while (colonIndex < contents.length && contents[colonIndex] <= 0x20) {
colonIndex++;
}
if (contents[colonIndex] !== 0x3a /* : */) {
return undefined;
}

// Find the '[' for the array
let arrayStartIndex = colonIndex + 1;
while (arrayStartIndex < contents.length && contents[arrayStartIndex] <= 0x20) {
arrayStartIndex++;
}
if (contents[arrayStartIndex] !== 0x5b /* [ */) {
return undefined;
}

// Scan until matching ']'
let depth = 0;
let inString = false;
for (let i = arrayStartIndex; i < contents.length; i++) {
const byte = contents[i];
if (inString) {
if (byte === 0x5c /* \ */) {
i++; // skip escaped character
} else if (byte === 0x22 /* " */) {
inString = false;
}
} else if (byte === 0x22 /* " */) {
inString = true;
} else if (byte === 0x5b /* [ */) {
depth++;
} else if (byte === 0x5d /* ] */) {
depth--;
if (depth === 0) {
try {
const slice = contents.toString('utf-8', arrayStartIndex, i + 1);
const parsed = JSON.parse(slice);

return Array.isArray(parsed) && parsed.every((s) => typeof s === 'string')
? (parsed as string[])
: undefined;
} catch {
return undefined;
}
}
}
}

return undefined;
}

function updateSourcemapFast(contents: Buffer, ignoreList: readonly number[]): Buffer | undefined {
let braceIndex = 0;
while (braceIndex < contents.length && contents[braceIndex] <= 0x20) {
braceIndex++;
}
if (contents[braceIndex] !== 0x7b /* { */) {
return undefined;
}

const injection = Buffer.from(`"${IGNORE_LIST_ID}":${JSON.stringify(ignoreList)},`, 'utf-8');

return Buffer.concat([
contents.subarray(0, braceIndex + 1),
injection,
contents.subarray(braceIndex + 1),
]);
}

/**
* Creates an esbuild plugin that updates generated sourcemaps to include the Chrome
* DevTools ignore list extension. All source files that originate from a node modules
Expand Down Expand Up @@ -68,7 +155,40 @@ export function createSourcemapIgnorelistPlugin(): Plugin {
continue;
}

let fastPathSuccess = false;
if (!contents.includes(IGNORE_LIST_BYTES)) {
const sources = extractSources(contents);
if (sources) {
const ignoreList: number[] = [];
for (let index = 0; index < sources.length; ++index) {
const location = sources[index].indexOf('node_modules/');
if (location === 0 || (location > 0 && sources[index][location - 1] === '/')) {
ignoreList.push(index);
}
}

if (ignoreList.length === 0) {
continue;
}

const updated = updateSourcemapFast(contents, ignoreList);
if (updated) {
file.contents = updated;
fastPathSuccess = true;
}
}
}

if (fastPathSuccess) {
continue;
}

// Fallback to full JSON parse/stringify if fast scanning or splicing fails
const map = JSON.parse(contents.toString('utf-8')) as SourceMap;
if (map[IGNORE_LIST_ID]) {
continue;
}

const ignoreList = [];
Comment thread
alan-agius4 marked this conversation as resolved.

// Check and store the index of each source originating from a node modules directory
Expand Down