From f02d6c184867895ebb86ae23ff8a5b75e4256682 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2015 20:17:17 -0800 Subject: [PATCH] Add infra for Prepack build option Summary: This adds a build option for using Prepack (an experimental packager) to build a bundle. It doesn't actually take on the npm package dependency because it's not published/open source (yet). This will be used while we experiment and should be maintained as the build system changes so that we can continue getting fresh builds. I found that saveBundleAndMap and processBundle were over abstracted and got in my way so I inlined it and removed the unit tests because the unit test was testing trivial code that is likely to change interface. I went with a separate build phase and a separate Bundle class even though there are a lot of commonalities. I imagine that the requirements for Prepack will continue to diverge. Especially for source maps but a larger refactor could try to unify these a bit more. The fact that modules are wrapped before the write phase seems to be an unfortunate architecture that makes this difficult. Closes https://github.com/facebook/react-native/pull/4226 Reviewed By: amasad Differential Revision: D2673760 Pulled By: sebmarkbage fb-gh-sync-id: 299ccc42e4be1d9dee19ade443ea3388db2e39a8 --- react-packager/index.js | 9 ++ react-packager/src/Bundler/PrepackBundle.js | 149 ++++++++++++++++++ react-packager/src/Bundler/index.js | 104 +++++++++--- react-packager/src/Server/index.js | 11 ++ .../src/SocketInterface/SocketClient.js | 8 + .../src/SocketInterface/SocketServer.js | 8 + 6 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 react-packager/src/Bundler/PrepackBundle.js diff --git a/react-packager/index.js b/react-packager/index.js index e0a29684..e1a294f4 100644 --- a/react-packager/index.js +++ b/react-packager/index.js @@ -35,6 +35,15 @@ exports.buildBundle = function(options, bundleOptions) { }); }; +exports.buildPrepackBundle = function(options, bundleOptions) { + var server = createNonPersistentServer(options); + return server.buildPrepackBundle(bundleOptions) + .then(function(p) { + server.end(); + return p; + }); +}; + exports.buildPackageFromUrl = exports.buildBundleFromUrl = function(options, reqUrl) { var server = createNonPersistentServer(options); diff --git a/react-packager/src/Bundler/PrepackBundle.js b/react-packager/src/Bundler/PrepackBundle.js new file mode 100644 index 00000000..123d3297 --- /dev/null +++ b/react-packager/src/Bundler/PrepackBundle.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); + +class PrepackBundle { + constructor(sourceMapUrl) { + this._finalized = false; + this._moduleIds = Object.create(null); + this._modules = Object.create(null); + this._eagerModules = []; + this._mainModule = null; + this._assets = []; + this._sourceMapUrl = sourceMapUrl; + } + + addModule(id, module, deps, isPolyfill) { + this._modules[module.sourcePath] = { module, deps }; + this._moduleIds[id] = module.sourcePath; + if (isPolyfill) { + this._eagerModules.push(id); + } + } + + addAsset(asset) { + this._assets.push(asset); + } + + // Synchronously load a file path. + _loadFilename(path) { + const module = this._modules[path]; + if (!module) { + throw new Error('Could not find file "' + path + '" in preloaded files.'); + } + return module.module.code; + } + + // Synchronously resolve a relative require from a parent module. + _resolveFilename(parentPath, relativePath) { + if (!parentPath) { + const resolvedPath = this._moduleIds[relativePath]; + if (!resolvedPath) { + throw new Error('Could not resolve "' + relativePath + '".'); + } + return resolvedPath; + } + const deps = this._modules[parentPath].deps; + const resolvedPath = deps[relativePath]; + if (!resolvedPath) { + throw new Error( + 'Could not resolve "' + relativePath + '" from "' + parentPath + '".' + ); + } + return resolvedPath; + } + + build(options) { + var prepack = require('prepack'); + + var batchedBridgeConfig = (options && options.batchedBridgeConfig) || null; + if (typeof batchedBridgeConfig === 'string') { + batchedBridgeConfig = JSON.parse( + fs.readFileSync(batchedBridgeConfig, 'utf-8') + ); + } + + var options = { + batchedBridgeConfig: batchedBridgeConfig, + environment: 'react-native', + resolveFilename: this._resolveFilename.bind(this), + loadFilename: this._loadFilename.bind(this), + eagerModules: this._eagerModules + }; + + return prepack.compileModule(this._mainModule, options); + } + + finalize(options) { + options = options || {}; + if (options.runMainModule) { + options.runBeforeMainModule.forEach(this._addRequireCall, this); + this._mainModule = options.mainModuleId; + } + + Object.freeze(this._moduleIds); + Object.freeze(this._modules); + Object.freeze(this._assets); + Object.freeze(this._eagerModules); + this._finalized = true; + } + + _addRequireCall(moduleId) { + this._eagerModules.push(moduleId); + } + + _assertFinalized() { + if (!this._finalized) { + throw new Error('Bundle needs to be finalized before getting any source'); + } + } + + getAssets() { + return this._assets; + } + + toJSON() { + if (!this._finalized) { + throw new Error('Cannot serialize bundle unless finalized'); + } + + return { + modules: this._modules, + moduleIds: this._moduleIds, + assets: this._assets, + sourceMapUrl: this._sourceMapUrl, + mainModule: this._mainModule, + eagerModules: this._eagerModules, + }; + } + + static fromJSON(json) { + const bundle = new PrepackBundle(json.sourceMapUrl); + bundle._assets = json.assets; + bundle._moduleIds = json.moduleIds; + bundle._modules = json.modules; + bundle._sourceMapUrl = json.sourceMapUrl; + + bundle._eagerModules = json.eagerModules; + bundle._mainModule = json.mainModule; + + Object.freeze(bundle._moduleIds); + Object.freeze(bundle._modules); + Object.freeze(bundle._assets); + Object.freeze(bundle._eagerModules); + + bundle._finalized = true; + + return bundle; + } +} + +module.exports = PrepackBundle; diff --git a/react-packager/src/Bundler/index.js b/react-packager/src/Bundler/index.js index 82a0282b..5f5d7959 100644 --- a/react-packager/src/Bundler/index.js +++ b/react-packager/src/Bundler/index.js @@ -18,6 +18,7 @@ const Cache = require('../Cache'); const Transformer = require('../JSTransformer'); const Resolver = require('../Resolver'); const Bundle = require('./Bundle'); +const PrepackBundle = require('./PrepackBundle'); const Activity = require('../Activity'); const ModuleTransport = require('../lib/ModuleTransport'); const declareOpts = require('../lib/declareOpts'); @@ -171,7 +172,7 @@ class Bundler { if (bar) { bar.tick(); } - return transformed; + return this._wrapTransformedModule(response, module, transformed); }) ) ); @@ -187,6 +188,68 @@ class Bundler { }); } + prepackBundle({ + entryFile, + runModule: runMainModule, + runBeforeMainModule, + sourceMapUrl, + dev: isDev, + platform, + }) { + const bundle = new PrepackBundle(sourceMapUrl); + const findEventId = Activity.startEvent('find dependencies'); + let transformEventId; + let mainModuleId; + + return this.getDependencies(entryFile, isDev, platform).then((response) => { + Activity.endEvent(findEventId); + transformEventId = Activity.startEvent('transform'); + + let bar; + if (process.stdout.isTTY) { + bar = new ProgressBar('transforming [:bar] :percent :current/:total', { + complete: '=', + incomplete: ' ', + width: 40, + total: response.dependencies.length, + }); + } + + mainModuleId = response.mainModuleId; + + return Promise.all( + response.dependencies.map( + module => this._transformModule( + bundle, + response, + module, + platform + ).then(transformed => { + if (bar) { + bar.tick(); + } + + var deps = Object.create(null); + var pairs = response.getResolvedDependencyPairs(module); + if (pairs) { + pairs.forEach(pair => { + deps[pair[0]] = pair[1].path; + }); + } + + return module.getName().then(name => { + bundle.addModule(name, transformed, deps, module.isPolyfill()); + }); + }) + ) + ); + }).then(() => { + Activity.endEvent(transformEventId); + bundle.finalize({runBeforeMainModule, runMainModule, mainModuleId }); + return bundle; + }); + } + invalidateFile(filePath) { this._transformer.invalidateFile(filePath); } @@ -228,35 +291,32 @@ class Bundler { } _transformModule(bundle, response, module, platform = null) { - let transform; - if (module.isAsset_DEPRECATED()) { - transform = this.generateAssetModule_DEPRECATED(bundle, module); + return this.generateAssetModule_DEPRECATED(bundle, module); } else if (module.isAsset()) { - transform = this.generateAssetModule(bundle, module, platform); + return this.generateAssetModule(bundle, module, platform); } else if (module.isJSON()) { - transform = generateJSONModule(module); + return generateJSONModule(module); } else { - transform = this._transformer.loadFileAndTransform( + return this._transformer.loadFileAndTransform( path.resolve(module.path) ); } + } - const resolver = this._resolver; - return transform.then( - transformed => resolver.wrapModule( - response, - module, - transformed.code - ).then( - code => new ModuleTransport({ - code: code, - map: transformed.map, - sourceCode: transformed.sourceCode, - sourcePath: transformed.sourcePath, - virtual: transformed.virtual, - }) - ) + _wrapTransformedModule(response, module, transformed) { + return this._resolver.wrapModule( + response, + module, + transformed.code + ).then( + code => new ModuleTransport({ + code: code, + map: transformed.map, + sourceCode: transformed.sourceCode, + sourcePath: transformed.sourcePath, + virtual: transformed.virtual, + }) ); } diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index 1abb78cf..28c5b4d1 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -190,6 +190,17 @@ class Server { }); } + buildPrepackBundle(options) { + return Promise.resolve().then(() => { + if (!options.platform) { + options.platform = getPlatformExtension(options.entryFile); + } + + const opts = bundleOpts(options); + return this._bundler.prepackBundle(opts); + }); + } + buildBundleFromUrl(reqUrl) { const options = this._getOptionsFromUrl(reqUrl); return this.buildBundle(options); diff --git a/react-packager/src/SocketInterface/SocketClient.js b/react-packager/src/SocketInterface/SocketClient.js index 41ce0df1..a344f5d9 100644 --- a/react-packager/src/SocketInterface/SocketClient.js +++ b/react-packager/src/SocketInterface/SocketClient.js @@ -9,6 +9,7 @@ 'use strict'; const Bundle = require('../Bundler/Bundle'); +const PrepackBundle = require('../Bundler/PrepackBundle'); const Promise = require('promise'); const bser = require('bser'); const debug = require('debug')('ReactNativePackager:SocketClient'); @@ -100,6 +101,13 @@ class SocketClient { }).then(json => Bundle.fromJSON(json)); } + buildPrepackBundle(options) { + return this._send({ + type: 'buildPrepackBundle', + data: options, + }).then(json => PrepackBundle.fromJSON(json)); + } + _send(message) { message.id = uid(); this._sock.write(bser.dumpToBuffer(message)); diff --git a/react-packager/src/SocketInterface/SocketServer.js b/react-packager/src/SocketInterface/SocketServer.js index 8258873a..56562e27 100644 --- a/react-packager/src/SocketInterface/SocketServer.js +++ b/react-packager/src/SocketInterface/SocketServer.js @@ -121,6 +121,14 @@ class SocketServer { ); break; + case 'buildPrepackBundle': + this._jobs++; + this._packagerServer.buildPrepackBundle(m.data).then( + (result) => this._reply(sock, m.id, 'result', result), + handleError, + ); + break; + case 'getOrderedDependencyPaths': this._jobs++; this._packagerServer.getOrderedDependencyPaths(m.data).then(