Add Source Maps support to Delta Bundler

Reviewed By: jeanlauliac

Differential Revision: D5793499

fbshipit-source-id: 67e49ed5f5bc9ccae2fb4982cd506fc03259589a
This commit is contained in:
Rafael Oleza 2017-09-11 08:22:28 -07:00 committed by Facebook Github Bot
parent dcf30322a5
commit e57e0002d1
8 changed files with 209 additions and 152 deletions

View File

@ -21,11 +21,11 @@ import type {BundleOptions} from '../Server';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Module from '../node-haste/Module';
export type DeltaResult = {
modified: Map<string, Module>,
deleted: Set<string>,
reset?: boolean,
};
export type DeltaResult = {|
+modified: Map<string, Module>,
+deleted: Set<string>,
+reset: boolean,
|};
/**
* This class is in charge of calculating the delta of changed modules that
@ -199,7 +199,7 @@ class DeltaCalculator extends EventEmitter {
// No changes happened. Return empty delta.
if (modifiedArray.length === 0) {
return {modified: new Map(), deleted: new Set()};
return {modified: new Map(), deleted: new Set(), reset: false};
}
// Build the modules from the files that have been modified.
@ -219,7 +219,7 @@ class DeltaCalculator extends EventEmitter {
// If there is no file with changes in its dependencies, we can just
// return the modified modules without recalculating the dependencies.
if (!filesWithChangedDependencies.some(value => value)) {
return {modified, deleted: new Set()};
return {modified, deleted: new Set(), reset: false};
}
// Recalculate all dependencies and append the newly added files to the
@ -233,6 +233,7 @@ class DeltaCalculator extends EventEmitter {
return {
modified,
deleted,
reset: false,
};
}

View File

@ -12,6 +12,8 @@
'use strict';
const {fromRawMappings} = require('../Bundler/source-map');
import type {DeltaBundle} from './';
/**
@ -21,16 +23,16 @@ import type {DeltaBundle} from './';
*/
class DeltaPatcher {
_lastBundle = {
pre: '',
post: '',
modules: {},
pre: new Map(),
post: new Map(),
modules: new Map(),
};
_initialized = false;
/**
* Applies a Delta Bundle to the current bundle.
*/
applyDelta(deltaBundle: DeltaBundle) {
applyDelta(deltaBundle: DeltaBundle): DeltaPatcher {
// Make sure that the first received delta is a fresh one.
if (!this._initialized && !deltaBundle.reset) {
throw new Error(
@ -43,30 +45,15 @@ class DeltaPatcher {
// Reset the current delta when we receive a fresh delta.
if (deltaBundle.reset) {
this._lastBundle = {
pre: '',
post: '',
modules: {},
pre: new Map(),
post: new Map(),
modules: new Map(),
};
}
// Override the prepended sources.
if (deltaBundle.pre) {
this._lastBundle.pre = deltaBundle.pre;
}
// Override the appended sources.
if (deltaBundle.post) {
this._lastBundle.post = deltaBundle.post;
}
// Patch the received modules.
for (const i in deltaBundle.delta) {
if (deltaBundle.delta[i] == null) {
delete this._lastBundle.modules[i];
} else {
this._lastBundle.modules[i] = deltaBundle.delta[i];
}
}
this._patchMap(this._lastBundle.pre, deltaBundle.pre);
this._patchMap(this._lastBundle.post, deltaBundle.post);
this._patchMap(this._lastBundle.modules, deltaBundle.delta);
return this;
}
@ -75,14 +62,34 @@ class DeltaPatcher {
* Converts the current delta bundle to a standard string bundle, ready to
* be interpreted by any JS VM.
*/
stringify() {
return []
.concat(
this._lastBundle.pre,
Object.values(this._lastBundle.modules),
this._lastBundle.post,
)
.join('\n;');
stringifyCode() {
const code = this._getAllModules().map(m => m.code);
return code.join('\n;');
}
stringifyMap({excludeSource}: {excludeSource?: boolean}) {
const mappings = fromRawMappings(this._getAllModules());
return mappings.toString(undefined, {excludeSource});
}
_getAllModules() {
return [].concat(
Array.from(this._lastBundle.pre.values()),
Array.from(this._lastBundle.modules.values()),
Array.from(this._lastBundle.post.values()),
);
}
_patchMap<K, V>(original: Map<K, V>, patch: Map<K, ?V>) {
for (const [key, value] of patch.entries()) {
if (value == null) {
original.delete(key);
} else {
original.set(key, value);
}
}
}
}

View File

@ -16,6 +16,7 @@ const DeltaCalculator = require('./DeltaCalculator');
const {EventEmitter} = require('events');
import type {RawMapping} from '../Bundler/source-map';
import type Bundler from '../Bundler';
import type {Options as JSTransformerOptions} from '../JSTransformer/worker';
import type Resolver from '../Resolver';
@ -23,12 +24,23 @@ import type {MappingsMap} from '../lib/SourceMap';
import type Module from '../node-haste/Module';
import type {Options as BundleOptions} from './';
export type DeltaTransformResponse = {
+pre: ?string,
+post: ?string,
+delta: {[key: string]: ?string},
type DeltaEntry = {|
+code: string,
+map: ?Array<RawMapping>,
+name: string,
+path: string,
+source: string,
|};
export type DeltaEntries = Map<number, ?DeltaEntry>;
export type DeltaTransformResponse = {|
+pre: DeltaEntries,
+post: DeltaEntries,
+delta: DeltaEntries,
+inverseDependencies: {[key: string]: $ReadOnlyArray<string>},
};
+reset: boolean,
|};
type Options = {|
+getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
@ -37,18 +49,19 @@ type Options = {|
/**
* This class is in charge of creating the delta bundle with the actual
* transformed source code for each of the modified modules.
* transformed source code for each of the modified modules. For each modified
* module it returns a `DeltaModule` object that contains the basic information
* about that file. Modules that have been deleted contain a `null` module
* parameter.
*
* The delta bundle format is the following:
* The actual return format is the following:
*
* {
* pre: '...', // source code to be prepended before all the modules.
* post: '...', // source code to be appended after all the modules
* // (normally here lay the require() call for the starup).
* delta: {
* 27: '...', // transformed source code of a modified module.
* 56: null, // deleted module.
* },
* pre: [{id, module: {}}], Scripts to be prepended before the actual
* modules.
* post: [{id, module: {}}], Scripts to be appended after all the modules
* (normally the initial require() calls).
* delta: [{id, module: {}}], Actual bundle modules (dependencies).
* }
*/
class DeltaTransformer extends EventEmitter {
@ -142,22 +155,21 @@ class DeltaTransformer extends EventEmitter {
// Get the transformed source code of each modified/added module.
const modifiedDelta = await this._transformModules(
modified,
Array.from(modified.values()),
resolver,
transformerOptions,
dependencyPairs,
);
const deletedDelta = Object.create(null);
deleted.forEach(id => {
deletedDelta[this._getModuleId({path: id})] = null;
modifiedDelta.set(this._getModuleId({path: id}), null);
});
// Return the source code that gets prepended to all the modules. This
// contains polyfills and startup code (like the require() implementation).
const prependSources = reset
? await this._getPrepend(transformerOptions, dependencyPairs)
: null;
: new Map();
// Return the source code that gets appended to all the modules. This
// contains the require() calls to startup the execution of the modules.
@ -166,7 +178,7 @@ class DeltaTransformer extends EventEmitter {
dependencyPairs,
this._deltaCalculator.getModulesByName(),
)
: null;
: new Map();
// Inverse dependencies are needed for HMR.
const inverseDependencies = this._getInverseDependencies(
@ -176,7 +188,7 @@ class DeltaTransformer extends EventEmitter {
return {
pre: prependSources,
post: appendSources,
delta: {...modifiedDelta, ...deletedDelta},
delta: modifiedDelta,
inverseDependencies,
reset,
};
@ -185,7 +197,7 @@ class DeltaTransformer extends EventEmitter {
async _getPrepend(
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<string> {
): Promise<DeltaEntries> {
const resolver = await this._bundler.getResolver();
// Get all the polyfills from the relevant option params (the
@ -210,25 +222,18 @@ class DeltaTransformer extends EventEmitter {
),
);
const sources = await Promise.all(
modules.map(async module => {
const result = await this._transformModule(
module,
return await this._transformModules(
modules,
resolver,
transformOptions,
dependencyPairs,
);
return result[1];
}),
);
return sources.join('\n;');
}
async _getAppend(
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
modulesByName: Map<string, Module>,
): Promise<string> {
): Promise<DeltaEntries> {
const resolver = await this._bundler.getResolver();
// Get the absolute path of the entry file, in order to be able to get the
@ -242,14 +247,29 @@ class DeltaTransformer extends EventEmitter {
// First, get the modules correspondant to all the module names defined in
// the `runBeforeMainModule` config variable. Then, append the entry point
// module so the last thing that gets required is the entry point.
const sources = this._bundleOptions.runBeforeMainModule
return new Map(
this._bundleOptions.runBeforeMainModule
.map(name => modulesByName.get(name))
.concat(entryPointModule)
.filter(Boolean)
.map(this._getModuleId)
.map(moduleId => `;require(${JSON.stringify(moduleId)});`);
.map(moduleId => {
const code = `;require(${JSON.stringify(moduleId)});`;
const name = 'require-' + String(moduleId);
const path = name + '.js';
return sources.join('\n');
return [
moduleId,
{
code,
map: null,
name,
source: code,
path,
},
];
}),
);
}
/**
@ -270,13 +290,14 @@ class DeltaTransformer extends EventEmitter {
}
async _transformModules(
modules: Map<string, Module>,
modules: Array<Module>,
resolver: Resolver,
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<{[key: string]: string}> {
const transformedModules = await Promise.all(
Array.from(modules.values()).map(module =>
): Promise<DeltaEntries> {
return new Map(
await Promise.all(
modules.map(module =>
this._transformModule(
module,
resolver,
@ -284,14 +305,8 @@ class DeltaTransformer extends EventEmitter {
dependencyPairs,
),
),
),
);
const output = Object.create(null);
transformedModules.forEach(([id, source]) => {
output[id] = source;
});
return output;
}
async _transformModule(
@ -299,7 +314,7 @@ class DeltaTransformer extends EventEmitter {
resolver: Resolver,
transformOptions: JSTransformerOptions,
dependencyPairs: Map<string, $ReadOnlyArray<[string, Module]>>,
): Promise<[number, string]> {
): Promise<[number, ?DeltaEntry]> {
const [name, metadata] = await Promise.all([
module.getName(),
this._getMetadata(module, transformOptions),
@ -327,9 +342,25 @@ class DeltaTransformer extends EventEmitter {
dependencyPairsForModule,
metadata.dependencyOffsets || [],
),
map: metadata.map,
};
return [this._getModuleId(module), wrapped.code];
// Ignore the Source Maps if the output of the transformer is not our
// custom rawMapping data structure, since the Delta bundler cannot process
// them. This can potentially happen when the minifier is enabled (since
// uglifyJS only returns standard Source Maps).
const map = Array.isArray(wrapped.map) ? wrapped.map : undefined;
return [
this._getModuleId(module),
{
code: wrapped.code,
map,
name,
source: metadata.source,
path: module.path,
},
];
}
async _getMetadata(
@ -338,7 +369,8 @@ class DeltaTransformer extends EventEmitter {
): Promise<{
+code: string,
+dependencyOffsets: ?Array<number>,
+map?: ?MappingsMap,
+map: ?MappingsMap,
+source: string,
}> {
if (module.isAsset()) {
const asset = await this._bundler.generateAssetObjAndCode(
@ -351,6 +383,7 @@ class DeltaTransformer extends EventEmitter {
code: asset.code,
dependencyOffsets: asset.meta.dependencyOffsets,
map: undefined,
source: '',
};
}

View File

@ -137,6 +137,7 @@ describe('DeltaCalculator', () => {
expect(await deltaCalculator.getDelta()).toEqual({
modified: new Map(),
deleted: new Set(),
reset: false,
});
});
@ -150,6 +151,7 @@ describe('DeltaCalculator', () => {
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo]]),
deleted: new Set(),
reset: false,
});
});
@ -167,6 +169,7 @@ describe('DeltaCalculator', () => {
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo]]),
deleted: new Set(['/bar']),
reset: false,
});
});
@ -185,6 +188,7 @@ describe('DeltaCalculator', () => {
expect(result).toEqual({
modified: new Map([['/foo', moduleFoo], ['/qux', moduleQux]]),
deleted: new Set(['/bar', '/baz']),
reset: false,
});
});

View File

@ -24,9 +24,9 @@ describe('DeltaPatcher', () => {
it('should throw if received a non-reset delta as the initial one', () => {
expect(() =>
deltaPatcher.applyDelta({
pre: 'pre',
post: 'post',
delta: {},
pre: new Map(),
post: new Map(),
delta: new Map(),
}),
).toThrow();
});
@ -35,13 +35,11 @@ describe('DeltaPatcher', () => {
const result = deltaPatcher
.applyDelta({
reset: 1,
pre: 'pre',
post: 'post',
delta: {
1: 'middle',
},
pre: new Map([[1, {code: 'pre'}]]),
post: new Map([[2, {code: 'post'}]]),
delta: new Map([[3, {code: 'middle'}]]),
})
.stringify();
.stringifyCode();
expect(result).toMatchSnapshot();
});
@ -50,56 +48,48 @@ describe('DeltaPatcher', () => {
const result = deltaPatcher
.applyDelta({
reset: 1,
pre: 'pre',
post: 'post',
delta: {
1: 'middle',
},
pre: new Map([[1000, {code: 'pre'}]]),
post: new Map([[2000, {code: 'post'}]]),
delta: new Map([[1, {code: 'middle'}]]),
})
.applyDelta({
delta: {
2: 'another',
},
pre: new Map(),
post: new Map(),
delta: new Map([[2, {code: 'another'}]]),
})
.applyDelta({
delta: {
2: 'another',
87: 'third',
},
pre: new Map(),
post: new Map(),
delta: new Map([[2, {code: 'another'}], [87, {code: 'third'}]]),
})
.stringify();
.stringifyCode();
expect(result).toMatchSnapshot();
const anotherResult = deltaPatcher
.applyDelta({
pre: 'new pre',
delta: {
2: 'another',
1: null,
},
pre: new Map([[1000, {code: 'new pre'}]]),
post: new Map(),
delta: new Map([[2, {code: 'another'}], [1, null]]),
})
.applyDelta({
delta: {
2: null,
12: 'twelve',
},
pre: new Map(),
post: new Map(),
delta: new Map([[2, null], [12, {code: 'twelve'}]]),
})
.stringify();
.stringifyCode();
expect(anotherResult).toMatchSnapshot();
expect(
deltaPatcher
.applyDelta({
pre: '1',
post: '1',
delta: {
12: 'ten',
},
pre: new Map([[1000, {code: '1'}]]),
post: new Map([[1000, {code: '1'}]]),
delta: new Map([[12, {code: 'ten'}]]),
reset: true,
})
.stringify(),
.stringifyCode(),
).toMatchSnapshot();
});
});

View File

@ -16,8 +16,8 @@ exports[`DeltaPatcher should apply many different patches correctly 1`] = `
exports[`DeltaPatcher should apply many different patches correctly 2`] = `
"new pre
;twelve
;third
;twelve
;post"
`;

View File

@ -17,14 +17,16 @@ const DeltaTransformer = require('./DeltaTransformer');
import type Bundler from '../Bundler';
import type {BundleOptions} from '../Server';
import type {DeltaEntries} from './DeltaTransformer';
export type DeltaBundle = {
id: string,
pre: ?string,
post: ?string,
delta: {[key: string]: ?string},
inverseDependencies: {[key: string]: $ReadOnlyArray<string>},
};
export type DeltaBundle = {|
+id: string,
+pre: DeltaEntries,
+post: DeltaEntries,
+delta: DeltaEntries,
+inverseDependencies: {[key: string]: $ReadOnlyArray<string>},
+reset: boolean,
|};
type MainOptions = {|
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
@ -59,7 +61,12 @@ class DeltaBundler {
}
async build(options: Options): Promise<DeltaBundle> {
const {deltaTransformer, id} = await this.getDeltaTransformer(options);
const {deltaTransformer, id} = await this.getDeltaTransformer({
...options,
// The Delta Bundler does not support minifying due to issues generating
// the source maps (T21699790).
minify: false,
});
const response = await deltaTransformer.getDelta();
return {
@ -99,6 +106,22 @@ class DeltaBundler {
}
async buildFullBundle(options: FullBuildOptions): Promise<string> {
let output = (await this._getDeltaPatcher(options)).stringifyCode();
if (options.sourceMapUrl) {
output += '//# sourceMappingURL=' + options.sourceMapUrl;
}
return output;
}
async buildFullSourceMap(options: FullBuildOptions): Promise<string> {
return (await this._getDeltaPatcher(options)).stringifyMap({
excludeSource: options.excludeSource,
});
}
async _getDeltaPatcher(options: FullBuildOptions): Promise<DeltaPatcher> {
const deltaBundle = await this.build({
...options,
wrapModules: true,
@ -108,11 +131,10 @@ class DeltaBundler {
if (!deltaPatcher) {
deltaPatcher = new DeltaPatcher();
this._deltaPatchers.set(deltaBundle.id, deltaPatcher);
}
return deltaPatcher.applyDelta(deltaBundle).stringify();
return deltaPatcher.applyDelta(deltaBundle);
}
}

View File

@ -95,11 +95,11 @@ class HmrServer<TClient: Client> {
const result = await client.deltaTransformer.getDelta();
const modules = [];
for (const id in result.delta) {
for (const [id, module] of result.delta) {
// The Delta Bundle can have null objects: these correspond to deleted
// modules, which we don't need to send to the client.
if (result.delta[id] != null) {
modules.push({id, code: result.delta[id]});
if (module != null) {
modules.push({id, code: module.code});
}
}