Skip to content
5 changes: 4 additions & 1 deletion lib/base64-vlq.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@

var base64 = require('./base64');

// Inlined base64 encode lookup for performance (avoids function call + bounds check)
var base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// A single base 64 digit can contain 6 bits of data. For the base 64 variable
// length quantities we use in the source map spec, the first bit is the sign,
// the next four bits are the actual value, and the 6th bit is the
Expand Down Expand Up @@ -103,7 +106,7 @@ exports.encode = function base64VLQ_encode(aValue) {
// continuation bit is marked.
digit |= VLQ_CONTINUATION_BIT;
}
encoded += base64.encode(digit);
encoded += base64Chars[digit];
} while (vlq > 0);

return encoded;
Expand Down
147 changes: 106 additions & 41 deletions lib/source-map-consumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
enumerable: true,
get: function () {
if (!this.__originalMappings) {
this._parseMappings(this._mappings, this.sourceRoot);
// Ensure generatedMappings are parsed first (may also set __originalMappings for IndexedSourceMapConsumer)
var generatedMappings = this._generatedMappings;
// Build originalMappings lazily if not already set (BasicSourceMapConsumer)
if (!this.__originalMappings) {
this._buildOriginalMappings();
}
}

return this.__originalMappings;
Expand Down Expand Up @@ -479,13 +484,25 @@ function sortGenerated(array, start) {
let n = array.length - start;
if (n <= 1) {
return;
} else if (n == 2) {
let a = array[start];
let b = array[start + 1];
if (compareGenerated(a, b) > 0) {
array[start] = b;
array[start + 1] = a;
}

// Check if already sorted (common case for well-formed source maps)
let sorted = true;
for (let i = start + 1; i < l; i++) {
if (compareGenerated(array[i - 1], array[i]) > 0) {
sorted = false;
break;
}
}
if (sorted) {
return;
}

if (n == 2) {
// Already checked above, must be out of order
let a = array[start];
array[start] = array[start + 1];
array[start + 1] = a;
} else if (n < 20) {
for (let i = start; i < l; i++) {
for (let j = i; j > start; j--) {
Expand All @@ -502,6 +519,25 @@ function sortGenerated(array, start) {
quickSort(array, compareGenerated, start);
}
}
// Lookup table for single-byte VLQ decode (no continuation bit)
// Maps base64 char code -> decoded signed value, or undefined if multi-byte
var vlqTable = [];
// Base64 decode table for multi-byte VLQ
var base64Table = new Int8Array(128);
(function() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
base64Table.fill(-1);
for (var i = 0; i < 64; i++) base64Table[chars.charCodeAt(i)] = i;
for (var i = 0; i < 128; i++) vlqTable[i] = undefined;
// Only first 32 base64 values (A-f) are single-byte VLQ (no continuation bit)
for (var i = 0; i < 32; i++) {
var charCode = chars.charCodeAt(i);
// Single-byte VLQ: bit 0 is sign, bits 1-4 are value
var value = i >> 1;
vlqTable[charCode] = (i & 1) ? -value : value;
}
})();

BasicSourceMapConsumer.prototype._parseMappings =
function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
var generatedLine = 1;
Expand All @@ -512,57 +548,73 @@ BasicSourceMapConsumer.prototype._parseMappings =
var previousName = 0;
var length = aStr.length;
var index = 0;
var cachedSegments = {};
var temp = {};
var originalMappings = [];
var generatedMappings = [];
var mapping, str, segment, end, value;
var mapping, value, charCode, digit, result, shift;
// Reuse segment array to avoid allocations per mapping
var segment = [0, 0, 0, 0, 0];
var segmentLength = 0;

let subarrayStart = 0;
while (index < length) {
if (aStr.charAt(index) === ';') {
charCode = aStr.charCodeAt(index);
if (charCode === 59) { // ';'
generatedLine++;
index++;
previousGeneratedColumn = 0;

sortGenerated(generatedMappings, subarrayStart);
subarrayStart = generatedMappings.length;
}
else if (aStr.charAt(index) === ',') {
else if (charCode === 44) { // ','
index++;
}
else {
mapping = new Mapping();
mapping.generatedLine = generatedLine;
mapping = {
generatedLine: generatedLine,
generatedColumn: 0,
source: null,
originalLine: null,
originalColumn: null,
name: null
};

for (end = index; end < length; end++) {
if (this._charIsMappingSeparator(aStr, end)) {
break;
// Decode VLQ values until we hit a separator
segmentLength = 0;
while (index < length) {
charCode = aStr.charCodeAt(index);
if (charCode === 44 || charCode === 59) break; // ',' or ';'
// Fast path for single-byte VLQ (most common case)
value = vlqTable[charCode];
if (value !== undefined) {
index++;
} else {
// Inline multi-byte VLQ decode
result = 0;
shift = 0;
do {
digit = base64Table[aStr.charCodeAt(index++)];
result += (digit & 31) << shift;
shift += 5;
} while (digit >= 32);
// Convert from VLQ signed
value = (result & 1) ? -(result >> 1) : (result >> 1);
}
}
str = aStr.slice(index, end);

segment = [];
while (index < end) {
base64VLQ.decode(aStr, index, temp);
value = temp.value;
index = temp.rest;
segment.push(value);
segment[segmentLength++] = value;
}

if (segment.length === 2) {
if (segmentLength === 2) {
throw new Error('Found a source, but no line and column');
}

if (segment.length === 3) {
if (segmentLength === 3) {
throw new Error('Found a source and line, but no column');
}

// Generated column.
mapping.generatedColumn = previousGeneratedColumn + segment[0];
previousGeneratedColumn = mapping.generatedColumn;

if (segment.length > 1) {
if (segmentLength > 1) {
// Original source.
mapping.source = previousSource + segment[1];
previousSource += segment[1];
Expand All @@ -577,29 +629,42 @@ BasicSourceMapConsumer.prototype._parseMappings =
mapping.originalColumn = previousOriginalColumn + segment[3];
previousOriginalColumn = mapping.originalColumn;

if (segment.length > 4) {
if (segmentLength > 4) {
// Original name.
mapping.name = previousName + segment[4];
previousName += segment[4];
}
}

generatedMappings.push(mapping);
if (typeof mapping.originalLine === 'number') {
let currentSource = mapping.source;
while (originalMappings.length <= currentSource) {
originalMappings.push(null);
}
if (originalMappings[currentSource] === null) {
originalMappings[currentSource] = [];
}
originalMappings[currentSource].push(mapping);
}
}
}

sortGenerated(generatedMappings, subarrayStart);
this.__generatedMappings = generatedMappings;
};

/**
* Build originalMappings lazily from generatedMappings.
*/
BasicSourceMapConsumer.prototype._buildOriginalMappings =
function SourceMapConsumer_buildOriginalMappings() {
var generatedMappings = this.__generatedMappings;
var originalMappings = [];

for (var i = 0; i < generatedMappings.length; i++) {
var mapping = generatedMappings[i];
if (typeof mapping.originalLine === 'number') {
var currentSource = mapping.source;
while (originalMappings.length <= currentSource) {
originalMappings.push(null);
}
if (originalMappings[currentSource] === null) {
originalMappings[currentSource] = [];
}
originalMappings[currentSource].push(mapping);
}
}

for (var i = 0; i < originalMappings.length; i++) {
if (originalMappings[i] != null) {
Expand Down
31 changes: 22 additions & 9 deletions lib/source-map-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,17 @@ SourceMapGenerator.prototype._validateMapping =
}
};

// Fast VLQ encode lookup for values -15 to 15 (single char output)
var vlqEncodeTable = [];
(function() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
for (var i = -15; i <= 15; i++) {
// VLQ signed: negative becomes odd, positive becomes even
var vlq = i < 0 ? ((-i) << 1) + 1 : (i << 1);
vlqEncodeTable[i + 15] = chars[vlq];
}
})();

/**
* Serialize the accumulated mappings in to the stream of base 64 VLQs
* specified by the source map format.
Expand All @@ -336,7 +347,7 @@ SourceMapGenerator.prototype._serializeMappings =
var previousName = 0;
var previousSource = 0;
var result = '';
var next;
var next, val;
var mapping;
var nameIdx;
var sourceIdx;
Expand All @@ -362,27 +373,29 @@ SourceMapGenerator.prototype._serializeMappings =
}
}

next += base64VLQ.encode(mapping.generatedColumn
- previousGeneratedColumn);
val = mapping.generatedColumn - previousGeneratedColumn;
next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val);
previousGeneratedColumn = mapping.generatedColumn;

if (mapping.source != null) {
sourceIdx = this._sources.indexOf(mapping.source);
next += base64VLQ.encode(sourceIdx - previousSource);
val = sourceIdx - previousSource;
next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val);
previousSource = sourceIdx;

// lines are stored 0-based in SourceMap spec version 3
next += base64VLQ.encode(mapping.originalLine - 1
- previousOriginalLine);
val = mapping.originalLine - 1 - previousOriginalLine;
next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val);
previousOriginalLine = mapping.originalLine - 1;

next += base64VLQ.encode(mapping.originalColumn
- previousOriginalColumn);
val = mapping.originalColumn - previousOriginalColumn;
next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val);
previousOriginalColumn = mapping.originalColumn;

if (mapping.name != null) {
nameIdx = this._names.indexOf(mapping.name);
next += base64VLQ.encode(nameIdx - previousName);
val = nameIdx - previousName;
next += (val >= -15 && val <= 15) ? vlqEncodeTable[val + 15] : base64VLQ.encode(val);
previousName = nameIdx;
}
}
Expand Down