mirror of https://github.com/status-im/metro.git
Use the new Graph object for generating HMR bundles
Reviewed By: jeanlauliac Differential Revision: D7275598 fbshipit-source-id: 912a60ebce7ccc291d138c6f1ef8b0fea2d5712b
This commit is contained in:
parent
395e0494a6
commit
9bae90b2b8
|
@ -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;
|
|
@ -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,29 +144,26 @@ 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',
|
||||
});
|
||||
const sentErrorMessage = JSON.parse(sendMessage.mock.calls[1][0]);
|
||||
expect(sentErrorMessage).toMatchObject({type: 'error'});
|
||||
expect(sentErrorMessage.body).toMatchObject({
|
||||
type: 'TransformError',
|
||||
message: 'test syntax error',
|
||||
errors: [
|
||||
{
|
||||
description: 'test syntax error',
|
||||
filename: 'EntryPoint.js',
|
||||
lineNumber: 123,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
|
||||
type: 'update-done',
|
||||
});
|
||||
done();
|
||||
}, 30);
|
||||
expect(JSON.parse(sendMessage.mock.calls[0][0])).toEqual({
|
||||
type: 'update-start',
|
||||
});
|
||||
const sentErrorMessage = JSON.parse(sendMessage.mock.calls[1][0]);
|
||||
expect(sentErrorMessage).toMatchObject({type: 'error'});
|
||||
expect(sentErrorMessage.body).toMatchObject({
|
||||
type: 'TransformError',
|
||||
message: 'test syntax error',
|
||||
errors: [
|
||||
{
|
||||
description: 'test syntax error',
|
||||
filename: 'EntryPoint.js',
|
||||
lineNumber: 123,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(JSON.parse(sendMessage.mock.calls[2][0])).toEqual({
|
||||
type: 'update-done',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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(
|
||||
const graph = await deltaBundler.buildGraph({
|
||||
assetPlugins: [],
|
||||
customTransformOptions,
|
||||
dev: true,
|
||||
entryPoints: [
|
||||
getAbsolutePath(bundleEntry, this._packagerServer.getProjectRoots()),
|
||||
platform,
|
||||
customTransformOptions,
|
||||
),
|
||||
);
|
||||
|
||||
// Trigger an initial build to start up the DeltaTransformer.
|
||||
const {id} = await deltaTransformer.getDelta();
|
||||
|
||||
this._lastSequenceId = id;
|
||||
],
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue