mirror of https://github.com/status-im/metro.git
Allow HMR client accept wrapped modules from metro
Reviewed By: davidaurelio Differential Revision: D6385273 fbshipit-source-id: 15109332aceb4c0523c668a54b1bf40d8e8fba80
This commit is contained in:
parent
ef9b4b0f0f
commit
c9e0589171
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -31,7 +31,6 @@ export type MainOptions = {|
|
|||
|
||||
export type Options = BundleOptions & {
|
||||
+deltaBundleId: ?string,
|
||||
+wrapModules: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -30,7 +30,6 @@ module.exports = function getBundlingOptionsForHmr(
|
|||
hot: true,
|
||||
minify: false,
|
||||
platform,
|
||||
wrapModules: false,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
Loading…
Reference in New Issue