Skip to content
Draft
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
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
: !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 on lines +189 to +190
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