diff --git a/packages/metro/src/DeltaBundler/DeltaCalculator.js b/packages/metro/src/DeltaBundler/DeltaCalculator.js index ac8dec5b..0a4668cc 100644 --- a/packages/metro/src/DeltaBundler/DeltaCalculator.js +++ b/packages/metro/src/DeltaBundler/DeltaCalculator.js @@ -12,6 +12,7 @@ const { initialTraverseDependencies, + reorderDependencies, traverseDependencies, } = require('./traverseDependencies'); const {EventEmitter} = require('events'); @@ -85,7 +86,7 @@ class DeltaCalculator extends EventEmitter { * Main method to calculate the delta of modules. It returns a DeltaResult, * which contain the modified/added modules and the removed modules. */ - async getDelta(): Promise { + async getDelta({reset}: {reset: boolean}): Promise { // If there is already a build in progress, wait until it finish to start // processing a new one (delta server doesn't support concurrent builds). if (this._currentBuildPromise) { @@ -134,6 +135,20 @@ class DeltaCalculator extends EventEmitter { this._currentBuildPromise = null; } + // Return all the modules if the client requested a reset delta. + if (reset) { + return { + modified: reorderDependencies( + this._dependencyEdges.get( + this._dependencyGraph.getAbsolutePath(this._options.entryFile), + ), + this._dependencyEdges, + ), + deleted: new Set(), + reset: true, + }; + } + return result; } diff --git a/packages/metro/src/DeltaBundler/DeltaPatcher.js b/packages/metro/src/DeltaBundler/DeltaPatcher.js index 71835551..fe4d3d94 100644 --- a/packages/metro/src/DeltaBundler/DeltaPatcher.js +++ b/packages/metro/src/DeltaBundler/DeltaPatcher.js @@ -27,6 +27,7 @@ class DeltaPatcher { pre: new Map(), post: new Map(), modules: new Map(), + id: undefined, }; _initialized = false; _lastNumModifiedFiles = 0; @@ -62,6 +63,7 @@ class DeltaPatcher { pre: new Map(), post: new Map(), modules: new Map(), + id: undefined, }; } @@ -76,9 +78,15 @@ class DeltaPatcher { this._patchMap(this._lastBundle.post, deltaBundle.post); this._patchMap(this._lastBundle.modules, deltaBundle.delta); + this._lastBundle.id = deltaBundle.id; + return this; } + getLastBundleId(): ?string { + return this._lastBundle.id; + } + /** * Returns the number of modified files in the last received Delta. This is * currently used to populate the `X-Metro-Files-Changed-Count` HTTP header diff --git a/packages/metro/src/DeltaBundler/DeltaTransformer.js b/packages/metro/src/DeltaBundler/DeltaTransformer.js index b3e8aced..cac53aef 100644 --- a/packages/metro/src/DeltaBundler/DeltaTransformer.js +++ b/packages/metro/src/DeltaBundler/DeltaTransformer.js @@ -14,6 +14,7 @@ const DeltaCalculator = require('./DeltaCalculator'); const addParamsToDefineCall = require('../lib/addParamsToDefineCall'); const createModuleIdFactory = require('../lib/createModuleIdFactory'); +const crypto = require('crypto'); const defaults = require('../defaults'); const getPreludeCode = require('../lib/getPreludeCode'); const nullthrows = require('fbjs/lib/nullthrows'); @@ -24,7 +25,8 @@ import type Bundler from '../Bundler'; import type {Options as JSTransformerOptions} from '../JSTransformer/worker'; import type DependencyGraph from '../node-haste/DependencyGraph'; import type Module from '../node-haste/Module'; -import type {Options as BundleOptions, MainOptions} from './'; +import type {BundleOptions} from '../shared/types.flow'; +import type {MainOptions} from './'; import type {DependencyEdge, DependencyEdges} from './traverseDependencies'; import type {MetroSourceMapSegmentTuple} from 'metro-source-map'; @@ -48,6 +50,7 @@ export type DeltaEntry = {| export type DeltaEntries = Map; export type DeltaTransformResponse = {| + +id: string, +pre: DeltaEntries, +post: DeltaEntries, +delta: DeltaEntries, @@ -82,6 +85,7 @@ class DeltaTransformer extends EventEmitter { _deltaCalculator: DeltaCalculator; _bundleOptions: BundleOptions; _currentBuildPromise: ?Promise; + _lastSequenceId: ?string; constructor( bundler: Bundler, @@ -153,7 +157,7 @@ class DeltaTransformer extends EventEmitter { if (!this._deltaCalculator.getDependencyEdges().size) { // If by any means the dependency graph has not been initialized, call // getDelta() to initialize it. - await this._getDelta(); + await this._getDelta({reset: false}); } return this._getDependencies; @@ -167,7 +171,7 @@ class DeltaTransformer extends EventEmitter { if (!this._deltaCalculator.getDependencyEdges().size) { // If by any means the dependency graph has not been initialized, call // getDelta() to initialize it. - await this._getDelta(); + await this._getDelta({reset: false}); } const dependencyEdges = this._deltaCalculator.getDependencyEdges(); @@ -204,7 +208,11 @@ class DeltaTransformer extends EventEmitter { * which contain the source code of the modified and added modules and the * list of removed modules. */ - async getDelta(): Promise { + async getDelta(sequenceId: ?string): Promise { + // If the passed sequenceId is different than the last calculated one, + // return a reset delta (since that means that the client is desynchronized) + const reset = !!this._lastSequenceId && sequenceId !== this._lastSequenceId; + // If there is already a build in progress, wait until it finish to start // processing a new one (delta transformer doesn't support concurrent // builds). @@ -212,7 +220,7 @@ class DeltaTransformer extends EventEmitter { await this._currentBuildPromise; } - this._currentBuildPromise = this._getDelta(); + this._currentBuildPromise = this._getDelta({reset}); let result; @@ -225,58 +233,57 @@ class DeltaTransformer extends EventEmitter { return result; } - async _getDelta(): Promise { + async _getDelta({ + reset: resetDelta, + }: { + reset: boolean, + }): Promise { // Calculate the delta of modules. - const {modified, deleted, reset} = await this._deltaCalculator.getDelta(); + const {modified, deleted, reset} = await this._deltaCalculator.getDelta({ + reset: resetDelta, + }); const transformerOptions = await this._deltaCalculator.getTransformerOptions(); const dependencyEdges = this._deltaCalculator.getDependencyEdges(); - try { - // Return the source code that gets prepended to all the modules. This - // contains polyfills and startup code (like the require() implementation). - const prependSources = reset - ? await this._getPrepend(transformerOptions, dependencyEdges) - : new Map(); + // Return the source code that gets prepended to all the modules. This + // contains polyfills and startup code (like the require() implementation). + const prependSources = reset + ? await this._getPrepend(transformerOptions, dependencyEdges) + : new Map(); - // Precalculate all module ids sequentially. We do this to be sure that the - // mapping between module -> moduleId is deterministic between runs. - const modules = Array.from(modified.values()); - modules.forEach(module => this._getModuleId(module.path)); + // Precalculate all module ids sequentially. We do this to be sure that the + // mapping between module -> moduleId is deterministic between runs. + const modules = Array.from(modified.values()); + modules.forEach(module => this._getModuleId(module.path)); - // Get the transformed source code of each modified/added module. - const modifiedDelta = await this._transformModules( - modules, - transformerOptions, - dependencyEdges, - ); + // Get the transformed source code of each modified/added module. + const modifiedDelta = await this._transformModules( + modules, + transformerOptions, + dependencyEdges, + ); - deleted.forEach(id => { - modifiedDelta.set(this._getModuleId(id), null); - }); + deleted.forEach(id => { + modifiedDelta.set(this._getModuleId(id), null); + }); - // Return the source code that gets appended to all the modules. This - // contains the require() calls to startup the execution of the modules. - const appendSources = reset - ? await this._getAppend(dependencyEdges) - : new Map(); + // Return the source code that gets appended to all the modules. This + // contains the require() calls to startup the execution of the modules. + const appendSources = reset + ? await this._getAppend(dependencyEdges) + : new Map(); - return { - pre: prependSources, - post: appendSources, - delta: modifiedDelta, - reset, - }; - } catch (e) { - // If any unexpected error happens while creating the bundle, the client - // is going to lose that specific delta, while the DeltaCalulator has - // already processed the changes. This will make that change to be lost, - // which can cause the final bundle to be invalid. In order to avoid that, - // we just reset the delta calculator when this happens. - this._deltaCalculator.reset(); + // generate a random + this._lastSequenceId = crypto.randomBytes(8).toString('hex'); - throw e; - } + return { + pre: prependSources, + post: appendSources, + delta: modifiedDelta, + reset, + id: this._lastSequenceId, + }; } _getDependencies = (path: string): Set => { diff --git a/packages/metro/src/DeltaBundler/Serializers.js b/packages/metro/src/DeltaBundler/Serializers.js index d63beda3..d2cfdccf 100644 --- a/packages/metro/src/DeltaBundler/Serializers.js +++ b/packages/metro/src/DeltaBundler/Serializers.js @@ -12,6 +12,7 @@ const DeltaPatcher = require('./DeltaPatcher'); +const stableHash = require('metro-cache/src/stableHash'); const toLocalPath = require('../node-haste/lib/toLocalPath'); const {getAssetData} = require('../Assets'); @@ -19,16 +20,15 @@ const {createRamBundleGroups} = require('../Bundler/util'); const {fromRawMappings} = require('metro-source-map'); import type {AssetData} from '../Assets'; -import type {BundleOptions} from '../shared/types.flow'; -import type {ModuleTransportLike} from '../shared/types.flow'; -import type DeltaBundler, {Options as BuildOptions} from './'; +import type {BundleOptions, ModuleTransportLike} from '../shared/types.flow'; +import type DeltaBundler from './'; import type DeltaTransformer, { DeltaEntry, DeltaTransformResponse, } from './DeltaTransformer'; import type {BabelSourceMap} from '@babel/core'; -export type Options = BundleOptions & { +export type DeltaOptions = BundleOptions & { deltaBundleId: ?string, }; @@ -49,16 +49,17 @@ export type RamBundleInfo = { async function deltaBundle( deltaBundler: DeltaBundler, - options: Options, + clientId: string, + options: DeltaOptions, ): Promise<{bundle: string, numModifiedFiles: number}> { - const {id, delta} = await _build(deltaBundler, options); + const {delta} = await _build(deltaBundler, clientId, options); function stringifyModule([id, module]) { return [id, module ? module.code : undefined]; } const bundle = JSON.stringify({ - id, + id: delta.id, pre: Array.from(delta.pre).map(stringifyModule), post: Array.from(delta.post).map(stringifyModule), delta: Array.from(delta.delta).map(stringifyModule), @@ -73,7 +74,7 @@ async function deltaBundle( async function fullSourceMap( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise { const {modules} = await _getAllModules(deltaBundler, options); @@ -84,7 +85,7 @@ async function fullSourceMap( async function fullSourceMapObject( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise { const {modules} = await _getAllModules(deltaBundler, options); @@ -98,7 +99,7 @@ async function fullSourceMapObject( */ async function fullBundle( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise<{bundle: string, numModifiedFiles: number, lastModified: Date}> { const {modules, numModifiedFiles, lastModified} = await _getAllModules( deltaBundler, @@ -116,7 +117,7 @@ async function fullBundle( async function getAllModules( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise<$ReadOnlyArray> { const {modules} = await _getAllModules(deltaBundler, options); @@ -125,16 +126,30 @@ async function getAllModules( async function _getAllModules( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise<{ modules: $ReadOnlyArray, numModifiedFiles: number, lastModified: Date, deltaTransformer: DeltaTransformer, }> { - const {id, delta, deltaTransformer} = await _build(deltaBundler, options); + const hashedOptions = options; + delete hashedOptions.sourceMapUrl; - const deltaPatcher = DeltaPatcher.get(id); + const clientId = '__SERVER__' + stableHash(hashedOptions).toString('hex'); + + const deltaPatcher = DeltaPatcher.get(clientId); + + options = { + ...options, + deltaBundleId: deltaPatcher.getLastBundleId(), + }; + + const {delta, deltaTransformer} = await _build( + deltaBundler, + clientId, + options, + ); const modules = deltaPatcher .applyDelta(delta) @@ -150,7 +165,7 @@ async function _getAllModules( async function getRamBundleInfo( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise { const {modules, deltaTransformer} = await _getAllModules( deltaBundler, @@ -226,7 +241,7 @@ async function getRamBundleInfo( async function getAssets( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise<$ReadOnlyArray> { const {modules} = await _getAllModules(deltaBundler, options); @@ -254,20 +269,20 @@ async function getAssets( async function _build( deltaBundler: DeltaBundler, - options: BuildOptions, + clientId: string, + options: DeltaOptions, ): Promise<{ - id: string, delta: DeltaTransformResponse, deltaTransformer: DeltaTransformer, }> { - const {deltaTransformer, id} = await deltaBundler.getDeltaTransformer( + const deltaTransformer = await deltaBundler.getDeltaTransformer( + clientId, options, ); - const delta = await deltaTransformer.getDelta(); + const delta = await deltaTransformer.getDelta(options.deltaBundleId); return { - id, delta, deltaTransformer, }; diff --git a/packages/metro/src/DeltaBundler/__tests__/DeltaBundler-test.js b/packages/metro/src/DeltaBundler/__tests__/DeltaBundler-test.js index 791d442e..a766e052 100644 --- a/packages/metro/src/DeltaBundler/__tests__/DeltaBundler-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/DeltaBundler-test.js @@ -43,27 +43,27 @@ describe('DeltaBundler', () => { }); it('should create a new transformer the first time it gets called', async () => { - await deltaBundler.getDeltaTransformer({deltaBundleId: 10}); + await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10}); expect(DeltaTransformer.create.mock.calls.length).toBe(1); }); it('should reuse the same transformer after a second call', async () => { - await deltaBundler.getDeltaTransformer({deltaBundleId: 10}); - await deltaBundler.getDeltaTransformer({deltaBundleId: 10}); + await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10}); + await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 20}); expect(DeltaTransformer.create.mock.calls.length).toBe(1); }); - it('should create different transformers when there is no delta bundle id', async () => { - await deltaBundler.getDeltaTransformer({}); - await deltaBundler.getDeltaTransformer({}); + it('should create different transformers for different clients', async () => { + await deltaBundler.getDeltaTransformer('foo', {}); + await deltaBundler.getDeltaTransformer('bar', {}); expect(DeltaTransformer.create.mock.calls.length).toBe(2); }); it('should reset everything after calling end()', async () => { - await deltaBundler.getDeltaTransformer({deltaBundleId: 10}); + await deltaBundler.getDeltaTransformer('foo', {deltaBundleId: 10}); deltaBundler.end(); diff --git a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js index 72b4175b..f6021471 100644 --- a/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/DeltaCalculator-test.js @@ -13,15 +13,16 @@ jest.mock('../../Bundler'); jest.mock('../traverseDependencies'); +const { + initialTraverseDependencies, + reorderDependencies, + traverseDependencies, +} = require('../traverseDependencies'); + const Bundler = require('../../Bundler'); const {EventEmitter} = require('events'); const DeltaCalculator = require('../DeltaCalculator'); -const { - initialTraverseDependencies, - traverseDependencies, -} = require('../traverseDependencies'); - const getTransformOptions = require('../../__fixtures__/getTransformOptions'); describe('DeltaCalculator', () => { @@ -38,8 +39,7 @@ describe('DeltaCalculator', () => { let deltaCalculator; let fileWatcher; let mockedDependencies; - - const bundlerMock = new Bundler(); + let bundlerMock; const options = { assetPlugins: [], @@ -72,6 +72,8 @@ describe('DeltaCalculator', () => { } beforeEach(async () => { + bundlerMock = new Bundler(); + mockedDependencies = [entryModule, moduleFoo, moduleBar, moduleBaz]; fileWatcher = new EventEmitter(); @@ -90,10 +92,29 @@ describe('DeltaCalculator', () => { initialTraverseDependencies.mockImplementationOnce( async (path, dg, opt, edges) => { - edgeModule = {...entryModule}; - edgeFoo = {...moduleFoo, inverseDependencies: ['/bundle']}; - edgeBar = {...moduleBar, inverseDependencies: ['/bundle']}; - edgeBaz = {...moduleBaz, inverseDependencies: ['/bundle']}; + edgeModule = { + ...entryModule, + dependencies: new Map([ + ['foo', '/foo'], + ['bar', '/bar'], + ['baz', '/baz'], + ]), + }; + edgeFoo = { + ...moduleFoo, + dependencies: new Map(), + inverseDependencies: ['/bundle'], + }; + edgeBar = { + ...moduleBar, + dependencies: new Map(), + inverseDependencies: ['/bundle'], + }; + edgeBaz = { + ...moduleBaz, + dependencies: new Map(), + inverseDependencies: ['/bundle'], + }; edges.set('/bundle', edgeModule); edges.set('/foo', edgeFoo); @@ -131,8 +152,10 @@ describe('DeltaCalculator', () => { }); afterEach(() => { - initialTraverseDependencies.mockReset(); + deltaCalculator.end(); + traverseDependencies.mockReset(); + initialTraverseDependencies.mockReset(); }); it('should start listening for file changes after being initialized', async () => { @@ -146,11 +169,44 @@ describe('DeltaCalculator', () => { }); it('should include the entry file when calculating the initial bundle', async () => { - const result = await deltaCalculator.getDelta(); + const result = await deltaCalculator.getDelta({reset: false}); expect(result).toEqual({ modified: new Map([ - ['/bundle', entryModule], + ['/bundle', edgeModule], + ['/foo', edgeFoo], + ['/bar', edgeBar], + ['/baz', edgeBaz], + ]), + deleted: new Set(), + reset: true, + }); + + jest.runAllTicks(); + }); + + it('should return an empty delta when there are no changes', async () => { + await deltaCalculator.getDelta({reset: false}); + + expect(await deltaCalculator.getDelta({reset: false})).toEqual({ + modified: new Map(), + deleted: new Set(), + reset: false, + }); + + expect(traverseDependencies.mock.calls.length).toBe(0); + }); + + it('should return a full delta when passing reset=true', async () => { + reorderDependencies.mockImplementation((_, edges) => edges); + + await deltaCalculator.getDelta({reset: false}); + + const result = await deltaCalculator.getDelta({reset: true}); + + expect(result).toEqual({ + modified: new Map([ + ['/bundle', edgeModule], ['/foo', edgeFoo], ['/bar', edgeBar], ['/baz', edgeBaz], @@ -160,20 +216,8 @@ describe('DeltaCalculator', () => { }); }); - it('should return an empty delta when there are no changes', async () => { - await deltaCalculator.getDelta(); - - expect(await deltaCalculator.getDelta()).toEqual({ - modified: new Map(), - deleted: new Set(), - reset: false, - }); - - expect(traverseDependencies.mock.calls.length).toBe(0); - }); - it('should calculate a delta after a simple modification', async () => { - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]}); @@ -184,7 +228,7 @@ describe('DeltaCalculator', () => { }), ); - const result = await deltaCalculator.getDelta(); + const result = await deltaCalculator.getDelta({reset: false}); expect(result).toEqual({ modified: new Map([['/foo', edgeFoo]]), @@ -197,7 +241,7 @@ describe('DeltaCalculator', () => { it('should calculate a delta after removing a dependency', async () => { // Get initial delta - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]}); @@ -208,7 +252,7 @@ describe('DeltaCalculator', () => { }), ); - const result = await deltaCalculator.getDelta(); + const result = await deltaCalculator.getDelta({reset: false}); expect(result).toEqual({ modified: new Map([['/foo', edgeFoo]]), @@ -221,7 +265,7 @@ describe('DeltaCalculator', () => { it('should calculate a delta after adding/removing dependencies', async () => { // Get initial delta - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]}); @@ -239,7 +283,7 @@ describe('DeltaCalculator', () => { }; }); - const result = await deltaCalculator.getDelta(); + const result = await deltaCalculator.getDelta({reset: false}); expect(result).toEqual({ modified: new Map([['/foo', edgeFoo], ['/qux', edgeQux]]), deleted: new Set(['/bar', '/baz']), @@ -248,7 +292,7 @@ describe('DeltaCalculator', () => { }); it('should emit an event when there is a relevant file change', async done => { - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); deltaCalculator.on('change', () => done()); @@ -259,7 +303,7 @@ describe('DeltaCalculator', () => { jest.useFakeTimers(); const onChangeFile = jest.fn(); - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); deltaCalculator.on('delete', onChangeFile); @@ -273,20 +317,24 @@ describe('DeltaCalculator', () => { }); it('should retry to build the last delta after getting an error', async () => { - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]}); traverseDependencies.mockReturnValue(Promise.reject(new Error())); - await expect(deltaCalculator.getDelta()).rejects.toBeInstanceOf(Error); + await expect( + deltaCalculator.getDelta({reset: false}), + ).rejects.toBeInstanceOf(Error); // This second time it should still throw an error. - await expect(deltaCalculator.getDelta()).rejects.toBeInstanceOf(Error); + await expect( + deltaCalculator.getDelta({reset: false}), + ).rejects.toBeInstanceOf(Error); }); it('should never try to traverse a file after deleting it', async () => { - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); // First modify the file fileWatcher.emit('change', {eventsQueue: [{filePath: '/foo'}]}); @@ -303,7 +351,7 @@ describe('DeltaCalculator', () => { }), ); - expect(await deltaCalculator.getDelta()).toEqual({ + expect(await deltaCalculator.getDelta({reset: false})).toEqual({ modified: new Map([['/bundle', edgeModule]]), deleted: new Set(['/foo']), reset: false, @@ -314,7 +362,7 @@ describe('DeltaCalculator', () => { }); it('should not do unnecessary work when adding a file after deleting it', async () => { - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); // First delete a file fileWatcher.emit('change', { @@ -331,7 +379,7 @@ describe('DeltaCalculator', () => { }), ); - await deltaCalculator.getDelta(); + await deltaCalculator.getDelta({reset: false}); expect(traverseDependencies).toHaveBeenCalledTimes(1); expect(traverseDependencies.mock.calls[0][0]).toEqual(['/foo']); diff --git a/packages/metro/src/DeltaBundler/__tests__/Serializers-test.js b/packages/metro/src/DeltaBundler/__tests__/Serializers-test.js index febbbcc5..38d31f79 100644 --- a/packages/metro/src/DeltaBundler/__tests__/Serializers-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/Serializers-test.js @@ -28,6 +28,7 @@ describe('Serializers', () => { let Serializers; const deltaResponse = { + id: '1234', pre: new Map([[1, {type: 'script', code: 'pre;', id: 1, path: '/pre.js'}]]), post: new Map([[2, {type: 'require', code: 'post;', id: 2, path: '/p'}]]), delta: new Map([ @@ -58,12 +59,9 @@ describe('Serializers', () => { deltaBundler = { async getDeltaTransformer() { return { - id: '1234', - deltaTransformer: { - getDelta, - getDependenciesFn, - getRamOptions, - }, + getDelta, + getDependenciesFn, + getRamOptions, }; }, getPostProcessModulesFn() { @@ -91,12 +89,13 @@ describe('Serializers', () => { it('should return the stringified delta bundle', async () => { expect( - await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}), + await Serializers.deltaBundle(deltaBundler, 'foo', {deltaBundleId: 10}), ).toMatchSnapshot(); // Simulate a delta with some changes now getDelta.mockReturnValueOnce( Promise.resolve({ + id: '1234', delta: new Map([[3, {code: 'modified module;'}], [4, null]]), pre: new Map(), post: new Map(), @@ -105,17 +104,16 @@ describe('Serializers', () => { ); expect( - await Serializers.deltaBundle(deltaBundler, {deltaBundleId: 10}), + await Serializers.deltaBundle(deltaBundler, 'foo', {deltaBundleId: 10}), ).toMatchSnapshot(); }); it('should build the full JS bundle', async () => { - expect( - await Serializers.fullBundle(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.fullBundle(deltaBundler, {})).toMatchSnapshot(); getDelta.mockReturnValueOnce( Promise.resolve({ + id: '1234', delta: new Map([[3, {code: 'modified module;'}], [4, null]]), pre: new Map([[5, {code: 'more pre;'}]]), post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]), @@ -126,7 +124,6 @@ describe('Serializers', () => { expect( await Serializers.fullBundle(deltaBundler, { - deltaBundleId: 10, sourceMapUrl: 'http://localhost:8081/myBundle.js', }), ).toMatchSnapshot(); @@ -135,12 +132,11 @@ describe('Serializers', () => { // This test actually does not test the sourcemaps generation logic, which // is already tested in the source-map file. it('should build the full Source Maps', async () => { - expect( - await Serializers.fullSourceMap(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot(); getDelta.mockReturnValueOnce( Promise.resolve({ + id: '1234', delta: new Map([[3, {code: 'modified module;'}], [4, null]]), pre: new Map([[5, {code: 'more pre;'}]]), post: new Map([[6, {code: 'bananas;'}], [7, {code: 'apples;'}]]), @@ -149,15 +145,11 @@ describe('Serializers', () => { ); setCurrentTime(CURRENT_TIME + 5000); - expect( - await Serializers.fullSourceMap(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.fullSourceMap(deltaBundler, {})).toMatchSnapshot(); }); it('should return all the bundle modules', async () => { - expect( - await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot(); getDelta.mockReturnValueOnce( Promise.resolve({ @@ -168,14 +160,12 @@ describe('Serializers', () => { }), ); - expect( - await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot(); }); it('should return the RAM bundle info', async () => { expect( - await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}), + await Serializers.getRamBundleInfo(deltaBundler, {}), ).toMatchSnapshot(); getDelta.mockReturnValueOnce( @@ -198,7 +188,7 @@ describe('Serializers', () => { ); expect( - await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}), + await Serializers.getRamBundleInfo(deltaBundler, {}), ).toMatchSnapshot(); }); @@ -241,14 +231,12 @@ describe('Serializers', () => { ); expect( - await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}), + await Serializers.getRamBundleInfo(deltaBundler, {}), ).toMatchSnapshot(); }); it('should return the bundle assets', async () => { - expect( - await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}), - ).toMatchSnapshot(); + expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot(); getDelta.mockReturnValueOnce( Promise.resolve({ @@ -261,12 +249,12 @@ describe('Serializers', () => { pre: new Map([[5, {code: 'more pre;'}]]), post: new Map([[6, {code: 'bananas;'}]]), inverseDependencies: [], + reset: true, }), ); expect( await Serializers.getAssets(deltaBundler, { - deltaBundleId: 10, platform: 'ios', }), ).toMatchSnapshot(); @@ -279,10 +267,6 @@ describe('Serializers', () => { ); }); - expect( - await Serializers.getAllModules(deltaBundler, { - deltaBundleId: 10, - }), - ).toMatchSnapshot(); + expect(await Serializers.getAllModules(deltaBundler, {})).toMatchSnapshot(); }); }); diff --git a/packages/metro/src/DeltaBundler/index.js b/packages/metro/src/DeltaBundler/index.js index 884aa8b6..a08599b0 100644 --- a/packages/metro/src/DeltaBundler/index.js +++ b/packages/metro/src/DeltaBundler/index.js @@ -27,16 +27,11 @@ export type MainOptions = {| postProcessModules?: PostProcessModules, |}; -export type Options = BundleOptions & { - +deltaBundleId: ?string, -}; - /** * `DeltaBundler` uses the `DeltaTransformer` to build bundle deltas. This * module handles all the transformer instances so it can support multiple * concurrent clients requesting their own deltas. This is done through the - * `deltaBundleId` options param (which maps a client to a specific delta - * transformer). + * `clientId` param (which maps a client to a specific delta transformer). */ class DeltaBundler { _bundler: Bundler; @@ -59,18 +54,10 @@ class DeltaBundler { } async getDeltaTransformer( - options: Options, - ): Promise<{deltaTransformer: DeltaTransformer, id: string}> { - let bundleId = options.deltaBundleId; - - // If no bundle id is passed, generate a new one (which is going to be - // returned as part of the bundle, so the client can later ask for an actual - // delta). - if (!bundleId) { - bundleId = String(this._currentId++); - } - - let deltaTransformer = this._deltaTransformers.get(bundleId); + clientId: string, + options: BundleOptions, + ): Promise { + let deltaTransformer = this._deltaTransformers.get(clientId); if (!deltaTransformer) { deltaTransformer = await DeltaTransformer.create( @@ -79,13 +66,10 @@ class DeltaBundler { options, ); - this._deltaTransformers.set(bundleId, deltaTransformer); + this._deltaTransformers.set(clientId, deltaTransformer); } - return { - deltaTransformer, - id: bundleId, - }; + return deltaTransformer; } getPostProcessModulesFn( diff --git a/packages/metro/src/DeltaBundler/traverseDependencies.js b/packages/metro/src/DeltaBundler/traverseDependencies.js index 8daa4d3b..fc5a1022 100644 --- a/packages/metro/src/DeltaBundler/traverseDependencies.js +++ b/packages/metro/src/DeltaBundler/traverseDependencies.js @@ -431,4 +431,5 @@ function flatten(input: Iterable>): Set { module.exports = { initialTraverseDependencies, traverseDependencies, + reorderDependencies, }; diff --git a/packages/metro/src/HmrServer/__tests__/HmrServer-test.js b/packages/metro/src/HmrServer/__tests__/HmrServer-test.js index a3fb4ef4..58aac42e 100644 --- a/packages/metro/src/HmrServer/__tests__/HmrServer-test.js +++ b/packages/metro/src/HmrServer/__tests__/HmrServer-test.js @@ -22,14 +22,12 @@ describe('HmrServer', () => { beforeEach(() => { deltaTransformerMock = new EventEmitter(); - deltaTransformerMock.getDelta = jest.fn(); + deltaTransformerMock.getDelta = jest.fn().mockReturnValue({id: '1234'}); deltaTransformerMock.getInverseDependencies = jest.fn(); getDeltaTransformerMock = jest .fn() - .mockReturnValue( - Promise.resolve({deltaTransformer: deltaTransformerMock}), - ); + .mockReturnValue(Promise.resolve(deltaTransformerMock)); deltaBundlerMock = { getDeltaTransformer: getDeltaTransformerMock, @@ -55,6 +53,7 @@ describe('HmrServer', () => { ); expect(getDeltaTransformerMock).toBeCalledWith( + '/hot?bundleEntry=EntryPoint.js&platform=ios', expect.objectContaining({ deltaBundleId: null, dev: true, diff --git a/packages/metro/src/HmrServer/getBundlingOptionsForHmr.js b/packages/metro/src/HmrServer/getBundlingOptionsForHmr.js index f56e8d5d..7d96a078 100644 --- a/packages/metro/src/HmrServer/getBundlingOptionsForHmr.js +++ b/packages/metro/src/HmrServer/getBundlingOptionsForHmr.js @@ -10,8 +10,8 @@ 'use strict'; -import type {Options as BundleOptions} from '../DeltaBundler'; import type {CustomTransformOptions} from '../JSTransformer/worker'; +import type {BundleOptions} from '../shared/types.flow'; /** * Module to easily create the needed configuration parameters needed for the diff --git a/packages/metro/src/HmrServer/index.js b/packages/metro/src/HmrServer/index.js index 79c9e093..82a74717 100644 --- a/packages/metro/src/HmrServer/index.js +++ b/packages/metro/src/HmrServer/index.js @@ -42,6 +42,7 @@ type Client = {| class HmrServer { _packagerServer: PackagerServer; _reporter: Reporter; + _lastSequenceId: ?string; constructor(packagerServer: PackagerServer) { this._packagerServer = packagerServer; @@ -62,12 +63,15 @@ class HmrServer { // 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( + const deltaTransformer = await deltaBundler.getDeltaTransformer( + clientUrl, getBundlingOptionsForHmr(bundleEntry, platform, customTransformOptions), ); // Trigger an initial build to start up the DeltaTransformer. - await deltaTransformer.getDelta(); + const {id} = await deltaTransformer.getDelta(); + + this._lastSequenceId = id; // Listen to file changes. const client = {sendFn, deltaTransformer}; @@ -113,7 +117,7 @@ class HmrServer { let result; try { - result = await client.deltaTransformer.getDelta(); + result = await client.deltaTransformer.getDelta(this._lastSequenceId); } catch (error) { const formattedError = formatBundlingError(error); @@ -138,6 +142,8 @@ class HmrServer { } } + this._lastSequenceId = result.id; + return { type: 'update', body: { diff --git a/packages/metro/src/Server/__tests__/Server-test.js b/packages/metro/src/Server/__tests__/Server-test.js index ce35b994..44924506 100644 --- a/packages/metro/src/Server/__tests__/Server-test.js +++ b/packages/metro/src/Server/__tests__/Server-test.js @@ -182,7 +182,6 @@ describe('processRequest', () => { assetPlugins: [], bundleType: 'bundle', customTransformOptions: {}, - deltaBundleId: expect.any(String), dev: true, entryFile: 'index.ios.js', entryModuleOnly: false, @@ -214,7 +213,6 @@ describe('processRequest', () => { assetPlugins: [], bundleType: 'bundle', customTransformOptions: {}, - deltaBundleId: expect.any(String), dev: true, entryFile: 'index.js', entryModuleOnly: false, @@ -246,7 +244,6 @@ describe('processRequest', () => { assetPlugins: ['assetPlugin1', 'assetPlugin2'], bundleType: 'bundle', customTransformOptions: {}, - deltaBundleId: expect.any(String), dev: true, entryFile: 'index.js', entryModuleOnly: false, @@ -286,14 +283,17 @@ describe('processRequest', () => { }); it('should send the correct deltaBundlerId to the bundler', () => { - Serializers.deltaBundle.mockImplementation(async (_, options) => { - expect(options.deltaBundleId).toBe('1234'); + Serializers.deltaBundle.mockImplementation( + async (_, clientId, options) => { + expect(clientId).toMatchSnapshot(); + expect(options.deltaBundleId).toBe('1234'); - return { - bundle: '{"delta": "bundle"}', - numModifiedFiles: 3, - }; - }); + return { + bundle: '{"delta": "bundle"}', + numModifiedFiles: 3, + }; + }, + ); return makeRequest( requestHandler, @@ -449,7 +449,6 @@ describe('processRequest', () => { { assetPlugins: [], customTransformOptions: {}, - deltaBundleId: null, dev: true, entryFile: 'foo file', entryModuleOnly: false, diff --git a/packages/metro/src/Server/__tests__/__snapshots__/Server-test.js.snap b/packages/metro/src/Server/__tests__/__snapshots__/Server-test.js.snap new file mode 100644 index 00000000..82f51698 --- /dev/null +++ b/packages/metro/src/Server/__tests__/__snapshots__/Server-test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`processRequest Generate delta bundle endpoint should send the correct deltaBundlerId to the bundler 1`] = `"{\\"sourceMapUrl\\":null,\\"bundleType\\":\\"delta\\",\\"customTransformOptions\\":{},\\"entryFile\\":\\"index.js\\",\\"deltaBundleId\\":null,\\"dev\\":true,\\"minify\\":false,\\"excludeSource\\":null,\\"hot\\":true,\\"runBeforeMainModule\\":[\\"InitializeCore\\"],\\"runModule\\":true,\\"inlineSourceMap\\":false,\\"isolateModuleIDs\\":false,\\"platform\\":\\"ios\\",\\"resolutionResponse\\":null,\\"entryModuleOnly\\":false,\\"assetPlugins\\":[],\\"onProgress\\":null,\\"unbundle\\":false}"`; diff --git a/packages/metro/src/Server/index.js b/packages/metro/src/Server/index.js index 31ced04e..e7357233 100644 --- a/packages/metro/src/Server/index.js +++ b/packages/metro/src/Server/index.js @@ -33,7 +33,7 @@ const resolveSync: ResolveSync = require('resolve').sync; import type {CustomError} from '../lib/formatBundlingError'; import type {IncomingMessage, ServerResponse} from 'http'; import type {Reporter} from '../lib/reporting'; -import type {Options as DeltaBundlerOptions} from '../DeltaBundler/Serializers'; +import type {DeltaOptions} from '../DeltaBundler/Serializers'; import type {BundleOptions, Options} from '../shared/types.flow'; import type { GetTransformOptions, @@ -229,7 +229,6 @@ class Server { async build(options: BundleOptions): Promise<{code: string, map: string}> { options = { ...options, - deltaBundleId: null, runBeforeMainModule: this._opts.getModulesRunBeforeMainModule( options.entryFile, ), @@ -252,16 +251,11 @@ class Server { } async getRamBundleInfo(options: BundleOptions): Promise { - options = {...options, deltaBundleId: null}; - return await Serializers.getRamBundleInfo(this._deltaBundler, options); } async getAssets(options: BundleOptions): Promise<$ReadOnlyArray> { - return await Serializers.getAssets(this._deltaBundler, { - ...options, - deltaBundleId: null, - }); + return await Serializers.getAssets(this._deltaBundler, options); } async getOrderedDependencyPaths(options: { @@ -274,7 +268,6 @@ class Server { ...Server.DEFAULT_BUNDLE_OPTIONS, ...options, bundleType: 'delta', - deltaBundleId: null, }; if (!bundleOptions.platform) { @@ -401,6 +394,7 @@ class Server { // can be ignored to calculate the options hash. const ignoredParams = { onProgress: null, + deltaBundleId: null, excludeSource: null, sourceMapUrl: null, }; @@ -444,7 +438,7 @@ class Server { _prepareDeltaBundler( req: IncomingMessage, mres: MultipartResponse, - ): {options: DeltaBundlerOptions, buildID: string} { + ): {options: DeltaOptions, buildID: string} { const options = this._getOptionsFromUrl( url.format({ ...url.parse(req.url), @@ -503,11 +497,14 @@ class Server { let output; + const clientId = this._optionsHash(options); + try { - output = await Serializers.deltaBundle(this._deltaBundler, { - ...options, - deltaBundleId: options.deltaBundleId, - }); + output = await Serializers.deltaBundle( + this._deltaBundler, + clientId, + options, + ); } catch (error) { this._handleError(mres, this._optionsHash(options), error); @@ -552,10 +549,7 @@ class Server { let result; try { - result = await Serializers.fullBundle(this._deltaBundler, { - ...options, - deltaBundleId: this._optionsHash(options), - }); + result = await Serializers.fullBundle(this._deltaBundler, options); } catch (error) { this._handleError(mres, this._optionsHash(options), error); @@ -619,10 +613,7 @@ class Server { let sourceMap; try { - sourceMap = await Serializers.fullSourceMap(this._deltaBundler, { - ...options, - deltaBundleId: this._optionsHash(options), - }); + sourceMap = await Serializers.fullSourceMap(this._deltaBundler, options); } catch (error) { this._handleError(mres, this._optionsHash(options), error); @@ -751,12 +742,9 @@ class Server { } async _sourceMapForURL(reqUrl: string): Promise { - const options: DeltaBundlerOptions = this._getOptionsFromUrl(reqUrl); + const options: DeltaOptions = this._getOptionsFromUrl(reqUrl); - return await Serializers.fullSourceMapObject(this._deltaBundler, { - ...options, - deltaBundleId: this._optionsHash(options), - }); + return await Serializers.fullSourceMapObject(this._deltaBundler, options); } _handleError(res: ServerResponse, bundleID: string, error: CustomError) { @@ -776,7 +764,7 @@ class Server { }); } - _getOptionsFromUrl(reqUrl: string): BundleOptions & DeltaBundlerOptions { + _getOptionsFromUrl(reqUrl: string): BundleOptions & DeltaOptions { // `true` to parse the query param as an object. const urlObj = nullthrows(url.parse(reqUrl, true)); const urlQuery = nullthrows(urlObj.query); diff --git a/packages/metro/src/lib/getOrderedDependencyPaths.js b/packages/metro/src/lib/getOrderedDependencyPaths.js index d27b8646..74c88780 100644 --- a/packages/metro/src/lib/getOrderedDependencyPaths.js +++ b/packages/metro/src/lib/getOrderedDependencyPaths.js @@ -14,12 +14,12 @@ const Serializers = require('../DeltaBundler/Serializers'); const {getAssetFiles} = require('../Assets'); -import type {Options} from '../DeltaBundler/Serializers'; import type DeltaBundler from '../DeltaBundler'; +import type {BundleOptions} from '../shared/types.flow'; async function getOrderedDependencyPaths( deltaBundler: DeltaBundler, - options: Options, + options: BundleOptions, ): Promise> { const modules = await Serializers.getAllModules(deltaBundler, options);