Skip to content

Commit

Permalink
Implement a source map consumer class
Browse files Browse the repository at this point in the history
Reviewed By: cpojer

Differential Revision: D16055594

fbshipit-source-id: 5b932783cce84fcfb82dab7461e8afc4df6bb735
  • Loading branch information
motiz88 authored and facebook-github-bot committed Jul 3, 2019
1 parent a2810a6 commit ccd508c
Show file tree
Hide file tree
Showing 16 changed files with 1,423 additions and 24 deletions.
5 changes: 4 additions & 1 deletion packages/metro-source-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"dependencies": {
"@babel/traverse": "^7.0.0",
"@babel/types": "^7.0.0",
"source-map": "^0.5.6"
"invariant": "^2.2.4",
"ob1": "0.54.1",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
},
"license": "MIT",
"devDependencies": {
Expand Down
63 changes: 63 additions & 0 deletions packages/metro-source-map/src/Consumer/AbstractConsumer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

const invariant = require('invariant');

const {GENERATED_ORDER, iterationOrderToString} = require('./constants');

import type {
SourcePosition,
GeneratedPositionLookup,
Mapping,
IConsumer,
IterationOrder,
} from './types.flow';

// Implementation details shared between MappingsConsumer and SectionsConsumer
class AbstractConsumer implements IConsumer {
_sourceMap: {+file?: string};

constructor(sourceMap: {+file?: string}) {
this._sourceMap = sourceMap;
}

originalPositionFor(
generatedPosition: GeneratedPositionLookup,
): SourcePosition {
invariant(false, 'Not implemented');
}

generatedMappings(): Iterable<Mapping> {
invariant(false, 'Not implemented');
}

eachMapping(
callback: (mapping: Mapping) => mixed,
context?: mixed = null,
order?: IterationOrder = GENERATED_ORDER,
) {
invariant(
order === GENERATED_ORDER,
`Iteration order not implemented: ${iterationOrderToString(order)}`,
);
for (const mapping of this.generatedMappings()) {
callback.call(context, mapping);
}
}

// flowlint unsafe-getters-setters:off
get file(): ?string {
return this._sourceMap.file;
}
}

module.exports = AbstractConsumer;
73 changes: 73 additions & 0 deletions packages/metro-source-map/src/Consumer/DelegatingConsumer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

const createConsumer = require('./createConsumer');

const {
GENERATED_ORDER,
ORIGINAL_ORDER,
GREATEST_LOWER_BOUND,
LEAST_UPPER_BOUND,
} = require('./constants');

import type {MixedSourceMap} from '../source-map';
import type {
SourcePosition,
GeneratedPositionLookup,
Mapping,
IConsumer,
IterationOrder,
} from './types.flow';

/**
* A source map consumer that supports both "basic" and "indexed" source maps.
* Uses `MappingsConsumer` and `SectionsConsumer` under the hood (via
* `createConsumer`).
*/
class DelegatingConsumer implements IConsumer {
static +GENERATED_ORDER = GENERATED_ORDER;
static +ORIGINAL_ORDER = ORIGINAL_ORDER;
static +GREATEST_LOWER_BOUND = GREATEST_LOWER_BOUND;
static +LEAST_UPPER_BOUND = LEAST_UPPER_BOUND;

_rootConsumer: IConsumer;

constructor(sourceMap: MixedSourceMap) {
this._rootConsumer = createConsumer(sourceMap);
return this._rootConsumer;
}

originalPositionFor(
generatedPosition: GeneratedPositionLookup,
): SourcePosition {
return this._rootConsumer.originalPositionFor(generatedPosition);
}

generatedMappings(): Iterable<Mapping> {
return this._rootConsumer.generatedMappings();
}

eachMapping(
callback: (mapping: Mapping) => mixed,
context?: mixed,
order?: IterationOrder,
): void {
return this._rootConsumer.eachMapping(callback, context, order);
}

// flowlint unsafe-getters-setters:off
get file(): ?string {
return this._rootConsumer.file;
}
}

module.exports = DelegatingConsumer;
197 changes: 197 additions & 0 deletions packages/metro-source-map/src/Consumer/MappingsConsumer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/

'use strict';

const AbstractConsumer = require('./AbstractConsumer');

const invariant = require('invariant');
const normalizeSourcePath = require('./normalizeSourcePath');

const {
FIRST_COLUMN,
FIRST_LINE,
GREATEST_LOWER_BOUND,
EMPTY_POSITION,
lookupBiasToString,
} = require('./constants');
const {greatestLowerBound} = require('./search');
const {add, get0, add0, sub, inc} = require('ob1');
const {decode: decodeVlq} = require('vlq');

import type {BasicSourceMap} from '../source-map';
import type {
SourcePosition,
GeneratedPositionLookup,
Mapping,
IConsumer,
} from './types.flow';

/**
* A source map consumer that supports "basic" source maps (that have a
* `mappings` field and no sections).
*/
class MappingsConsumer extends AbstractConsumer implements IConsumer {
_sourceMap: BasicSourceMap;
_decodedMappings: ?$ReadOnlyArray<Mapping>;
_normalizedSources: ?$ReadOnlyArray<string>;

constructor(sourceMap: BasicSourceMap) {
super(sourceMap);
this._sourceMap = sourceMap;
this._decodedMappings = null;
this._normalizedSources = null;
}

originalPositionFor(
generatedPosition: GeneratedPositionLookup,
): SourcePosition {
const {line, column} = generatedPosition;
if (line == null || column == null) {
return {...EMPTY_POSITION};
}
if (generatedPosition.bias != null) {
invariant(
generatedPosition.bias === GREATEST_LOWER_BOUND,
`Unimplemented lookup bias: ${lookupBiasToString(
generatedPosition.bias,
)}`,
);
}
const mappings = this._decodeAndCacheMappings();
const index = greatestLowerBound(
mappings,
{line, column},
(position, mapping) => {
if (position.line === mapping.generatedLine) {
return get0(sub(position.column, mapping.generatedColumn));
}
return get0(sub(position.line, mapping.generatedLine));
},
);
if (
index != null &&
mappings[index].generatedLine === generatedPosition.line
) {
const mapping = mappings[index];
return {
source: mapping.source,
name: mapping.name,
line: mapping.originalLine,
column: mapping.originalColumn,
};
}
return {...EMPTY_POSITION};
}

*_decodeMappings() {
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();
for (let i = 0; i < mappingsRaw.length; i = next) {
switch (mappingsRaw[i]) {
case ';':
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;
}
}
const mappingRaw = mappingsRaw.slice(i, next);
let decodedVlqValues;
if (vlqCache.has(mappingRaw)) {
decodedVlqValues = vlqCache.get(mappingRaw);
} else {
decodedVlqValues = decodeVlq(mappingRaw);
vlqCache.set(mappingRaw, decodedVlqValues);
}
invariant(Array.isArray(decodedVlqValues), 'Decoding VLQ tuple failed');
const [
generatedColumnDelta,
sourceIndexDelta,
originalLineDelta,
originalColumnDelta,
nameIndexDelta,
] = decodedVlqValues;
decodeVlq(mappingRaw);
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)];
}
}

yield mapping;
}
}

_normalizeAndCacheSources(): $ReadOnlyArray<string> {
if (!this._normalizedSources) {
this._normalizedSources = this._sourceMap.sources.map(source =>
normalizeSourcePath(source, this._sourceMap),
);
}
return this._normalizedSources;
}

_decodeAndCacheMappings(): $ReadOnlyArray<Mapping> {
if (!this._decodedMappings) {
this._decodedMappings = [...this._decodeMappings()];
}
return this._decodedMappings;
}

generatedMappings(): Iterable<Mapping> {
return this._decodeAndCacheMappings();
}
}

module.exports = MappingsConsumer;

0 comments on commit ccd508c

Please sign in to comment.