Allow HMR client accept wrapped modules from metro

Reviewed By: davidaurelio

Differential Revision: D6385273

fbshipit-source-id: 15109332aceb4c0523c668a54b1bf40d8e8fba80
This commit is contained in:
Rafael Oleza 2017-11-22 11:24:25 -08:00 committed by Facebook Github Bot
parent ef9b4b0f0f
commit c9e0589171
10 changed files with 340 additions and 69 deletions

View File

@ -161,10 +161,7 @@ class DeltaTransformer extends EventEmitter {
* Returns a function that can be used to calculate synchronously the
* transitive dependencies of any given file within the dependency graph.
**/
async getInverseDependencies(): Promise<{
[key: string]: $ReadOnlyArray<string>,
__proto__: null,
}> {
async getInverseDependencies(): Promise<Map<number, $ReadOnlyArray<number>>> {
if (!this._deltaCalculator.getDependencyEdges().size) {
// If by any means the dependency graph has not been initialized, call
// getDelta() to initialize it.
@ -172,12 +169,13 @@ class DeltaTransformer extends EventEmitter {
}
const dependencyEdges = this._deltaCalculator.getDependencyEdges();
const output = Object.create(null);
const output = new Map();
for (const [path, {inverseDependencies}] of dependencyEdges.entries()) {
output[this._getModuleId(path)] = Array.from(
inverseDependencies,
).map(dep => this._getModuleId(dep));
output.set(
this._getModuleId(path),
Array.from(inverseDependencies).map(dep => this._getModuleId(dep)),
);
}
return output;
@ -419,28 +417,16 @@ class DeltaTransformer extends EventEmitter {
const edge = dependencyEdges.get(module.path);
const dependencyPairs = edge ? edge.dependencies : new Map();
const wrapped = this._bundleOptions.wrapModules
? this._resolver.wrapModule({
module,
getModuleId: this._getModuleId,
dependencyPairs,
dependencyOffsets: metadata.dependencyOffsets || [],
name,
code: metadata.code,
map: metadata.map,
minify: this._bundleOptions.minify,
dev: this._bundleOptions.dev,
})
: {
code: this._resolver.resolveRequires(
module,
this._getModuleId,
metadata.code,
dependencyPairs,
metadata.dependencyOffsets || [],
),
map: metadata.map,
};
const wrapped = this._resolver.wrapModule({
module,
getModuleId: this._getModuleId,
dependencyPairs,
dependencyOffsets: metadata.dependencyOffsets || [],
name,
code: metadata.code,
map: metadata.map,
dev: this._bundleOptions.dev,
});
const {code, map} = transformOptions.minify
? await this._resolver.minifyModule(

View File

@ -49,10 +49,7 @@ async function deltaBundle(
deltaBundler: DeltaBundler,
options: Options,
): Promise<{bundle: string, numModifiedFiles: number}> {
const {id, delta} = await _build(deltaBundler, {
...options,
wrapModules: true,
});
const {id, delta} = await _build(deltaBundler, options);
function stringifyModule([id, module]) {
return [id, module ? module.code : undefined];
@ -76,10 +73,7 @@ async function fullSourceMap(
deltaBundler: DeltaBundler,
options: Options,
): Promise<string> {
const {modules} = await _getAllModules(deltaBundler, {
...options,
wrapModules: true,
});
const {modules} = await _getAllModules(deltaBundler, options);
return fromRawMappings(modules).toString(undefined, {
excludeSource: options.excludeSource,
@ -90,10 +84,7 @@ async function fullSourceMapObject(
deltaBundler: DeltaBundler,
options: Options,
): Promise<MappingsMap> {
const {modules} = await _getAllModules(deltaBundler, {
...options,
wrapModules: true,
});
const {modules} = await _getAllModules(deltaBundler, options);
return fromRawMappings(modules).toMap(undefined, {
excludeSource: options.excludeSource,
@ -125,10 +116,7 @@ async function getAllModules(
deltaBundler: DeltaBundler,
options: Options,
): Promise<$ReadOnlyArray<DeltaEntry>> {
const {modules} = await _getAllModules(deltaBundler, {
...options,
wrapModules: true,
});
const {modules} = await _getAllModules(deltaBundler, options);
return modules;
}
@ -142,10 +130,7 @@ async function _getAllModules(
lastModified: Date,
deltaTransformer: DeltaTransformer,
}> {
const {id, delta, deltaTransformer} = await _build(deltaBundler, {
...options,
wrapModules: true,
});
const {id, delta, deltaTransformer} = await _build(deltaBundler, options);
const deltaPatcher = DeltaPatcher.get(id);
@ -165,10 +150,10 @@ async function getRamBundleInfo(
deltaBundler: DeltaBundler,
options: Options,
): Promise<RamBundleInfo> {
const {modules, deltaTransformer} = await _getAllModules(deltaBundler, {
...options,
wrapModules: true,
});
const {modules, deltaTransformer} = await _getAllModules(
deltaBundler,
options,
);
const ramModules = modules.map(module => ({
id: module.id,

View File

@ -31,7 +31,6 @@ export type MainOptions = {|
export type Options = BundleOptions & {
+deltaBundleId: ?string,
+wrapModules: boolean,
};
/**

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @format
* @emails oncall+js_foundation
*/
'use strict';
const HmrServer = require('..');
const {EventEmitter} = require('events');
describe('HmrServer', () => {
let hmrServer;
let reporterMock;
let serverMock;
let deltaBundlerMock;
let deltaTransformerMock;
let getDeltaTransformerMock;
beforeEach(() => {
deltaTransformerMock = new EventEmitter();
deltaTransformerMock.getDelta = jest.fn();
deltaTransformerMock.getInverseDependencies = jest.fn();
getDeltaTransformerMock = jest
.fn()
.mockReturnValue(
Promise.resolve({deltaTransformer: deltaTransformerMock}),
);
deltaBundlerMock = {
getDeltaTransformer: getDeltaTransformerMock,
};
serverMock = {
getDeltaBundler() {
return deltaBundlerMock;
},
};
getDeltaTransformerMock.reporterMock = {
update: jest.fn(),
};
hmrServer = new HmrServer(serverMock, reporterMock);
});
it('should pass the correct options to the delta bundler', async () => {
await hmrServer.onClientConnect(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
jest.fn(),
);
expect(getDeltaTransformerMock).toBeCalledWith(
expect.objectContaining({
deltaBundleId: null,
dev: true,
entryFile: 'EntryPoint.js',
minify: false,
platform: 'ios',
}),
);
});
it('should generate an initial delta when a client is connected', async () => {
await hmrServer.onClientConnect(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
jest.fn(),
);
expect(deltaTransformerMock.getDelta).toBeCalled();
});
it('should return the correctly formatted HMR message after a file change', async done => {
jest.useRealTimers();
const sendMessage = jest.fn();
await hmrServer.onClientConnect(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
sendMessage,
);
deltaTransformerMock.getDelta.mockReturnValue(
Promise.resolve({
delta: new Map([[1, {code: '__d(function() { alert("hi"); });'}]]),
}),
);
deltaTransformerMock.getInverseDependencies.mockReturnValue(
Promise.resolve(
new Map([
[1, [2, 3]],
[2, []],
[3, [4]],
[4, []],
[5, [1, 2, 3]], // this shouldn't be added to the response
]),
),
);
deltaTransformerMock.emit('change');
setTimeout(function() {
expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({
type: 'update-start',
});
expect(JSON.parse(sendMessage.mock.calls[1][0])).toEqual({
type: 'update',
body: {
modules: [
{
id: 1,
code:
'__d(function() { alert("hi"); },{"1":[2,3],"2":[],"3":[4],"4":[]});',
},
],
sourceURLs: {},
sourceMappingURLs: {},
},
});
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
type: 'update-done',
});
done();
}, 30);
});
});

View File

@ -30,7 +30,6 @@ module.exports = function getBundlingOptionsForHmr(
hot: true,
minify: false,
platform,
wrapModules: false,
};
return {

View File

@ -12,6 +12,7 @@
'use strict';
const addParamsToDefineCall = require('../lib/addParamsToDefineCall');
const formatBundlingError = require('../lib/formatBundlingError');
const getBundlingOptionsForHmr = require('./getBundlingOptionsForHmr');
const querystring = require('querystring');
@ -125,6 +126,8 @@ class HmrServer<TClient: Client> {
}
const modules = [];
const inverseDependencies = await client.deltaTransformer.getInverseDependencies();
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.
@ -132,7 +135,9 @@ class HmrServer<TClient: Client> {
// When there are new modules added on the dependency tree, they are
// appended on the Delta Bundle, but HMR needs to have them at the
// beginning.
modules.unshift({id, code: module.code});
modules.unshift(
this._prepareModule(id, module.code, inverseDependencies),
);
}
}
@ -140,12 +145,84 @@ class HmrServer<TClient: Client> {
type: 'update',
body: {
modules,
inverseDependencies: await client.deltaTransformer.getInverseDependencies(),
sourceURLs: {},
sourceMappingURLs: {}, // TODO: handle Source Maps
},
};
}
/**
* We need to add the inverse dependencies of that specific module into
* the define() call, to make the HMR logic in the client able to propagate
* the changes to the module dependants, if needed.
*
* To do so, we need to append the inverse dependencies object as the last
* parameter to the __d() call from the code that we get from the bundler.
*
* So, we need to transform this:
*
* __d(
* function(global, ...) { (module transformed code) },
* moduleId,
* dependencyMap?,
* moduleName?
* );
*
* Into this:
*
* __d(
* function(global, ...) { (module transformed code) },
* moduleId,
* dependencyMap?,
* moduleName?,
* inverseDependencies,
* );
*/
_prepareModule(
id: number,
code: string,
inverseDependencies: Map<number, $ReadOnlyArray<number>>,
): {id: number, code: string} {
const moduleInverseDependencies = Object.create(null);
this._addInverseDep(id, inverseDependencies, moduleInverseDependencies);
return {
id,
code: addParamsToDefineCall(code, moduleInverseDependencies),
};
}
/**
* Instead of adding the whole inverseDependncies object into each changed
* module (which can be really huge if the dependency graph is big), we only
* add the needed inverseDependencies for each changed module (we do this by
* traversing upwards the dependency graph).
*/
_addInverseDep(
module: number,
inverseDependencies: Map<number, $ReadOnlyArray<number>>,
moduleInverseDependencies: {
[key: number]: Array<number>,
__proto__: null,
},
) {
if (module in moduleInverseDependencies) {
return;
}
moduleInverseDependencies[module] = [];
for (const inverse of inverseDependencies.get(module) || []) {
moduleInverseDependencies[module].push(inverse);
this._addInverseDep(
inverse,
inverseDependencies,
moduleInverseDependencies,
);
}
}
}
module.exports = HmrServer;

View File

@ -12,6 +12,7 @@
'use strict';
const addParamsToDefineCall = require('../../lib/addParamsToDefineCall');
const virtualModule = require('../module').virtual;
import type {IdsForPathFn, Module} from '../types.flow';
@ -29,19 +30,19 @@ function addModuleIdsToModuleWrapper(
): string {
const {dependencies, file} = module;
const {code} = file;
const index = code.lastIndexOf(')');
// calling `idForPath` on the module itself first gives us a lower module id
// for the file itself than for its dependencies. That reflects their order
// in the bundle.
const fileId = idForPath(file);
// This code runs for both development and production builds, after
// minification. That's why we leave out all spaces.
const depencyIds = dependencies.length
? `,[${dependencies.map(idForPath).join(',')}]`
: '';
return code.slice(0, index) + `,${fileId}` + depencyIds + code.slice(index);
const paramsToAdd = [fileId];
if (dependencies.length) {
paramsToAdd.push(dependencies.map(idForPath));
}
return addParamsToDefineCall(code, ...paramsToAdd);
}
exports.addModuleIdsToModuleWrapper = addModuleIdsToModuleWrapper;

View File

@ -67,6 +67,16 @@ function define(
dependencyMap?: DependencyMap,
) {
if (moduleId in modules) {
if (__DEV__) {
// (We take `inverseDependencies` from `arguments` to avoid an unused
// named parameter in `define` in production.
const inverseDependencies = arguments[4];
// If the module has already been defined and we're in dev mode,
// hot reload it.
global.__accept(moduleId, factory, dependencyMap, inverseDependencies);
}
// prevent repeated calls to `global.nativeRequire` to overwrite modules
// that are already loaded
return;
@ -256,7 +266,13 @@ if (__DEV__) {
}
const notAccepted = dependentModules.filter(
module => !accept(module, /*factory*/ undefined, inverseDependencies),
module =>
!accept(
module,
/*factory*/ undefined,
/*dependencyMap*/ undefined,
inverseDependencies,
),
);
const parents = [];
@ -275,14 +291,14 @@ if (__DEV__) {
const accept = function(
id: ModuleID,
factory?: FactoryFn,
dependencyMap?: DependencyMap,
inverseDependencies: {[key: ModuleID]: Array<ModuleID>},
) {
const mod = modules[id];
if (!mod && factory) {
// new modules need a factory
define(factory, id);
return true; // new modules don't need to be accepted
// New modules are going to be handled by the define() method.
return true;
}
const {hot} = mod;
@ -298,6 +314,9 @@ if (__DEV__) {
if (factory) {
mod.factory = factory;
}
if (dependencyMap) {
mod.dependencyMap = dependencyMap;
}
mod.hasError = false;
mod.isInitialized = false;
require(id);

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @format
* @emails oncall+js_foundation
*/
'use strict';
const addParamsToDefineCall = require('../addParamsToDefineCall');
describe('addParamsToDefineCall', () => {
const input = '__d(function() {}); // SourceMapUrl=something';
it('adds a simple parameter', () => {
expect(addParamsToDefineCall(input, 10)).toEqual(
'__d(function() {},10); // SourceMapUrl=something',
);
});
it('adds several parameters', () => {
expect(addParamsToDefineCall(input, 10, {foo: 'bar'})).toEqual(
'__d(function() {},10,{"foo":"bar"}); // SourceMapUrl=something',
);
});
it('adds null parameters', () => {
expect(addParamsToDefineCall(input, null, 10)).toEqual(
'__d(function() {},null,10); // SourceMapUrl=something',
);
});
it('adds undefined parameters', () => {
expect(addParamsToDefineCall(input, null, 10)).toEqual(
'__d(function() {},null,10); // SourceMapUrl=something',
);
});
});

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
'use strict';
/**
* Simple way of adding additional parameters to the end of the define calls.
*
* This is used to add extra information to the generaic compiled modules (like
* the dependencyMap object or the list of inverse dependencies).
*/
function addParamsToDefineCall(
code: string,
...paramsToAdd: Array<mixed>
): string {
const index = code.lastIndexOf(')');
const params = paramsToAdd.map(
param => (param !== undefined ? JSON.stringify(param) : 'undefined'),
);
return code.slice(0, index) + ',' + params.join(',') + code.slice(index);
}
module.exports = addParamsToDefineCall;