OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / webpack-sources / lib / helpers / createMappingsSerializer.js

479 lines · javascript ref:
/*
	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;