Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 1 addition & 2 deletions packages/metro-source-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
"metro-symbolicate": "0.87.1",
"nullthrows": "^1.1.1",
"ob1": "0.87.1",
"source-map": "^0.5.6",
"vlq": "^1.0.0"

@robhogan robhogan Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe to merge without Meta import because even though we're dropping a dependency here, metro-symbolicate has the same vlq dependency, so no lockfile change.

(This code snippet makes it look like we're dropping two deps - we're not, source-map just loses the trailing comma)

"source-map": "^0.5.6"
},
"license": "MIT",
"devDependencies": {
Expand Down
152 changes: 86 additions & 66 deletions packages/metro-source-map/src/Consumer/MappingsConsumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,23 @@ import normalizeSourcePath from './normalizeSourcePath';
import {greatestLowerBound} from './search';
import invariant from 'invariant';
import {add, add0, get0, inc, sub} from 'ob1';
import {decode as decodeVlq} from 'vlq';

/* eslint-disable no-bitwise */

const COMMA = 44; // ','
const SEMICOLON = 59; // ';'
const VLQ_BASE_SHIFT = 5;
const VLQ_BASE_MASK = (1 << VLQ_BASE_SHIFT) - 1;
const VLQ_CONTINUATION_BIT = 1 << VLQ_BASE_SHIFT;
const BASE64_DECODE: Int8Array = (() => {
const table = new Int8Array(128).fill(-1);
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
for (let i = 0; i < chars.length; i++) {
table[chars.charCodeAt(i)] = i;
}
return table;
})();

/**
* A source map consumer that supports "basic" source maps (that have a
Expand Down Expand Up @@ -93,85 +109,89 @@ export default class MappingsConsumer
return {...EMPTY_POSITION};
}

*_decodeMappings(): Generator<Mapping, void, void> {
_decodeMappings(): Array<Mapping> {
const normalizedSources = this._normalizeAndCacheSources();
const {mappings: mappingsRaw, names} = this._sourceMap;
const result: Array<Mapping> = [];

let generatedLine = FIRST_LINE;
let generatedColumn = FIRST_COLUMN;
let originalLine = FIRST_LINE;
let originalColumn = FIRST_COLUMN;
let nameIndex = add0(0);
let sourceIndex = add0(0);

const normalizedSources = this._normalizeAndCacheSources();

const {mappings: mappingsRaw, names} = this._sourceMap;
let next;
const vlqCache = new Map<string, any>();
for (let i = 0; i < mappingsRaw.length; i = next) {
switch (mappingsRaw[i]) {
case ';':
// The VLQ fields of the segment being decoded, decoded in place from
// character codes rather than by slicing out each segment.
const fields = [0, 0, 0, 0, 0];
let fieldCount = 0;
let value = 0;
let shift = 0;

const length = mappingsRaw.length;
for (let i = 0; i <= length; i++) {
const charCode = i < length ? mappingsRaw.charCodeAt(i) : SEMICOLON;
if (charCode === COMMA || charCode === SEMICOLON) {
// A trailing field cut off mid-VLQ is dropped, as `vlq` does.
invariant(
fieldCount > 0 || shift === 0,
'Invalid generated column delta',
);
value = 0;
shift = 0;
if (fieldCount > 0) {
invariant(fieldCount !== 2, 'Invalid original line delta');
invariant(fieldCount !== 3, 'Invalid original column delta');
generatedColumn = add(generatedColumn, fields[0]);
let source = null;
let name = null;
let mappingOriginalLine = null;
let mappingOriginalColumn = null;
if (fieldCount >= 4) {
sourceIndex = add(sourceIndex, fields[1]);
source = normalizedSources[get0(sourceIndex)];
originalLine = add(originalLine, fields[2]);
originalColumn = add(originalColumn, fields[3]);
mappingOriginalLine = originalLine;
mappingOriginalColumn = originalColumn;
if (fieldCount >= 5) {
nameIndex = add(nameIndex, fields[4]);
name = names[get0(nameIndex)];
}
}
result.push({
generatedLine,
generatedColumn,
source,
name,
originalLine: mappingOriginalLine,
originalColumn: mappingOriginalColumn,
});
fieldCount = 0;
}
if (charCode === SEMICOLON && i < length) {
generatedLine = inc(generatedLine);
generatedColumn = FIRST_COLUMN;
/* falls through */
case ',':
next = i + 1;
continue;
}
findNext: for (next = i + 1; next < mappingsRaw.length; ++next) {
switch (mappingsRaw[next]) {
case ';':
/* falls through */
case ',':
break findNext;
}
continue;
}
const mappingRaw = mappingsRaw.slice(i, next);
let decodedVlqValues;
if (vlqCache.has(mappingRaw)) {
decodedVlqValues = vlqCache.get(mappingRaw);
const digit = charCode < 128 ? BASE64_DECODE[charCode] : -1;
invariant(digit !== -1, 'Invalid character in source map mappings');
value = value + ((digit & VLQ_BASE_MASK) << shift);
if (digit & VLQ_CONTINUATION_BIT) {
shift = shift + VLQ_BASE_SHIFT;
} else {
decodedVlqValues = decodeVlq(mappingRaw);
vlqCache.set(mappingRaw, decodedVlqValues);
}
invariant(Array.isArray(decodedVlqValues), 'Decoding VLQ tuple failed');
const [
generatedColumnDelta,
sourceIndexDelta,
originalLineDelta,
originalColumnDelta,
nameIndexDelta,
] = decodedVlqValues;
invariant(generatedColumnDelta != null, 'Invalid generated column delta');
generatedColumn = add(generatedColumn, generatedColumnDelta);
const mapping: {...Mapping, ...} = {
generatedLine,
generatedColumn,
source: null,
name: null,
originalLine: null,
originalColumn: null,
};

if (sourceIndexDelta != null) {
sourceIndex = add(sourceIndex, sourceIndexDelta);
mapping.source = normalizedSources[get0(sourceIndex)];

invariant(originalLineDelta != null, 'Invalid original line delta');
invariant(originalColumnDelta != null, 'Invalid original column delta');

originalLine = add(originalLine, originalLineDelta);
originalColumn = add(originalColumn, originalColumnDelta);

mapping.originalLine = originalLine;
mapping.originalColumn = originalColumn;

if (nameIndexDelta != null) {
nameIndex = add(nameIndex, nameIndexDelta);
mapping.name = names[get0(nameIndex)];
const negate = value & 1;
value = value >>> 1;
if (fieldCount < 5) {
fields[fieldCount] = negate ? -value : value;
}
fieldCount++;
value = 0;
shift = 0;
}

yield mapping;
}
return result;
}

