Use the new Graph object for generating HMR bundles

Reviewed By: jeanlauliac

Differential Revision: D7275598

fbshipit-source-id: 912a60ebce7ccc291d138c6f1ef8b0fea2d5712b
This commit is contained in:
Rafael Oleza 2018-03-20 06:53:30 -07:00 committed by Facebook Github Bot
parent 395e0494a6
commit 9bae90b2b8
4 changed files with 206 additions and 251 deletions

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* 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 addParamsToDefineCall = require('../../lib/addParamsToDefineCall');
const {wrapModule} = require('./helpers/js');
import type {Delta, Graph} from '../';
import type {DependencyEdge} from '../traverseDependencies';
type Options = {
createModuleId: string => number,
};
export type Result = {
type: string,
body: {
modules: $ReadOnlyArray<{|+id: number, +code: string|}>,
sourceURLs: {},
sourceMappingURLs: {},
},
};
function hmrJSBundle(delta: Delta, graph: Graph, options: Options): Result {
const modules = [];
for (const module of delta.modified.values()) {
modules.push(_prepareModule(module, graph, options));
}
return {
type: 'update',
body: {
modules,
sourceURLs: {},
sourceMappingURLs: {}, // TODO: handle Source Maps
},
};
}
function _prepareModule(
module: DependencyEdge,
graph: Graph,
options: Options,
): {|+id: number, +code: string|} {
const code = wrapModule(module, {
createModuleId: options.createModuleId,
dev: true,
});
const inverseDependencies = _getInverseDependencies(module.path, graph);
// Transform the inverse dependency paths to ids.
const inverseDependenciesById = Object.create(null);
Object.keys(inverseDependencies).forEach(path => {
inverseDependenciesById[options.createModuleId(path)] = inverseDependencies[
path
].map(options.createModuleId);
});
return {
id: options.createModuleId(module.path),
code: addParamsToDefineCall(code, inverseDependenciesById),
};
}
/**
* 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).
*/
function _getInverseDependencies(
path: string,
graph: Graph,
inverseDependencies: {[key: string]: Array<string>} = {},
): {[key: string]: Array<string>} {
// Dependency alredy traversed.
if (path in inverseDependencies) {
return inverseDependencies;
}
const module = graph.dependencies.get(path);
if (!module) {
return inverseDependencies;
}
inverseDependencies[path] = [];
for (const inverse of module.inverseDependencies) {
inverseDependencies[path].push(inverse);
_getInverseDependencies(inverse, graph, inverseDependencies);
}
return inverseDependencies;
}
module.exports = hmrJSBundle;

View File

