UNPKG

webpack-sources

Version:

Source code handling classes for webpack

479 lines (455 loc) 13.8 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; /** * @callback MappingsSerializer * @param {number} generatedLine generated line * @param {number} generatedColumn generated column * @param {number} sourceIndex source index * @param {number} originalLine original line * @param {number} originalColumn generated line * @param {number} nameIndex generated line * @returns {string} result */ /** * A push-based serializer: `add()` appends one mapping to an internal * byte buffer, `finish()` materialises the whole `mappings` string in a * single allocation. Compared to the string-returning * {@link MappingsSerializer} this avoids every per-mapping intermediate * string (each VLQ digit concatenation) plus the caller-side * `mappings += str` cons chain — together the dominant allocation site * of `map()` / `sourceAndMap()`. * @typedef {object} MappingsWriter * @property {(generatedLine: number, generatedColumn: number, sourceIndex: number, originalLine: number, originalColumn: number, nameIndex: number) => void} add append one mapping * @property {() => string} finish materialise the mappings string */ const ALPHABET = [ ..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", ]; // Char codes of ALPHABET for the buffer-writing path. const ALPHABET_CODES = new Uint8Array(64); for (let i = 0; i < 64; i++) ALPHABET_CODES[i] = ALPHABET[i].charCodeAt(0); const CH_SEMICOLON = 59; // ; const CH_COMMA = 44; // , const CH_A = 65; // A const CONTINUATION_BIT = 0x20; /** * Append a VLQ-encoded signed integer to `str`. Hoisted to module scope so * that both serializers share a single function object and avoid allocating * a new closure on every call. * @param {string} str current string buffer * @param {number} value signed integer to encode * @returns {string} updated string buffer */ const writeValue = (str, value) => { const sign = (value >>> 31) & 1; const mask = value >> 31; const absValue = (value + mask) ^ mask; let data = (absValue << 1) | sign; for (;;) { const sextet = data & 0x1f; data >>= 5; if (data === 0) { return str + ALPHABET[sextet]; } str += ALPHABET[sextet | CONTINUATION_BIT]; } }; /** * Byte-buffer state shared by the writer variants. A plain object (not a * class) so the hot `add` closures capture it directly. * @returns {{ buf: Uint8Array, pos: number }} state */ const createBufferState = () => ({ buf: new Uint8Array(1024), pos: 0 }); /** * Ensure space for `n` more bytes. * @param {{ buf: Uint8Array, pos: number }} state state * @param {number} n bytes needed * @returns {Uint8Array} the (possibly grown) buffer */ const ensure = (state, n) => { const { buf, pos } = state; if (pos + n <= buf.length) return buf; let nextLength = buf.length * 2; while (nextLength < pos + n) nextLength *= 2; const next = new Uint8Array(nextLength); next.set(buf); state.buf = next; return next; }; /** * Append a VLQ-encoded signed integer to the byte buffer. The caller must * have reserved space already (a 32-bit value needs at most 7 sextets). * @param {{ buf: Uint8Array, pos: number }} state state * @param {number} value signed integer to encode * @returns {void} */ const writeValueBytes = (state, value) => { const { buf } = state; let { pos } = state; const sign = (value >>> 31) & 1; const mask = value >> 31; const absValue = (value + mask) ^ mask; let data = (absValue << 1) | sign; for (;;) { const sextet = data & 0x1f; data >>= 5; if (data === 0) { buf[pos++] = ALPHABET_CODES[sextet]; break; } buf[pos++] = ALPHABET_CODES[sextet | CONTINUATION_BIT]; } state.pos = pos; }; /** * @param {{ buf: Uint8Array, pos: number }} state state * @returns {string} the mappings accumulated so far */ const bufferToString = (state) => { if (state.pos === 0) return ""; // The mappings alphabet is pure ASCII, so a latin1 decode is exact and // needs no re-encoding pass. return Buffer.from(state.buf.buffer, 0, state.pos).toString("latin1"); }; /** * @returns {MappingsWriter} writer */ const createFullMappingsWriter = () => { const state = createBufferState(); let currentLine = 1; let currentColumn = 0; let currentSourceIndex = 0; let currentOriginalLine = 1; let currentOriginalColumn = 0; let currentNameIndex = 0; let activeMapping = false; let activeName = false; let initial = true; return { add( generatedLine, generatedColumn, sourceIndex, originalLine, originalColumn, nameIndex, ) { if (activeMapping && currentLine === generatedLine) { // A mapping is still active if ( sourceIndex === currentSourceIndex && originalLine === currentOriginalLine && originalColumn === currentOriginalColumn && !activeName && nameIndex < 0 ) { // avoid repeating the same original mapping return; } } // No mapping is active else if (sourceIndex < 0) { // avoid writing unneccessary generated mappings return; } // Reserve the worst case for one mapping in a single check: // line separators + 5 VLQ values of up to 7 sextets each. const lineDiff = generatedLine - currentLine; const buf = ensure(state, (lineDiff > 0 ? lineDiff : 1) + 35); if (lineDiff > 0) { // Consecutive lines (diff === 1) are the dominant case. buf[state.pos++] = CH_SEMICOLON; for (let i = 1; i < lineDiff; i++) buf[state.pos++] = CH_SEMICOLON; currentLine = generatedLine; currentColumn = 0; initial = false; } else if (initial) { initial = false; } else { buf[state.pos++] = CH_COMMA; } writeValueBytes(state, generatedColumn - currentColumn); currentColumn = generatedColumn; if (sourceIndex >= 0) { activeMapping = true; if (sourceIndex === currentSourceIndex) { buf[state.pos++] = CH_A; } else { writeValueBytes(state, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; } writeValueBytes(state, originalLine - currentOriginalLine); currentOriginalLine = originalLine; if (originalColumn === currentOriginalColumn) { buf[state.pos++] = CH_A; } else { writeValueBytes(state, originalColumn - currentOriginalColumn); currentOriginalColumn = originalColumn; } if (nameIndex >= 0) { writeValueBytes(state, nameIndex - currentNameIndex); currentNameIndex = nameIndex; activeName = true; } else { activeName = false; } } else { activeMapping = false; } }, finish: () => bufferToString(state), }; }; const createFullMappingsSerializer = () => { let currentLine = 1; let currentColumn = 0; let currentSourceIndex = 0; let currentOriginalLine = 1; let currentOriginalColumn = 0; let currentNameIndex = 0; let activeMapping = false; let activeName = false; let initial = true; /** @type {MappingsSerializer} */ return ( generatedLine, generatedColumn, sourceIndex, originalLine, originalColumn, nameIndex, ) => { if (activeMapping && currentLine === generatedLine) { // A mapping is still active if ( sourceIndex === currentSourceIndex && originalLine === currentOriginalLine && originalColumn === currentOriginalColumn && !activeName && nameIndex < 0 ) { // avoid repeating the same original mapping return ""; } } // No mapping is active else if (sourceIndex < 0) { // avoid writing unneccessary generated mappings return ""; } let str; if (currentLine < generatedLine) { // Consecutive lines (diff === 1) are the dominant case; avoid the // `.repeat()` call entirely for them. str = generatedLine === currentLine + 1 ? ";" : ";".repeat(generatedLine - currentLine); currentLine = generatedLine; currentColumn = 0; initial = false; } else if (initial) { str = ""; initial = false; } else { str = ","; } str = writeValue(str, generatedColumn - currentColumn); currentColumn = generatedColumn; if (sourceIndex >= 0) { activeMapping = true; if (sourceIndex === currentSourceIndex) { str += "A"; } else { str = writeValue(str, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; } str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; if (originalColumn === currentOriginalColumn) { str += "A"; } else { str = writeValue(str, originalColumn - currentOriginalColumn); currentOriginalColumn = originalColumn; } if (nameIndex >= 0) { str = writeValue(str, nameIndex - currentNameIndex); currentNameIndex = nameIndex; activeName = true; } else { activeName = false; } } else { activeMapping = false; } return str; }; }; const createLinesOnlyMappingsSerializer = () => { let lastWrittenLine = 0; let currentLine = 1; let currentSourceIndex = 0; let currentOriginalLine = 1; /** @type {MappingsSerializer} */ return ( generatedLine, _generatedColumn, sourceIndex, originalLine, _originalColumn, _nameIndex, ) => { if (sourceIndex < 0) { // avoid writing generated mappings at all return ""; } if (lastWrittenLine === generatedLine) { // avoid writing multiple original mappings per line return ""; } let str; lastWrittenLine = generatedLine; if (generatedLine === currentLine + 1) { currentLine = generatedLine; if (sourceIndex === currentSourceIndex) { if (originalLine === currentOriginalLine + 1) { currentOriginalLine = originalLine; return ";AACA"; } str = ";AA"; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; return `${str}A`; } str = ";A"; str = writeValue(str, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; return `${str}A`; } str = ";".repeat(generatedLine - currentLine); currentLine = generatedLine; if (sourceIndex === currentSourceIndex) { if (originalLine === currentOriginalLine + 1) { currentOriginalLine = originalLine; return `${str}AACA`; } str += "AA"; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; return `${str}A`; } str += "A"; str = writeValue(str, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; return `${str}A`; }; }; /** * Lines-only mappings emit at most one short — usually constant — segment * per generated line, so the classic string encoding plus cons-string * append is already optimal there (measurably faster than byte-writing). * Only the full serializer, which emits per token, benefits from the byte * buffer. The encoding below mirrors `createLinesOnlyMappingsSerializer`, * inlined so `add` costs a single call. * @returns {MappingsWriter} writer */ const createLinesOnlyMappingsWriter = () => { let mappings = ""; let lastWrittenLine = 0; let currentLine = 1; let currentSourceIndex = 0; let currentOriginalLine = 1; return { add( generatedLine, _generatedColumn, sourceIndex, originalLine, _originalColumn, _nameIndex, ) { if (sourceIndex < 0) { // avoid writing generated mappings at all return; } if (lastWrittenLine === generatedLine) { // avoid writing multiple original mappings per line return; } let str; lastWrittenLine = generatedLine; if (generatedLine === currentLine + 1) { currentLine = generatedLine; if (sourceIndex === currentSourceIndex) { if (originalLine === currentOriginalLine + 1) { currentOriginalLine = originalLine; mappings += ";AACA"; return; } str = ";AA"; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; mappings += `${str}A`; return; } str = ";A"; str = writeValue(str, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; mappings += `${str}A`; return; } str = ";".repeat(generatedLine - currentLine); currentLine = generatedLine; if (sourceIndex === currentSourceIndex) { if (originalLine === currentOriginalLine + 1) { currentOriginalLine = originalLine; mappings += `${str}AACA`; return; } str += "AA"; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; mappings += `${str}A`; return; } str += "A"; str = writeValue(str, sourceIndex - currentSourceIndex); currentSourceIndex = sourceIndex; str = writeValue(str, originalLine - currentOriginalLine); currentOriginalLine = originalLine; mappings += `${str}A`; }, finish: () => mappings, }; }; /** * @param {{ columns?: boolean }=} options options * @returns {MappingsSerializer} mappings serializer */ const createMappingsSerializer = (options) => { const linesOnly = options && options.columns === false; return linesOnly ? createLinesOnlyMappingsSerializer() : createFullMappingsSerializer(); }; /** * @param {{ columns?: boolean }=} options options * @returns {MappingsWriter} push-based mappings writer */ const createMappingsWriter = (options) => { const linesOnly = options && options.columns === false; return linesOnly ? createLinesOnlyMappingsWriter() : createFullMappingsWriter(); }; module.exports = createMappingsSerializer; module.exports.createMappingsWriter = createMappingsWriter;