From 98518c9fffb3e02195e4d410760184e08689e978 Mon Sep 17 00:00:00 2001 From: Rafael Oleza Date: Wed, 8 Nov 2017 07:03:29 -0800 Subject: [PATCH] Add ramGroups and preloadedModules support when creating RAM bundles Reviewed By: davidaurelio Differential Revision: D6250307 fbshipit-source-id: b7143fa31ded9fcaa28025f05b9d28013361ea0e --- packages/metro-bundler/src/Bundler/index.js | 30 +++++ packages/metro-bundler/src/Bundler/util.js | 2 +- .../src/DeltaBundler/DeltaTransformer.js | 68 +++++++++++ .../src/DeltaBundler/Serializers.js | 106 +++++++++++++---- .../__tests__/Serializers-test.js | 54 +++++++++ .../__snapshots__/Serializers-test.js.snap | 110 ++++++++++++++++++ 6 files changed, 345 insertions(+), 25 deletions(-) diff --git a/packages/metro-bundler/src/Bundler/index.js b/packages/metro-bundler/src/Bundler/index.js index 1bd438f2..cb61ee84 100644 --- a/packages/metro-bundler/src/Bundler/index.js +++ b/packages/metro-bundler/src/Bundler/index.js @@ -876,6 +876,36 @@ class Bundler { return transform || {inlineRequires: false}; } + /** + * Returns the options needed to create a RAM bundle. + */ + async getRamOptions( + entryFile: string, + options: {dev: boolean, platform: ?string}, + getDependencies: string => Promise>, + ): Promise<{| + +preloadedModules: {[string]: true}, + +ramGroups: Array, + |}> { + if (!this._getTransformOptions) { + return { + preloadedModules: {}, + ramGroups: [], + }; + } + + const {preloadedModules, ramGroups} = await this._getTransformOptions( + [entryFile], + {dev: options.dev, hot: true, platform: options.platform}, + getDependencies, + ); + + return { + preloadedModules: preloadedModules || {}, + ramGroups: ramGroups || [], + }; + } + /* * Helper method to return the global transform options that are kept in the * Bundler. diff --git a/packages/metro-bundler/src/Bundler/util.js b/packages/metro-bundler/src/Bundler/util.js index b1039e17..9ea77c33 100644 --- a/packages/metro-bundler/src/Bundler/util.js +++ b/packages/metro-bundler/src/Bundler/util.js @@ -36,7 +36,7 @@ export type PlatformRemoteFileMap = { type SubTree = ( moduleTransport: T, moduleTransportsByPath: Map, -) => Generator; +) => Iterable; const assetPropertyBlacklist = new Set(['files', 'fileSystemLocation', 'path']); diff --git a/packages/metro-bundler/src/DeltaBundler/DeltaTransformer.js b/packages/metro-bundler/src/DeltaBundler/DeltaTransformer.js index f3d55df5..54822342 100644 --- a/packages/metro-bundler/src/DeltaBundler/DeltaTransformer.js +++ b/packages/metro-bundler/src/DeltaBundler/DeltaTransformer.js @@ -153,6 +153,36 @@ class DeltaTransformer extends EventEmitter { return this._deltaCalculator.end(); } + /** + * Returns a function that can be used to calculate synchronously the + * transitive dependencies of any given file within the dependency graph. + **/ + async getDependenciesFn() { + if (!this._deltaCalculator.getDependencyEdges().size) { + // If by any means the dependency graph has not been initialized, call + // getDelta() to initialize it. + await this._getDelta(); + } + + return this._getDependencies; + } + + async getRamOptions( + entryFile: string, + options: {dev: boolean, platform: ?string}, + ): Promise<{| + +preloadedModules: {[string]: true}, + +ramGroups: $ReadOnlyArray, + |}> { + const getDependenciesFn = await this.getDependenciesFn(); + + return await this._bundler.getRamOptions( + entryFile, + options, + async (path: string) => Array.from(getDependenciesFn(path)), + ); + } + /** * Main method to calculate the bundle delta. It returns a DeltaResult, * which contain the source code of the modified and added modules and the @@ -226,6 +256,44 @@ class DeltaTransformer extends EventEmitter { }; } + _getDependencies = (path: string): Set => { + const dependencies = this._getDeps( + path, + this._deltaCalculator.getDependencyEdges(), + new Set(), + ); + + // Remove the main entry point, since this method only returns the + // dependencies. + dependencies.delete(path); + + return dependencies; + }; + + _getDeps( + path: string, + edges: DependencyEdges, + deps: Set, + ): Set { + if (deps.has(path)) { + return deps; + } + + const edge = edges.get(path); + + if (!edge) { + return deps; + } + + deps.add(path); + + for (const [, dependencyPath] of edge.dependencies) { + this._getDeps(dependencyPath, edges, deps); + } + + return deps; + } + async _getPrepend( transformOptions: JSTransformerOptions, dependencyEdges: DependencyEdges, diff --git a/packages/metro-bundler/src/DeltaBundler/Serializers.js b/packages/metro-bundler/src/DeltaBundler/Serializers.js index dca38b4c..b96fd9b9 100644 --- a/packages/metro-bundler/src/DeltaBundler/Serializers.js +++ b/packages/metro-bundler/src/DeltaBundler/Serializers.js @@ -15,13 +15,17 @@ const DeltaPatcher = require('./DeltaPatcher'); const {fromRawMappings} = require('../Bundler/source-map'); +const {createRamBundleGroups} = require('../Bundler/util'); import type {AssetData} from '../AssetServer'; import type {BundleOptions} from '../Server'; import type {MappingsMap} from '../lib/SourceMap'; import type {ModuleTransportLike} from '../shared/types.flow'; import type DeltaBundler, {Options as BuildOptions} from './'; -import type {DeltaEntry, DeltaTransformResponse} from './DeltaTransformer'; +import type DeltaTransformer, { + DeltaEntry, + DeltaTransformResponse, +} from './DeltaTransformer'; export type Options = BundleOptions & { deltaBundleId: ?string, @@ -30,8 +34,8 @@ export type Options = BundleOptions & { export type RamModule = ModuleTransportLike; export type RamBundleInfo = { - startupModules: $ReadOnlyArray, - lazyModules: $ReadOnlyArray, + startupModules: $ReadOnlyArray, + lazyModules: $ReadOnlyArray, groups: Map>, }; @@ -140,33 +144,82 @@ async function getRamBundleInfo( deltaBundler: DeltaBundler, options: Options, ): Promise { - let modules = await getAllModules(deltaBundler, options); + const {id, delta, deltaTransformer} = await _build(deltaBundler, { + ...options, + wrapModules: true, + }); - modules = modules.map(module => { - const map = fromRawMappings([module]).toMap(module.path, { - excludeSource: options.excludeSource, + const modules = DeltaPatcher.get(id) + .applyDelta(delta) + .getAllModules() + .map(module => { + const map = fromRawMappings([module]).toMap(module.path, { + excludeSource: options.excludeSource, + }); + + return { + id: module.id, + code: module.code, + map, + name: module.name, + sourcePath: module.path, + source: module.source, + type: module.type, + }; }); - return { - id: module.id, - code: module.code, - map, - name: module.name, - sourcePath: module.path, - source: module.source, - type: module.type, - }; + const { + preloadedModules, + ramGroups, + } = await deltaTransformer.getRamOptions(options.entryFile, { + dev: options.dev, + platform: options.platform, }); - const startupModules = modules.filter(module => { - return module.type === 'script' || module.type === 'require'; - }); - const lazyModules = modules.filter(module => { - return module.type === 'asset' || module.type === 'module'; + const startupModules = []; + const lazyModules = []; + modules.forEach(module => { + if (preloadedModules.hasOwnProperty(module.sourcePath)) { + startupModules.push(module); + return; + } + + if (module.type === 'script' || module.type === 'require') { + startupModules.push(module); + return; + } + + if (module.type === 'asset' || module.type === 'module') { + lazyModules.push(module); + } }); - // TODO: Implement RAM groups functionality in Delta Bundler. - return {startupModules, lazyModules, groups: new Map()}; + const getDependencies = await deltaTransformer.getDependenciesFn(); + + const groups = createRamBundleGroups( + ramGroups, + lazyModules, + (module: RamModule, dependenciesByPath: Map) => { + const deps = getDependencies(module.sourcePath); + const output = new Set(); + + for (const dependency of deps) { + const module = dependenciesByPath.get(dependency); + + if (module) { + output.add(module.id); + } + } + + return output; + }, + ); + + return { + startupModules, + lazyModules, + groups, + }; } async function getAssets( @@ -192,7 +245,11 @@ async function getAssets( async function _build( deltaBundler: DeltaBundler, options: BuildOptions, -): Promise<{id: string, delta: DeltaTransformResponse}> { +): Promise<{ + id: string, + delta: DeltaTransformResponse, + deltaTransformer: DeltaTransformer, +}> { const {deltaTransformer, id} = await deltaBundler.getDeltaTransformer( options, ); @@ -200,6 +257,7 @@ async function _build( return { id, delta: await deltaTransformer.getDelta(), + deltaTransformer, }; } diff --git a/packages/metro-bundler/src/DeltaBundler/__tests__/Serializers-test.js b/packages/metro-bundler/src/DeltaBundler/__tests__/Serializers-test.js index 3c7df543..cb2edca7 100644 --- a/packages/metro-bundler/src/DeltaBundler/__tests__/Serializers-test.js +++ b/packages/metro-bundler/src/DeltaBundler/__tests__/Serializers-test.js @@ -19,6 +19,8 @@ const CURRENT_TIME = 1482363367000; describe('Serializers', () => { const OriginalDate = global.Date; const getDelta = jest.fn(); + const getDependenciesFn = jest.fn(); + const getRamOptions = jest.fn(); let deltaBundler; const deltaResponse = { @@ -38,6 +40,13 @@ describe('Serializers', () => { beforeEach(() => { getDelta.mockReturnValueOnce(Promise.resolve(deltaResponse)); + getDependenciesFn.mockReturnValue(Promise.resolve(() => new Set())); + getRamOptions.mockReturnValue( + Promise.resolve({ + preloadedModules: {}, + ramGroups: [], + }), + ); deltaBundler = { async getDeltaTransformer() { @@ -45,6 +54,8 @@ describe('Serializers', () => { id: '1234', deltaTransformer: { getDelta, + getDependenciesFn, + getRamOptions, }, }; }, @@ -173,6 +184,49 @@ describe('Serializers', () => { ).toMatchSnapshot(); }); + it('should use the preloadedModules and ramGroup configs to build a RAM bundle', async () => { + getDelta.mockReset(); + getDependenciesFn.mockReset(); + + getDelta.mockReturnValue( + Promise.resolve({ + delta: new Map([ + [3, {type: 'module', code: 'code', id: 3, path: '/foo/3.js'}], + [4, {type: 'module', code: 'code', id: 4, path: '/foo/4.js'}], + [5, {type: 'module', code: 'code', id: 5, path: '/foo/5.js'}], + [6, {type: 'module', code: 'code', id: 6, path: '/foo/6.js'}], + ]), + pre: new Map([ + [7, {type: 'script', code: 'more pre;', id: 7, path: '/foo/7.js'}], + ]), + post: new Map([ + [8, {type: 'require', code: 'bananas;', id: 8, path: '/foo/8.js'}], + ]), + inverseDependencies: [], + reset: 1, + }), + ); + + getRamOptions.mockReturnValue( + Promise.resolve({ + preloadedModules: {'/foo/3.js': true}, + ramGroups: ['/foo/5.js'], + }), + ); + + getDependenciesFn.mockReturnValue( + Promise.resolve(path => { + expect(path).toBe('/foo/5.js'); + + return new Set(['/foo/6.js']); + }), + ); + + expect( + await Serializers.getRamBundleInfo(deltaBundler, {deltaBundleId: 10}), + ).toMatchSnapshot(); + }); + it('should return the bundle assets', async () => { expect( await Serializers.getAllModules(deltaBundler, {deltaBundleId: 10}), diff --git a/packages/metro-bundler/src/DeltaBundler/__tests__/__snapshots__/Serializers-test.js.snap b/packages/metro-bundler/src/DeltaBundler/__tests__/__snapshots__/Serializers-test.js.snap index e27b9c67..b0d6d1e0 100644 --- a/packages/metro-bundler/src/DeltaBundler/__tests__/__snapshots__/Serializers-test.js.snap +++ b/packages/metro-bundler/src/DeltaBundler/__tests__/__snapshots__/Serializers-test.js.snap @@ -371,3 +371,113 @@ Object { "numModifiedFiles": 2, } `; + +exports[`Serializers should use the preloadedModules and ramGroup configs to build a RAM bundle 1`] = ` +Object { + "groups": Map { + 5 => Set { + 6, + }, + }, + "lazyModules": Array [ + Object { + "code": "code", + "id": 4, + "map": Object { + "file": "/foo/4.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/4.js", + "type": "module", + }, + Object { + "code": "code", + "id": 5, + "map": Object { + "file": "/foo/5.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/5.js", + "type": "module", + }, + Object { + "code": "code", + "id": 6, + "map": Object { + "file": "/foo/6.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/6.js", + "type": "module", + }, + ], + "startupModules": Array [ + Object { + "code": "more pre;", + "id": 7, + "map": Object { + "file": "/foo/7.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/7.js", + "type": "script", + }, + Object { + "code": "code", + "id": 3, + "map": Object { + "file": "/foo/3.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/3.js", + "type": "module", + }, + Object { + "code": "bananas;", + "id": 8, + "map": Object { + "file": "/foo/8.js", + "mappings": "", + "names": Array [], + "sources": Array [], + "sourcesContent": Array [], + "version": 3, + }, + "name": undefined, + "source": undefined, + "sourcePath": "/foo/8.js", + "type": "require", + }, + ], +} +`;