@ -13,26 +13,29 @@ jest.mock('../../lib/getAbsolutePath');
const HmrServer = require('..'); const HmrServer = require('..');
const {EventEmitter} = require('events');
describe('HmrServer', () => { describe('HmrServer', () => {
let hmrServer; let hmrServer;
let serverMock; let serverMock;
let buildGraphMock;
let deltaBundlerMock; let deltaBundlerMock;
let deltaTransformerMock; let callbacks;
let getDeltaTransformerMock; let mockedGraph;
beforeEach(() => { beforeEach(() => {
deltaTransformerMock = new EventEmitter(); mockedGraph = {
deltaTransformerMock.getDelta = jest.fn().mockReturnValue({id: '1234'}); dependencies: new Map(),
deltaTransformerMock.getInverseDependencies = jest.fn(); entryPoint: 'EntryPoint.js',
};
getDeltaTransformerMock = jest buildGraphMock = jest.fn().mockReturnValue(mockedGraph);
.fn()
.mockReturnValue(Promise.resolve(deltaTransformerMock)); callbacks = new Map();
deltaBundlerMock = { deltaBundlerMock = {
getDeltaTransformer: getDeltaTransformerMock, buildGraph: buildGraphMock,
listen: (graph, cb) => {
callbacks.set(graph, cb);
},
}; };
serverMock = { serverMock = {
getDeltaBundler() { getDeltaBundler() {
@ -46,6 +49,11 @@ describe('HmrServer', () => {
getProjectRoots() { getProjectRoots() {
return ['/root']; return ['/root'];
}, },
_opts: {
createModuleId(path) {
return path + '-id';
},
},
}; };
hmrServer = new HmrServer(serverMock); hmrServer = new HmrServer(serverMock);
@ -57,29 +65,17 @@ describe('HmrServer', () => {
jest.fn(), jest.fn(),
); );
expect(getDeltaTransformerMock).toBeCalledWith( expect(buildGraphMock).toBeCalledWith(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
expect.objectContaining({ expect.objectContaining({
deltaBundleId: null,
dev: true, dev: true,
entryFile: '/root/EntryPoint.js', entryPoints: ['/root/EntryPoint.js'],
minify: false, minify: false,
platform: 'ios', platform: 'ios',
}), }),
); );
}); });
it('should generate an initial delta when a client is connected', async () => { it('should return the correctly formatted HMR message after a file change', 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(); const sendMessage = jest.fn();
await hmrServer.onClientConnect( await hmrServer.onClientConnect(
@ -87,51 +83,51 @@ describe('HmrServer', () => {
sendMessage, sendMessage,
); );
deltaTransformerMock.getDelta.mockReturnValue( deltaBundlerMock.getDelta = jest.fn().mockReturnValue(
Promise.resolve({ Promise.resolve({
delta: new Map([[1, {code: '__d(function() { alert("hi"); });'}]]), modified: new Map([
[
'/hi',
{
dependencies: new Map(),
inverseDependencies: new Set(),
path: '/hi',
output: {
code: '__d(function() { alert("hi"); });',
type: 'module',
},
},
],
]),
}), }),
); );
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'); await callbacks.get(mockedGraph)();
setTimeout(function() { expect(sendMessage.mock.calls.map(call => JSON.parse(call[0]))).toEqual([
expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({ {
type: 'update-start', type: 'update-start',
}); },
expect(JSON.parse(sendMessage.mock.calls[1][0])).toEqual({ {
type: 'update', type: 'update',
body: { body: {
modules: [ modules: [
{ {
id: 1, id: '/hi-id',
code: code: '__d(function() { alert("hi"); },"/hi-id",[],"hi",{});',
'__d(function() { alert("hi"); },{"1":[2,3],"2":[],"3":[4],"4":[]});',
}, },
], ],
sourceURLs: {}, sourceURLs: {},
sourceMappingURLs: {}, sourceMappingURLs: {},
}, },
}); },
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({ {
type: 'update-done', type: 'update-done',
}); },
done(); ]);
}, 30);
}); });
it('should return error messages when there is a transform error', async done => { it('should return error messages when there is a transform error', async () => {
jest.useRealTimers(); jest.useRealTimers();
const sendMessage = jest.fn(); const sendMessage = jest.fn();
@ -140,7 +136,7 @@ describe('HmrServer', () => {
sendMessage, sendMessage,
); );
deltaTransformerMock.getDelta.mockImplementation(async () => { deltaBundlerMock.getDelta = jest.fn().mockImplementation(async () => {
const transformError = new SyntaxError('test syntax error'); const transformError = new SyntaxError('test syntax error');
transformError.type = 'TransformError'; transformError.type = 'TransformError';
transformError.filename = 'EntryPoint.js'; transformError.filename = 'EntryPoint.js';
@ -148,9 +144,8 @@ describe('HmrServer', () => {
throw transformError; throw transformError;
}); });
deltaTransformerMock.emit('change'); await callbacks.get(mockedGraph)();
setTimeout(function() {
expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({ expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({
type: 'update-start', type: 'update-start',
}); });
@ -170,7 +165,5 @@ describe('HmrServer', () => {
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({ expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
type: 'update-done', type: 'update-done',
}); });
done();
}, 30);
}); });
}); });

View File

@ -1,52 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
import type {CustomTransformOptions} from '../JSTransformer/worker';
import type {BundleOptions} from '../shared/types.flow';
/**
* Module to easily create the needed configuration parameters needed for the
* bundler for HMR (since a lot of params are not relevant in this use case).
*/
module.exports = function getBundlingOptionsForHmr(
entryFile: string,
platform: string,
customTransformOptions: CustomTransformOptions,
): BundleOptions {
// These are the really meaningful bundling options. The others below are
// not relevant for HMR.
const mainOptions = {
deltaBundleId: null,
entryFile,
hot: true,
minify: false,
platform,
};
return {
...mainOptions,
assetPlugins: [],
bundleType: 'hmr',
customTransformOptions,
dev: true,
entryModuleOnly: false,
excludeSource: false,
inlineSourceMap: false,
isolateModuleIDs: false,
onProgress: null,
resolutionResponse: null,
runBeforeMainModule: [],
runModule: false,
sourceMapUrl: '',
unbundle: false,
};
};

View File

@ -10,10 +10,9 @@
'use strict'; 'use strict';
const addParamsToDefineCall = require('../lib/addParamsToDefineCall');
const formatBundlingError = require('../lib/formatBundlingError'); const formatBundlingError = require('../lib/formatBundlingError');
const getAbsolutePath = require('../lib/getAbsolutePath'); const getAbsolutePath = require('../lib/getAbsolutePath');
const getBundlingOptionsForHmr = require('./getBundlingOptionsForHmr'); const hmrJSBundle = require('../DeltaBundler/Serializers/hmrJSBundle');
const nullthrows = require('fbjs/lib/nullthrows'); const nullthrows = require('fbjs/lib/nullthrows');
const parseCustomTransformOptions = require('../lib/parseCustomTransformOptions'); const parseCustomTransformOptions = require('../lib/parseCustomTransformOptions');
const url = require('url'); const url = require('url');
@ -22,13 +21,12 @@ const {
Logger: {createActionStartEntry, createActionEndEntry, log}, Logger: {createActionStartEntry, createActionEndEntry, log},
} = require('metro-core'); } = require('metro-core');
import type DeltaTransformer from '../DeltaBundler/DeltaTransformer'; import type {Graph} from '../DeltaBundler/DeltaCalculator';
import type PackagerServer from '../Server'; import type PackagerServer from '../Server';
import type {Reporter} from '../lib/reporting'; import type {Reporter} from '../lib/reporting';
type Client = {| type Client = {|
clientId: string, graph: Graph,
deltaTransformer: DeltaTransformer,
sendFn: (data: string) => mixed, sendFn: (data: string) => mixed,
|}; |};
@ -44,7 +42,6 @@ type Client = {|
class HmrServer<TClient: Client> { class HmrServer<TClient: Client> {
_packagerServer: PackagerServer; _packagerServer: PackagerServer;
_reporter: Reporter; _reporter: Reporter;
_lastSequenceId: ?string;
constructor(packagerServer: PackagerServer) { constructor(packagerServer: PackagerServer) {
this._packagerServer = packagerServer; this._packagerServer = packagerServer;
@ -65,23 +62,24 @@ class HmrServer<TClient: Client> {
// DeltaBundleId param through the WS connection and we'll be able to share // DeltaBundleId param through the WS connection and we'll be able to share
// the same DeltaTransformer between the WS connection and the HTTP one. // the same DeltaTransformer between the WS connection and the HTTP one.
const deltaBundler = this._packagerServer.getDeltaBundler(); const deltaBundler = this._packagerServer.getDeltaBundler();
const deltaTransformer = await deltaBundler.getDeltaTransformer( const graph = await deltaBundler.buildGraph({
clientUrl, assetPlugins: [],
getBundlingOptionsForHmr(
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
platform,
customTransformOptions, customTransformOptions,
), dev: true,
); entryPoints: [
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
// Trigger an initial build to start up the DeltaTransformer. ],
const {id} = await deltaTransformer.getDelta(); hot: true,
minify: false,
this._lastSequenceId = id; onProgress: null,
platform,
type: 'module',
});
// Listen to file changes. // Listen to file changes.
const client = {clientId: clientUrl, deltaTransformer, sendFn}; const client = {sendFn, graph};
deltaTransformer.on('change', this._handleFileChange.bind(this, client));
deltaBundler.listen(graph, this._handleFileChange.bind(this, client));
return client; return client;
} }
@ -97,7 +95,7 @@ class HmrServer<TClient: Client> {
onClientDisconnect(client: TClient) { onClientDisconnect(client: TClient) {
// We can safely stop the delta transformer since the // We can safely stop the delta transformer since the
// transformer is not shared between clients. // transformer is not shared between clients.
this._packagerServer.getDeltaBundler().endTransformer(client.clientId); this._packagerServer.getDeltaBundler().endGraph(client.graph);
} }
async _handleFileChange(client: Client) { async _handleFileChange(client: Client) {
@ -122,11 +120,17 @@ class HmrServer<TClient: Client> {
}); });
} }
async _prepareResponse(client: Client): Promise<{type: string, body: {}}> { async _prepareResponse(
let result; client: Client,
): Promise<{type: string, body: Object}> {
const deltaBundler = this._packagerServer.getDeltaBundler();
try { try {
result = await client.deltaTransformer.getDelta(this._lastSequenceId); const delta = await deltaBundler.getDelta(client.graph, {reset: false});
return hmrJSBundle(delta, client.graph, {
createModuleId: this._packagerServer._opts.createModuleId,
});
} catch (error) { } catch (error) {
const formattedError = formatBundlingError(error); const formattedError = formatBundlingError(error);
@ -134,104 +138,6 @@ class HmrServer<TClient: Client> {
return {type: 'error', body: formattedError}; return {type: 'error', body: formattedError};
} }
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.
if (module != null) {
// When there are new modules added on the dependency graph, the delta
// bundler returns them first, so the HMR logic does not need to worry
// about sorting modules when passing them to the client.
modules.push(this._prepareModule(id, module.code, inverseDependencies));
}
}
this._lastSequenceId = result.id;
return {
type: 'update',
body: {
modules,
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,
);
}
} }
} }