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 {EventEmitter} = require('events');
describe('HmrServer', () => {
let hmrServer;
let serverMock;
let buildGraphMock;
let deltaBundlerMock;
let deltaTransformerMock;
let getDeltaTransformerMock;
let callbacks;
let mockedGraph;
beforeEach(() => {
deltaTransformerMock = new EventEmitter();
deltaTransformerMock.getDelta = jest.fn().mockReturnValue({id: '1234'});
deltaTransformerMock.getInverseDependencies = jest.fn();
mockedGraph = {
dependencies: new Map(),
entryPoint: 'EntryPoint.js',
};
getDeltaTransformerMock = jest
.fn()
.mockReturnValue(Promise.resolve(deltaTransformerMock));
buildGraphMock = jest.fn().mockReturnValue(mockedGraph);
callbacks = new Map();
deltaBundlerMock = {
getDeltaTransformer: getDeltaTransformerMock,
buildGraph: buildGraphMock,
listen: (graph, cb) => {
callbacks.set(graph, cb);
},
};
serverMock = {
getDeltaBundler() {
@ -46,6 +49,11 @@ describe('HmrServer', () => {
getProjectRoots() {
return ['/root'];
},
_opts: {
createModuleId(path) {
return path + '-id';
},
},
};
hmrServer = new HmrServer(serverMock);
@ -57,29 +65,17 @@ describe('HmrServer', () => {
jest.fn(),
);
expect(getDeltaTransformerMock).toBeCalledWith(
'/hot?bundleEntry=EntryPoint.js&platform=ios',
expect(buildGraphMock).toBeCalledWith(
expect.objectContaining({
deltaBundleId: null,
dev: true,
entryFile: '/root/EntryPoint.js',
entryPoints: ['/root/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();
it('should return the correctly formatted HMR message after a file change', async () => {
const sendMessage = jest.fn();
await hmrServer.onClientConnect(
@ -87,51 +83,51 @@ describe('HmrServer', () => {
sendMessage,
);
deltaTransformerMock.getDelta.mockReturnValue(
deltaBundlerMock.getDelta = jest.fn().mockReturnValue(
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(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({
expect(sendMessage.mock.calls.map(call => JSON.parse(call[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":[]});',
id: '/hi-id',
code: '__d(function() { alert("hi"); },"/hi-id",[],"hi",{});',
},
],
sourceURLs: {},
sourceMappingURLs: {},
},
});
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
},
{
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();
const sendMessage = jest.fn();
@ -140,7 +136,7 @@ describe('HmrServer', () => {
sendMessage,
);
deltaTransformerMock.getDelta.mockImplementation(async () => {
deltaBundlerMock.getDelta = jest.fn().mockImplementation(async () => {
const transformError = new SyntaxError('test syntax error');
transformError.type = 'TransformError';
transformError.filename = 'EntryPoint.js';
@ -148,9 +144,8 @@ describe('HmrServer', () => {
throw transformError;
});
deltaTransformerMock.emit('change');
await callbacks.get(mockedGraph)();
setTimeout(function() {
expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({
type: 'update-start',
});
@ -170,7 +165,5 @@ describe('HmrServer', () => {
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
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';
const addParamsToDefineCall = require('../lib/addParamsToDefineCall');
const formatBundlingError = require('../lib/formatBundlingError');
const getAbsolutePath = require('../lib/getAbsolutePath');
const getBundlingOptionsForHmr = require('./getBundlingOptionsForHmr');
const hmrJSBundle = require('../DeltaBundler/Serializers/hmrJSBundle');
const nullthrows = require('fbjs/lib/nullthrows');
const parseCustomTransformOptions = require('../lib/parseCustomTransformOptions');
const url = require('url');
@ -22,13 +21,12 @@ const {
Logger: {createActionStartEntry, createActionEndEntry, log},
} = require('metro-core');
import type DeltaTransformer from '../DeltaBundler/DeltaTransformer';
import type {Graph} from '../DeltaBundler/DeltaCalculator';
import type PackagerServer from '../Server';
import type {Reporter} from '../lib/reporting';
type Client = {|
clientId: string,
deltaTransformer: DeltaTransformer,
graph: Graph,
sendFn: (data: string) => mixed,
|};
@ -44,7 +42,6 @@ type Client = {|
class HmrServer<TClient: Client> {
_packagerServer: PackagerServer;
_reporter: Reporter;
_lastSequenceId: ?string;
constructor(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
// the same DeltaTransformer between the WS connection and the HTTP one.
const deltaBundler = this._packagerServer.getDeltaBundler();
const deltaTransformer = await deltaBundler.getDeltaTransformer(
clientUrl,
getBundlingOptionsForHmr(
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
platform,
const graph = await deltaBundler.buildGraph({
assetPlugins: [],
customTransformOptions,
),
);
// Trigger an initial build to start up the DeltaTransformer.
const {id} = await deltaTransformer.getDelta();
this._lastSequenceId = id;
dev: true,
entryPoints: [
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
],
hot: true,
minify: false,
onProgress: null,
platform,
type: 'module',
});
// Listen to file changes.
const client = {clientId: clientUrl, deltaTransformer, sendFn};
deltaTransformer.on('change', this._handleFileChange.bind(this, client));
const client = {sendFn, graph};
deltaBundler.listen(graph, this._handleFileChange.bind(this, client));
return client;
}
@ -97,7 +95,7 @@ class HmrServer<TClient: Client> {
onClientDisconnect(client: TClient) {
// We can safely stop the delta transformer since the
// transformer is not shared between clients.
this._packagerServer.getDeltaBundler().endTransformer(client.clientId);
this._packagerServer.getDeltaBundler().endGraph(client.graph);
}
async _handleFileChange(client: Client) {
@ -122,11 +120,17 @@ class HmrServer<TClient: Client> {
});
}
async _prepareResponse(client: Client): Promise<{type: string, body: {}}> {
let result;
async _prepareResponse(
client: Client,
): Promise<{type: string, body: Object}> {
const deltaBundler = this._packagerServer.getDeltaBundler();
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) {
const formattedError = formatBundlingError(error);
@ -134,104 +138,6 @@ class HmrServer<TClient: Client> {
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,
);
}
}
}