diff --git a/local-cli/bundle/buildBundle.js b/local-cli/bundle/buildBundle.js index 60a88411a1da39..b8c90ae18773c8 100644 --- a/local-cli/bundle/buildBundle.js +++ b/local-cli/bundle/buildBundle.js @@ -14,6 +14,7 @@ const TerminalReporter = require('../../packager/react-packager/src/lib/Terminal const outputBundle = require('./output/bundle'); const path = require('path'); +const fs = require('fs'); const saveAssets = require('./saveAssets'); const defaultAssetExts = require('../../packager/defaults').assetExts; @@ -61,6 +62,12 @@ function buildBundle(args, config, output = outputBundle, packagerInstance) { reporter: new TerminalReporter(), }; + if (typeof args.manifestFile === 'string') { + options.manifestReferrence = JSON.parse( + fs.readFileSync(args.manifestFile, 'utf-8') + ); + } + packagerInstance = new Server(options); shouldClosePackager = true; } diff --git a/local-cli/bundle/bundleCommandLineArgs.js b/local-cli/bundle/bundleCommandLineArgs.js index 5186c4fc939e7b..11498604414299 100644 --- a/local-cli/bundle/bundleCommandLineArgs.js +++ b/local-cli/bundle/bundleCommandLineArgs.js @@ -34,6 +34,12 @@ module.exports = [ }, { command: '--sourcemap-output [string]', description: 'File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map', + }, { + command: '--manifest-output [string]', + description: 'File name where to store the manifest file for bundle splitting, ex. ./output/base.manifest.json', + }, { + command: '--manifest-file [path]', + description: 'Path to the manifest file if want to split bundle, ex. ./output/base.manifest.json', }, { command: '--assets-dest [string]', description: 'Directory name where to store assets referenced in the bundle', diff --git a/local-cli/bundle/output/bundle.js b/local-cli/bundle/output/bundle.js index 06217ba30618dd..ad206bf0b56004 100644 --- a/local-cli/bundle/output/bundle.js +++ b/local-cli/bundle/output/bundle.js @@ -40,7 +40,8 @@ function saveBundleAndMap( bundleOutput, bundleEncoding: encoding, dev, - sourcemapOutput + sourcemapOutput, + manifestOutput } = options; log('start'); @@ -58,14 +59,28 @@ function saveBundleAndMap( Promise.all([writeBundle, writeMetadata]) .then(() => log('Done writing bundle output')); + const writeTasks = [writeBundle]; + if (sourcemapOutput) { log('Writing sourcemap output to:', sourcemapOutput); const writeMap = writeFile(sourcemapOutput, codeWithMap.map, null); writeMap.then(() => log('Done writing sourcemap output')); - return Promise.all([writeBundle, writeMetadata, writeMap]); - } else { - return writeBundle; + writeTasks.push(writeMetadata, writeMap); + } + + if (manifestOutput) { + log('Writing manifest output to:', manifestOutput); + const manifest = createBundleManifest(bundle); + const writeManifest = writeFile(manifestOutput, manifest, null); + writeManifest.then(() => log('Done writing manifest output')); + writeTasks.push(writeManifest); } + + return Promise.all(writeTasks); +} + +function createBundleManifest(bundle) { + return JSON.stringify(bundle.getManifest(), null, 2); } exports.build = buildBundle; diff --git a/local-cli/bundle/types.flow.js b/local-cli/bundle/types.flow.js index d0768fef854974..5d25f95f8c3f78 100644 --- a/local-cli/bundle/types.flow.js +++ b/local-cli/bundle/types.flow.js @@ -36,6 +36,7 @@ export type OutputOptions = { dev?: boolean, platform: string, sourcemapOutput?: string, + manifestOutput?: string, }; export type RequestOptions = {| diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js index bf4066960f543c..50ea4840e8663d 100644 --- a/packager/react-packager/src/Bundler/Bundle.js +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -120,6 +120,7 @@ class Bundle extends BundleBase { sourceCode: code, sourcePath: name + '.js', meta: {preloaded: true}, + isRequireCall: true, })); this._numRequireCalls += 1; } @@ -242,6 +243,30 @@ class Bundle extends BundleBase { return eTag; } + getManifest() { + const modules = this.getModules(); + const manifest: { + modules: Object, + lastId: number, + } = { + modules: {}, + lastId:0, + }; + modules.forEach(module => { + // Filter out polyfills and requireCalls + if (module.name && !module.isPolyfill && !module.isRequireCall ) { + manifest.modules[module.name] = { + id: module.id, + }; + } + if (typeof module.id === 'number' && typeof manifest.lastId === 'number') { + manifest.lastId = Math.max(manifest.lastId, module.id); + } + }); + + return manifest; + } + _getSourceMapFile() { return this._sourceMapUrl ? this._sourceMapUrl.replace('.map', '.bundle') diff --git a/packager/react-packager/src/Bundler/__tests__/Bundle-test.js b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js index 48258046c359af..55ec2286fcaedc 100644 --- a/packager/react-packager/src/Bundler/__tests__/Bundle-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -315,6 +315,84 @@ describe('Bundle', () => { }); }); + describe('manifest bundle', () => { + it('exclude polyfills and requireCalls in manifest', () => { + const modules = [ + { + bundle: bundle, + id:0, + code: 'foo', + sourceCode: 'foo', + sourcePath: 'foo path', + name: 'path/foo.js', + }, + { + bundle: bundle, + id:'prefix-1', + code: 'bar', + sourceCode: 'bar', + sourcePath: 'bar path', + name: 'path/bar.js', + }, + { + bundle: bundle, + id:2, + code: 'foobar', + sourceCode: 'foobar', + sourcePath: 'foobar path', + name: 'path/foobar.js', + }, + { + bundle: bundle, + id:3, + code: 'uname module', + sourceCode: 'uname module', + sourcePath: '', + }, + { + bundle: bundle, + id:4, + code: 'polyfill module', + sourceCode: 'polyfill module', + sourcePath: '', + isPolyfill: true, + }, + { + bundle: bundle, + id:5, + code: ';require(0);', + sourceCode: ';require(0);', + sourcePath: '', + isRequireCall: true, + } + ]; + + return Promise.all(modules.map(module => addModule(module))).then(() => { + }).then(() => { + bundle.setMainModuleId(0); + bundle.finalize({ + runBeforeMainModule: [], + runMainModule: true, + }); + const manifest = bundle.getManifest(); + expect(manifest).toEqual({ + modules:{ + 'path/foo.js': { + id: 0, + }, + 'path/bar.js': { + id: 'prefix-1', + }, + 'path/foobar.js': { + id: 2 + } + }, + lastId:5 + }); + }); + }); + }); + describe('main module id:', function() { it('can save a main module ID', function() { const id = 'arbitrary module ID'; @@ -482,7 +560,20 @@ function resolverFor(code, map) { }; } -function addModule({bundle, code, sourceCode, sourcePath, map, virtual, polyfill, meta, id = ''}) { +function addModule({ + bundle, + code, + sourceCode, + sourcePath, + map, + virtual, + polyfill, + meta, + id = '', + name, + isPolyfill, + isRequireCall, +}) { return bundle.addModule( resolverFor(code, map), null, @@ -496,6 +587,9 @@ function addModule({bundle, code, sourceCode, sourcePath, map, virtual, polyfill meta, virtual, polyfill, + name, + isPolyfill, + isRequireCall, }), ); } diff --git a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js index dfb2ddcbc3b3af..24aef9cabf1a21 100644 --- a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -27,6 +27,7 @@ jest .mock('../../lib/declareOpts'); var Bundler = require('../'); +var Bundle = require('../Bundle'); var Resolver = require('../../Resolver'); var sizeOf = require('image-size'); var fs = require('fs'); @@ -35,6 +36,7 @@ describe('Bundler', function() { function createModule({ path, + name, id, dependencies, isAsset, @@ -46,7 +48,7 @@ describe('Bundler', function() { path, resolution, getDependencies: () => Promise.resolve(dependencies), - getName: () => Promise.resolve(id), + getName: () => Promise.resolve(name ? name : id), isJSON: () => isJSON, isAsset: () => isAsset, isPolyfill: () => isPolyfill, @@ -335,4 +337,122 @@ describe('Bundler', function() { ])); }); }); + + + describe('bundle with manifest reference', () => { + var otherBundler; + var doBundle; + var getModuleIdInResolver; + var refModule = {path: '/root/ref.js', name: '/root/ref.js', dependencies: []}; + var moduleFoo = {path: '/root/foo.js', name: '/root/foo.js', dependencies: []}; + var moduleBar = {path: '/root/bar.js', name: '/root/bar.js', dependencies: []}; + var manifestReferrence = { + modules: { + [refModule.name]: { + id: 456 + } + }, + lastId: 10, + }; + + beforeEach(function() { + fs.statSync.mockImplementation(function() { + return { + isDirectory: () => true + }; + }); + + Resolver.mockImplementation(function() { + return { + getDependencies: ( + entryFile, + options, + transformOptions, + onProgress, + getModuleId + ) => { + getModuleIdInResolver = getModuleId; + return Promise.resolve({ + mainModuleId: 0, + dependencies: [ + createModule(refModule), + createModule(moduleFoo), + createModule(moduleBar), + createModule({path: '', name: 'aPolyfill.js', dependencies: [], isPolyfill:true}), + createModule({path: '', name: 'anotherPolyfill.js', dependencies: [], isPolyfill:true}), + ], + transformOptions, + getModuleId, + getResolvedDependencyPairs: () => [], + }) + }, + getModuleSystemDependencies: () => [], + wrapModule: options => Promise.resolve(options), + }; + }); + + Bundle.mockImplementation(function() { + const _modules = []; + + return { + setRamGroups: jest.fn(), + setMainModuleId: jest.fn(), + finalize: jest.fn(), + getModules: () => { + return _modules; + }, + addModule: (resolver, resolutionResponse, module, moduleTransport) => { + return _modules.push({...moduleTransport}) - 1; + } + } + }); + + otherBundler = new Bundler({ + projectRoots: ['/root'], + assetServer: { + getAssetData: jest.fn(), + }, + manifestReferrence, + }); + + doBundle = otherBundler.bundle({ + entryFile: '/root/foo.js', + runBeforeMainModule: [], + runModule: true, + }); + }); + + it('skip dependencies that exist in the manifest reference', function() { + return doBundle.then(bundle => { + const modulesNames = bundle.getModules().map(module => module.name); + + expect(modulesNames.indexOf(refModule.name)).toBe(-1); + expect(modulesNames.indexOf(moduleFoo.name)).not.toBe(-1); + expect(modulesNames.indexOf(moduleBar.name)).not.toBe(-1); + }); + }); + + it('skip polyfills if used manifest reference', function() { + return doBundle.then(bundle => { + expect(bundle.getModules().some(module => module.name == 'somePolyfill.js')).toBeFalsy(); + expect(bundle.getModules().some(module => module.isPolyfill)).toBeFalsy(); + }); + }); + + it('get the moduleId from manifest reference that if module was already exists in the manifest', function() { + const refModuleReference = manifestReferrence.modules[refModule.name]; + return doBundle.then(bundle => { + expect(getModuleIdInResolver(refModule)).toBe(refModuleReference.id); + expect(otherBundler._getModuleId(refModule)).toBe(refModuleReference.id); + }); + }); + + it('create new moduleId should bigger than the lastId in the manifest', function() { + return doBundle.then(bundle => { + bundle.getModules().forEach(module => { + expect(module.id).toBeGreaterThan(manifestReferrence.lastId); + }); + }); + }); + }); }); diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index 396888ef21a1ca..4d70cc5b27e95b 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -113,6 +113,10 @@ const validateOpts = declareOpts({ type: 'boolean', default: false, }, + manifestReferrence: { + type: 'object', + required: false, + }, reporter: { type: 'object', }, @@ -153,6 +157,8 @@ class Bundler { _projectRoots: Array; _assetServer: AssetServer; _getTransformOptions: void | GetTransformOptions<*>; + _startModuleId: number; + _extenalModules: ?Object; constructor(options: Options) { const opts = this._opts = validateOpts(options); @@ -180,7 +186,19 @@ class Bundler { transformModuleHash, ]; - this._getModuleId = createModuleIdFactory(); + const manifest = opts.manifestReferrence; + if (manifest) { + this._startModuleId = 1 + manifest.lastId; + this._extenalModules = manifest.modules; + } else { + this._startModuleId = 0; + this._extenalModules = null; + } + + this._getModuleId = createModuleIdFactory({ + extenalModules: this._extenalModules, + startId: this._startModuleId, + }); if (opts.transformModulePath) { /* $FlowFixMe: dynamic requires prevent static typing :'( */ @@ -340,6 +358,12 @@ class Bundler { response.dependencies = response.dependencies.filter(module => module.path.endsWith(entryFile) ); + } else if (this._extenalModules) { + const extenals = this._extenalModules; + /* If used extenal reference, we don't need polyfills again */ + response.dependencies = response.dependencies.filter(module => + module.name && !module.isPolyfill() && !extenals[module.name] + ); } else { response.dependencies = moduleSystemDeps.concat(response.dependencies); } @@ -424,9 +448,14 @@ class Bundler { minify, isolateModuleIDs, generateSourceMaps: unbundle || generateSourceMaps, - }); + }).then(response => Promise.all(response.dependencies.map(module => + module.getName().then(name => { + module.name = name; + }) + )).then(() => response)); } + return Promise.resolve(resolutionResponse).then(response => { bundle.setRamGroups(response.transformOptions.transform.ramGroups); @@ -564,7 +593,10 @@ class Bundler { {dev, platform, recursive}, transformOptions, onProgress, - isolateModuleIDs ? createModuleIdFactory() : this._getModuleId, + isolateModuleIDs ? createModuleIdFactory({ + extenalModules: this._extenalModules, + startId: this._startModuleId, + }) : this._getModuleId, ); }); } @@ -653,7 +685,8 @@ class Bundler { map, meta: {dependencies, dependencyOffsets, preloaded, dependencyPairs}, sourceCode: source, - sourcePath: module.path + sourcePath: module.path, + isPolyfill: module.isPolyfill() }); }); } @@ -796,15 +829,22 @@ function verifyRootExists(root) { assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); } -function createModuleIdFactory() { +function createModuleIdFactory({extenalModules, startId: nextId = 0}) { const fileToIdMap = Object.create(null); - let nextId = 0; - return ({path: modulePath}) => { - if (!(modulePath in fileToIdMap)) { - fileToIdMap[modulePath] = nextId; + return (module: { + path: string, + name: string + }) => { + if (extenalModules && module.name) { + if (module.name in extenalModules) { + return extenalModules[module.name].id; + } + } + if (!(module.path in fileToIdMap)) { + fileToIdMap[module.path] = nextId; nextId += 1; } - return fileToIdMap[modulePath]; + return fileToIdMap[module.path]; }; } diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 811ad4c484a00a..4f9fd00fc3f3c2 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -108,6 +108,10 @@ const validateOpts = declareOpts({ type: 'boolean', default: false, }, + manifestReferrence: { + type: 'object', + required: false, + }, reporter: { type: 'object', }, diff --git a/packager/react-packager/src/lib/ModuleTransport.js b/packager/react-packager/src/lib/ModuleTransport.js index 1222ac9399cb20..7b29480949a768 100644 --- a/packager/react-packager/src/lib/ModuleTransport.js +++ b/packager/react-packager/src/lib/ModuleTransport.js @@ -29,6 +29,8 @@ class ModuleTransport { meta: ?Metadata; polyfill: ?boolean; map: ?MixedSourceMap; + isPolyfill: ?boolean; + isRequireCall: ?boolean; constructor(data: { name: string, @@ -40,6 +42,8 @@ class ModuleTransport { meta?: ?Metadata, polyfill?: ?boolean, map?: ?MixedSourceMap, + isPolyfill?: ?boolean, + isRequireCall?: ?boolean, }) { this.name = data.name; @@ -59,6 +63,8 @@ class ModuleTransport { this.meta = data.meta; this.polyfill = data.polyfill; this.map = data.map; + this.isPolyfill = data.isPolyfill; + this.isRequireCall = data.isRequireCall; Object.freeze(this); } diff --git a/packager/react-packager/src/node-haste/Module.js b/packager/react-packager/src/node-haste/Module.js index b4479ee07863f6..94c4f87692710a 100644 --- a/packager/react-packager/src/node-haste/Module.js +++ b/packager/react-packager/src/node-haste/Module.js @@ -65,6 +65,7 @@ class Module { path: string; type: string; + name: string; _moduleCache: ModuleCache; _cache: Cache;