_normalizeAndCacheSources(): ReadonlyArray<string> {
Expand All @@ -185,7 +205,7 @@ export default class MappingsConsumer

_decodeAndCacheMappings(): ReadonlyArray<Mapping> {
if (!this._decodedMappings) {
this._decodedMappings = [...this._decodeMappings()];
this._decodedMappings = this._decodeMappings();
}
return this._decodedMappings;
}
Expand Down
58 changes: 58 additions & 0 deletions packages/metro-source-map/src/__tests__/Consumer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,64 @@ describe('basic maps', () => {
]
`);
});

test('multi-digit and negative deltas, empty segments and lines', () => {
expect([
...new Consumer({
version: 3,
mappings: 'AAAU,gBCoBHC,,w+B;;KDjBg9D',
names: ['name0', 'name1'],
sources: ['source0', 'source1'],
}).generatedMappings(),
]).toEqual([
{
generatedLine: 1,
generatedColumn: 0,
source: 'source0',
name: null,
originalLine: 1,
originalColumn: 10,
},
{
generatedLine: 1,
generatedColumn: 16,
source: 'source1',
name: 'name1',
originalLine: 21,
originalColumn: 7,
},
{
generatedLine: 1,
generatedColumn: 1016,
source: null,
name: null,
originalLine: null,
originalColumn: null,
},
{
generatedLine: 3,
generatedColumn: 5,
source: 'source0',
name: null,
originalLine: 4,
originalColumn: 2007,
},
]);
});

test.each([
['AA', 'Invalid original line delta'],
['AAAA,AAA', 'Invalid original column delta'],
['AAAA,A!AA', undefined],
])('throws on malformed mappings %s', (mappings, message) => {
const consumer = new Consumer({
version: 3,
mappings,
names: [],
sources: ['source0'],
});
expect(() => [...consumer.generatedMappings()]).toThrow(message);
});
});

describe('sourceContentFor', () => {
Expand Down
Loading