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
2 changes: 1 addition & 1 deletion packages/metro-source-map/src/B64Builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class B64Builder {
return this;
}
this.hasSegment = false;
if (this.pos + n >= this.buffer.length) {
while (this.pos + n >= this.buffer.length) {
this._realloc();
}
while (n--) {
Expand Down
6 changes: 6 additions & 0 deletions packages/metro-source-map/src/__tests__/B64Builder-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ test('can mark zero new lines in the generated code', () => {
expect(builder.toString()).toEqual('');
});

test('can mark more new lines than twice the initial buffer size', () => {
const lines = 3 * 1024 * 1024;
builder.startSegment(0).markLines(lines).startSegment(0);
expect(builder.toString()).toEqual('A' + ';'.repeat(lines) + 'A');
});

test('does not add commas when just starting a segment', () => {
builder.startSegment(0);
expect(builder.toString()).toEqual('A');
Expand Down
230 changes: 194 additions & 36 deletions packages/metro-source-map/src/composeSourceMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@
*/

import type {SourcePosition} from './Consumer/types';
import type {IConsumer, MixedSourceMap} from './source-map';
import type {
FBSourcesArray,
HermesFunctionOffsets,
IConsumer,
MixedSourceMap,
} from './source-map';
import type {Number0, Number1} from 'ob1';

import B64Builder from './B64Builder';
import Consumer from './Consumer';
import {SourceMapGenerator} from 'source-map';
import {get0, get1} from 'ob1';

// TODO(t67648443): Bypass the `sort-requires` rule for this file because of a dependency cycle.
Consumer;
Expand All @@ -41,45 +47,189 @@ export default function composeSourceMaps(
})
.reverse();

const generator = new SourceMapGenerator({
file: consumers[0].file,
});
const sources: Array<string> = [];
const names: Array<string> = [];
const sourceIndices: Map<string, number> = new Map();
const nameIndices: Map<string, number> = new Map();

// The composed mappings as parallel arrays, in the order the last map
// yields them. An unmapped position has source index -1 and original
// line and column 0.
const generatedLines: Array<number> = [];
const generatedColumns: Array<number> = [];
const mappingSources: Array<number> = [];
const originalLines: Array<number> = [];
const originalColumns: Array<number> = [];
const mappingNames: Array<number> = [];
let sorted = true;

const earlierConsumers = consumers.slice(1);
consumers[0].eachMapping(mapping => {
const original =
mapping.originalLine == null || mapping.originalColumn == null
? {line: null, column: null, source: null, name: null}
: earlierConsumers.length === 0
? {
line: mapping.originalLine,
column: mapping.originalColumn,
source: mapping.source,
name: mapping.name,
}
: findOriginalPosition(
earlierConsumers,
mapping.originalLine,
mapping.originalColumn,
);
generator.addMapping({
generated: {
line: mapping.generatedLine,
column: mapping.generatedColumn,
},
original:
original.line != null
? {
line: original.line,
column: original.column,
}
: null,
source: original.source,
name: original.name,
});
let originalLine: ?number = null;
let originalColumn: ?number = null;
let source: ?string = null;
let name: ?string = null;
const mappingOriginalLine = mapping.originalLine;
const mappingOriginalColumn = mapping.originalColumn;
if (mappingOriginalLine != null && mappingOriginalColumn != null) {
if (earlierConsumers.length === 0) {
originalLine = get1(mappingOriginalLine);
originalColumn = get0(mappingOriginalColumn);
source = mapping.source;
name = mapping.name;
} else {
const original = findOriginalPosition(
earlierConsumers,
mappingOriginalLine,
mappingOriginalColumn,
);
originalLine = original.line;
originalColumn = original.column;
source = original.source;
name = original.name;
}
}
const generatedLine = get1(mapping.generatedLine);
const generatedColumn = get0(mapping.generatedColumn);
const hasOriginal = originalLine != null;
if (!(
generatedLine > 0 &&
generatedColumn >= 0 &&
(originalLine != null
? originalLine > 0 && (originalColumn ?? 0) >= 0 && source
Comment thread
Copilot marked this conversation as resolved.
: !source && !name)
)) {
throw new Error(
'Invalid mapping: ' +
JSON.stringify({
generated: {line: generatedLine, column: generatedColumn},
source,
original: hasOriginal
? {line: originalLine, column: originalColumn}
: null,
name,
}),
);
}

const count = generatedLines.length;
if (
count > 0 &&
(generatedLine < generatedLines[count - 1] ||
(generatedLine === generatedLines[count - 1] &&
generatedColumn < generatedColumns[count - 1]))
) {
sorted = false;
}
generatedLines.push(generatedLine);
generatedColumns.push(generatedColumn);
if (originalLine != null && source != null) {
let sourceIndex = sourceIndices.get(source);
if (sourceIndex == null) {
sourceIndex = sources.length;
sources.push(source);
sourceIndices.set(source, sourceIndex);
}
mappingSources.push(sourceIndex);
originalLines.push(originalLine);
originalColumns.push(originalColumn ?? 0);
} else {
mappingSources.push(-1);
originalLines.push(0);
originalColumns.push(0);
}
if (name != null) {
let nameIndex = nameIndices.get(name);
if (nameIndex == null) {
nameIndex = names.length;
names.push(name);
nameIndices.set(name, nameIndex);
}
mappingNames.push(nameIndex);
} else {
mappingNames.push(-1);
}
});

const composedMap = generator.toJSON();
// Orders two composed mappings by generated position, then source,
// original position and name.
const compare = (a: number, b: number): number =>
generatedLines[a] - generatedLines[b] ||
generatedColumns[a] - generatedColumns[b] ||
compareStrings(
mappingSources[a] === -1 ? null : sources[mappingSources[a]],
mappingSources[b] === -1 ? null : sources[mappingSources[b]],
) ||
originalLines[a] - originalLines[b] ||
originalColumns[a] - originalColumns[b] ||
compareStrings(
mappingNames[a] === -1 ? null : names[mappingNames[a]],
mappingNames[b] === -1 ? null : names[mappingNames[b]],
);

const count = generatedLines.length;
let order: ?Array<number> = null;
if (!sorted) {
order = Array.from({length: count}, (_, i) => i);
order.sort(compare);
}

// Encode the mappings, dropping any identical to the one before it.
const builder = new B64Builder();
let previousIndex = -1;
let previousGeneratedLine = 1;
let previousGeneratedColumn = 0;
let previousSource = 0;
let previousOriginalLine = 0;
let previousOriginalColumn = 0;
let previousName = 0;
for (let position = 0; position < count; position++) {
const index = order == null ? position : order[position];
const generatedLine = generatedLines[index];
if (generatedLine !== previousGeneratedLine) {
builder.markLines(generatedLine - previousGeneratedLine);
Comment thread
robhogan marked this conversation as resolved.
previousGeneratedLine = generatedLine;
previousGeneratedColumn = 0;
} else if (previousIndex !== -1 && compare(index, previousIndex) === 0) {
previousIndex = index;
continue;
}
previousIndex = index;

builder.startSegment(generatedColumns[index] - previousGeneratedColumn);
previousGeneratedColumn = generatedColumns[index];
const sourceIndex = mappingSources[index];
if (sourceIndex !== -1) {
builder.append(sourceIndex - previousSource);
previousSource = sourceIndex;
// Original lines are 0-based in the encoding.
builder.append(originalLines[index] - 1 - previousOriginalLine);
previousOriginalLine = originalLines[index] - 1;
builder.append(originalColumns[index] - previousOriginalColumn);
previousOriginalColumn = originalColumns[index];
const nameIndex = mappingNames[index];
if (nameIndex !== -1) {
builder.append(nameIndex - previousName);
previousName = nameIndex;
}
}
}

const composedMap: {
version: number,
sources: Array<string>,
names: Array<string>,
mappings: string,
file?: string,
sourcesContent?: Array<?string>,
x_facebook_sources?: FBSourcesArray,
x_hermes_function_offsets?: HermesFunctionOffsets,
x_google_ignoreList?: Array<number>,
} = {version: 3, sources, names, mappings: builder.toString()};
const {file} = consumers[0];
if (file != null) {
composedMap.file = file;
}

composedMap.sourcesContent = composedMap.sources.map(source =>
consumers[consumers.length - 1].sourceContentFor(source, true),
Expand All @@ -103,6 +253,14 @@ export default function composeSourceMaps(
return composedMap;
}

function compareStrings(a: ?string, b: ?string): number {
if (a === b) {
return 0;
}
// $FlowFixMe[invalid-compare] Matches `source-map`'s ordering, null included
return a > b ? 1 : -1;
}

function findOriginalPosition(
consumers: ReadonlyArray<IConsumer>,
generatedLine: Number1,
Expand Down
Loading