From f2438b440d410ca1aa3ca7d0544d6e000e29e774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Wed, 27 Jan 2016 14:55:02 -0800 Subject: [PATCH] Hot Loading Sourcemaps Summary: public To make sourcemaps work on Hot Loading work, we'll need to be able to serve them for each module that is dynamically replaced. To do so we introduced a new parameter to the bundler, namely `entryModuleOnly` to decide whether or not to process the full dependency tree or just the module associated to the entry file. Also we need to add `//sourceMappingURL` to the HMR updates so that in case of an error the runtime retrieves the sourcemaps for the file on which an error occurred from the server. Finally, we need to refactor a bit how we load the HMR updates into JSC. Unfortunately, if the code is eval'ed when an error is thrown, the line and column number are missing. This is a bug/missing feature in JSC. To walkaround the issue we need to eval the code on native. This adds a bit of complexity to HMR as for both platforms we'll have to have a thin module to inject code but I don't see any other alternative. when debugging this is not needed as Chrome supports sourceMappingURLs on eval'ed code Reviewed By: javache Differential Revision: D2841788 fb-gh-sync-id: ad9370d26894527a151cea722463e694c670227e --- .../Initialization/ExceptionsManager.js | 42 +++++----- .../Initialization/SourceMapsCache.js | 44 +++++++++++ .../Initialization/SourceMapsUtils.js | 76 ++++++++++++++++++ .../Initialization/loadSourceMap.js | 66 ---------------- .../Initialization/parseErrorStack.js | 21 ++++- .../Initialization/source-map-url.js | 2 +- Libraries/Utilities/HMRClient.js | 28 ++++++- React/Executors/RCTJSCExecutor.m | 18 +++++ React/Modules/RCTRedBox.m | 2 +- local-cli/server/util/attachHMRServer.js | 19 ++--- packager/react-packager/src/Bundler/Bundle.js | 10 ++- .../react-packager/src/Bundler/BundleBase.js | 4 + .../react-packager/src/Bundler/HMRBundle.js | 31 +++++--- .../src/Bundler/__tests__/Bundle-test.js | 4 +- packager/react-packager/src/Bundler/index.js | 79 ++++++++++++++++--- .../src/Server/__tests__/Server-test.js | 4 + packager/react-packager/src/Server/index.js | 9 +++ 17 files changed, 331 insertions(+), 128 deletions(-) create mode 100644 Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js create mode 100644 Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js delete mode 100644 Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index f7537669f..a75b166e2 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -11,37 +11,37 @@ */ 'use strict'; -var sourceMapPromise; - -var exceptionID = 0; +let exceptionID = 0; /** * Handles the developer-visible aspect of errors and exceptions */ function reportException(e: Error, isFatal: bool) { - var loadSourceMap = require('loadSourceMap'); - var parseErrorStack = require('parseErrorStack'); - var RCTExceptionsManager = require('NativeModules').ExceptionsManager; + const parseErrorStack = require('parseErrorStack'); + const RCTExceptionsManager = require('NativeModules').ExceptionsManager; - var currentExceptionID = ++exceptionID; + const currentExceptionID = ++exceptionID; if (RCTExceptionsManager) { - var stack = parseErrorStack(e); + const stack = parseErrorStack(e); if (isFatal) { RCTExceptionsManager.reportFatalException(e.message, stack, currentExceptionID); } else { RCTExceptionsManager.reportSoftException(e.message, stack, currentExceptionID); } if (__DEV__) { - (sourceMapPromise = sourceMapPromise || loadSourceMap()) - .then(map => { - var prettyStack = parseErrorStack(e, map); - RCTExceptionsManager.updateExceptionMessage(e.message, prettyStack, currentExceptionID); - }) - .catch(error => { - // This can happen in a variety of normal situations, such as - // Network module not being available, or when running locally - console.warn('Unable to load source map: ' + error.message); - }); + require('SourceMapsCache').getSourceMaps().then(sourceMaps => { + const prettyStack = parseErrorStack(e, sourceMaps); + RCTExceptionsManager.updateExceptionMessage( + e.message, + prettyStack, + currentExceptionID, + ); + }) + .catch(error => { + // This can happen in a variety of normal situations, such as + // Network module not being available, or when running locally + console.warn('Unable to load source map: ' + error.message); + }); } } } @@ -81,15 +81,15 @@ function installConsoleErrorReporter() { if (arguments[0] && arguments[0].stack) { reportException(arguments[0], /* isFatal */ false); } else { - var stringifySafe = require('stringifySafe'); - var str = Array.prototype.map.call(arguments, stringifySafe).join(', '); + const stringifySafe = require('stringifySafe'); + const str = Array.prototype.map.call(arguments, stringifySafe).join(', '); if (str.slice(0, 10) === '"Warning: ') { // React warnings use console.error so that a stack trace is shown, but // we don't (currently) want these to show a redbox // (Note: Logic duplicated in polyfills/console.js.) return; } - var error : any = new Error('console.error: ' + str); + const error : any = new Error('console.error: ' + str); error.framesToPop = 1; reportException(error, /* isFatal */ false); } diff --git a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js new file mode 100644 index 000000000..63ab9c642 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js @@ -0,0 +1,44 @@ +/** + * 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. + * + * @providesModule SourceMapsCache + */ +'use strict'; + +const getObjectValues = require('getObjectValues'); +const SourceMapsUtils = require('SourceMapsUtils'); + +const sourceMapsCache = {}; + +const SourceMapsCache = { + mainSourceMapID: 'main', + + fetch({text, url, fullSourceMappingURL}) { + const sourceMappingURL = fullSourceMappingURL + ? fullSourceMappingURL + : SourceMapsUtils.extractSourceMapURL({text, url}); + + sourceMapsCache[sourceMappingURL] = SourceMapsUtils.fetchSourceMap( + sourceMappingURL + ); + }, + + getSourceMaps() { + fetchMainSourceMap(); + return Promise.all(getObjectValues(sourceMapsCache)); + }, +}; + +function fetchMainSourceMap() { + if (!sourceMapsCache[SourceMapsCache.mainSourceMapID]) { + sourceMapsCache[SourceMapsCache.mainSourceMapID] = + SourceMapsUtils.fetchMainSourceMap(); + } +} + +module.exports = SourceMapsCache; diff --git a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js new file mode 100644 index 000000000..f49a107c1 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js @@ -0,0 +1,76 @@ +/** + * 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. + * + * @providesModule SourceMapsUtils + * @flow + */ + +'use strict'; + +var HMRClient = require('../../Utilities/HMRClient'); +var Promise = require('Promise'); +var NativeModules = require('NativeModules'); +var SourceMapConsumer = require('SourceMap').SourceMapConsumer; +var SourceMapURL = require('./source-map-url'); + +var RCTSourceCode = NativeModules.SourceCode; +var RCTNetworking = NativeModules.Networking; + +var SourceMapsUtils = { + fetchMainSourceMap(): Promise { + return SourceMapsUtils._getMainSourceMapURL().then(url => + SourceMapsUtils.fetchSourceMap(url) + ); + }, + + fetchSourceMap(sourceMappingURL: string): Promise { + return fetch(sourceMappingURL) + .then(response => response.text()) + .then(map => new SourceMapConsumer(map)); + }, + + extractSourceMapURL(data: ({url:string, text:string})): ?string { + const url = data.url; + const text = data.text; + var mapURL = SourceMapURL.getFrom(text); + if (!mapURL) { + return null; + } + var baseURLs = url.match(/(.+:\/\/.*?)\//); + if (!baseURLs || baseURLs.length < 2) { + return null; + } + return baseURLs[1] + mapURL; + }, + + _getMainSourceMapURL(): Promise { + if (global.RAW_SOURCE_MAP) { + return Promise.resolve(global.RAW_SOURCE_MAP); + } + + if (!RCTSourceCode) { + return Promise.reject(new Error('RCTSourceCode module is not available')); + } + + if (!RCTNetworking) { + // Used internally by fetch + return Promise.reject(new Error('RCTNetworking module is not available')); + } + + return RCTSourceCode.getScriptText() + .then(SourceMapsUtils.extractSourceMapURL) + .then((url) => { + if (url === null) { + return Promise.reject(new Error('No source map URL found. May be running from bundled file.')); + } + return Promise.resolve(url); + }); + }, +}; + +module.exports = SourceMapsUtils; diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js deleted file mode 100644 index 852579c60..000000000 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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. - * - * @providesModule loadSourceMap - * @flow - */ - -'use strict'; - -var Promise = require('Promise'); -var NativeModules = require('NativeModules'); -var SourceMapConsumer = require('SourceMap').SourceMapConsumer; -var SourceMapURL = require('./source-map-url'); - -var RCTSourceCode = NativeModules.SourceCode; -var RCTNetworking = NativeModules.Networking; - -function loadSourceMap(): Promise { - return fetchSourceMap() - .then(map => new SourceMapConsumer(map)); -} - -function fetchSourceMap(): Promise { - if (global.RAW_SOURCE_MAP) { - return Promise.resolve(global.RAW_SOURCE_MAP); - } - - if (!RCTSourceCode) { - return Promise.reject(new Error('RCTSourceCode module is not available')); - } - - if (!RCTNetworking) { - // Used internally by fetch - return Promise.reject(new Error('RCTNetworking module is not available')); - } - - return RCTSourceCode.getScriptText() - .then(extractSourceMapURL) - .then((url) => { - if (url === null) { - return Promise.reject(new Error('No source map URL found. May be running from bundled file.')); - } - return Promise.resolve(url); - }) - .then(fetch) - .then(response => response.text()); -} - -function extractSourceMapURL({url, text, fullSourceMappingURL}): ?string { - if (fullSourceMappingURL) { - return fullSourceMappingURL; - } - var mapURL = SourceMapURL.getFrom(text); - if (!mapURL) { - return null; - } - var baseURL = url.match(/(.+:\/\/.*?)\//)[1]; - return baseURL + mapURL; -} - -module.exports = loadSourceMap; diff --git a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js index 63076c7b8..5fec94315 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js +++ b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js @@ -19,7 +19,11 @@ function resolveSourceMaps(sourceMapInstance, stackFrame) { column: stackFrame.column, }); if (orig) { - stackFrame.file = orig.source; + // remove query string if any + const queryStringStartIndex = orig.source.indexOf('?'); + stackFrame.file = queryStringStartIndex === -1 + ? orig.source + : orig.source.substring(0, queryStringStartIndex); stackFrame.lineNumber = orig.line; stackFrame.column = orig.column; } @@ -27,7 +31,7 @@ function resolveSourceMaps(sourceMapInstance, stackFrame) { } } -function parseErrorStack(e, sourceMapInstance) { +function parseErrorStack(e, sourceMaps) { if (!e || !e.stack) { return []; } @@ -39,8 +43,17 @@ function parseErrorStack(e, sourceMapInstance) { stack.shift(); } - if (sourceMapInstance) { - stack.forEach(resolveSourceMaps.bind(null, sourceMapInstance)); + if (sourceMaps) { + sourceMaps.forEach((sourceMap, index) => { + stack.forEach(frame => { + if (frame.file.indexOf(sourceMap.file) !== -1 || + frame.file.replace('.map', '.bundle').indexOf( + sourceMap.file + ) !== -1) { + resolveSourceMaps(sourceMap, frame); + } + }); + }); } return stack; diff --git a/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js b/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js index 8342a5a90..f44740031 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js +++ b/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js @@ -29,7 +29,7 @@ void (function(root, factory) { } }(this, function() { - var innerRegex = /[#@] sourceMappingURL=([^\s'"]*)/ + var innerRegex = /[#@] source(?:Mapping)?URL=([^\s'"]*)/ var regex = RegExp( "(?:" + diff --git a/Libraries/Utilities/HMRClient.js b/Libraries/Utilities/HMRClient.js index f500748d5..79be2be45 100644 --- a/Libraries/Utilities/HMRClient.js +++ b/Libraries/Utilities/HMRClient.js @@ -25,7 +25,7 @@ const HMRClient = { const host = 'localhost'; const port = '8081'; - // need to require WebSocket inside of `enable` function because the + // need to require WebSocket inside of `enable` function because // this module is defined as a `polyfillGlobal`. // See `InitializeJavascriptAppEngine.js` const WebSocket = require('WebSocket'); @@ -51,7 +51,31 @@ Error: ${e.message}` activeWS.onmessage = ({data}) => { data = JSON.parse(data); if (data.type === 'update') { - eval(data.body); // eslint-disable-line no-eval + const modules = data.body.modules; + const sourceMappingURLs = data.body.sourceMappingURLs; + const sourceURLs = data.body.sourceURLs; + + const RCTRedBox = require('NativeModules').RedBox; + RCTRedBox && RCTRedBox.dismiss && RCTRedBox.dismiss(); + + modules.forEach((code, i) => { + code = code + '\n\n' + sourceMappingURLs[i]; + + require('SourceMapsCache').fetch({ + text: code, + url: sourceURLs[i], + sourceMappingURL: sourceMappingURLs[i], + }); + + // on JSC we need to inject from native for sourcemaps to work + // (Safari doesn't support `sourceMappingURL` nor any variant when + // evaluating code) but on Chrome we can simply use eval + const injectFunction = typeof __injectHMRUpdate === 'function' + ? __injectHMRUpdate + : eval; + + injectFunction(code, sourceURLs[i]); + }) return; } diff --git a/React/Executors/RCTJSCExecutor.m b/React/Executors/RCTJSCExecutor.m index 6e5736a9f..9e4c6201e 100644 --- a/React/Executors/RCTJSCExecutor.m +++ b/React/Executors/RCTJSCExecutor.m @@ -23,6 +23,8 @@ #import "RCTPerformanceLogger.h" #import "RCTUtils.h" #import "RCTJSCProfiler.h" +#import "RCTRedBox.h" +#import "RCTSourceCode.h" static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled"; @@ -493,6 +495,22 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) RCTAssertParam(sourceURL); __weak RCTJSCExecutor *weakSelf = self; +#if RCT_DEV + _context.context[@"__injectHMRUpdate"] = ^(NSString *sourceCode, NSString *sourceCodeURL) { + RCTJSCExecutor *strongSelf = weakSelf; + + if (!strongSelf) { + return; + } + + JSStringRef execJSString = JSStringCreateWithUTF8CString(sourceCode.UTF8String); + JSStringRef jsURL = JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); + JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL); + JSStringRelease(jsURL); + JSStringRelease(execJSString); + }; +#endif + [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf || !strongSelf.isValid) { diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index b8717b150..ec0cfb850 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -309,7 +309,7 @@ RCT_EXPORT_MODULE() }); } -- (void)dismiss +RCT_EXPORT_METHOD(dismiss) { dispatch_async(dispatch_get_main_queue(), ^{ [_window dismiss]; diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js index 4a118d399..c3ea086ed 100644 --- a/local-cli/server/util/attachHMRServer.js +++ b/local-cli/server/util/attachHMRServer.js @@ -171,17 +171,18 @@ function attachHMRServer({httpServer, path, packagerServer}) { }) }) .then(bundle => { - if (!client || !bundle) { + if (!client || !bundle || bundle.isEmpty()) { return; } - const hmrUpdate = bundle.getSource(); - if (hmrUpdate) { - return JSON.stringify({ - type: 'update', - body: hmrUpdate, - }); - } + return JSON.stringify({ + type: 'update', + body: { + modules: bundle.getModulesCode(), + sourceURLs: bundle.getSourceURLs(), + sourceMappingURLs: bundle.getSourceMappingURLs(), + }, + }); }) .catch(error => { // send errors to the client instead of killing packager server @@ -207,7 +208,7 @@ function attachHMRServer({httpServer, path, packagerServer}) { return JSON.stringify({type: 'error', body}); }) .then(update => { - if (!client) { + if (!client || !update) { return; } diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js index 2641550a0..9700af088 100644 --- a/packager/react-packager/src/Bundler/Bundle.js +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -206,7 +206,7 @@ class Bundle extends BundleBase { _getCombinedSourceMaps(options) { const result = { version: 3, - file: 'bundle.js', + file: this._getSourceMapFile(), sections: [], }; @@ -246,7 +246,7 @@ class Bundle extends BundleBase { const mappings = this._getMappings(); const map = { - file: 'bundle.js', + file: this._getSourceMapFile(), sources: _.pluck(super.getModules(), 'sourcePath'), version: 3, names: [], @@ -262,6 +262,12 @@ class Bundle extends BundleBase { return eTag; } + _getSourceMapFile() { + return this._sourceMapUrl + ? this._sourceMapUrl.replace('.map', '.bundle') + : 'bundle.js'; + } + _getMappings() { const modules = super.getModules(); diff --git a/packager/react-packager/src/Bundler/BundleBase.js b/packager/react-packager/src/Bundler/BundleBase.js index b9e1cbe36..79cbfefc3 100644 --- a/packager/react-packager/src/Bundler/BundleBase.js +++ b/packager/react-packager/src/Bundler/BundleBase.js @@ -19,6 +19,10 @@ class BundleBase { this._mainModuleId = this._mainModuleName = undefined; } + isEmpty() { + return this._modules.length === 0 && this._assets.length === 0; + } + getMainModuleId() { return this._mainModuleId; } diff --git a/packager/react-packager/src/Bundler/HMRBundle.js b/packager/react-packager/src/Bundler/HMRBundle.js index fa8c95db7..49a21dfd8 100644 --- a/packager/react-packager/src/Bundler/HMRBundle.js +++ b/packager/react-packager/src/Bundler/HMRBundle.js @@ -8,12 +8,17 @@ */ 'use strict'; +const _ = require('underscore'); const BundleBase = require('./BundleBase'); const ModuleTransport = require('../lib/ModuleTransport'); class HMRBundle extends BundleBase { - constructor() { + constructor({sourceURLFn, sourceMappingURLFn}) { super(); + this._sourceURLFn = sourceURLFn + this._sourceMappingURLFn = sourceMappingURLFn; + this._sourceURLs = []; + this._sourceMappingURLs = []; } addModule(resolver, response, module, transformed) { @@ -21,14 +26,8 @@ class HMRBundle extends BundleBase { module, transformed.code, ).then(({name, code}) => { - code = ` - __accept( - '${name}', - function(global, require, module, exports) { - ${code} - } - ); - `; + // need to be in single line so that lines match on sourcemaps + code = `__accept(${JSON.stringify(name)}, function(global, require, module, exports) { ${code} });`; const moduleTransport = new ModuleTransport({ code, @@ -40,8 +39,22 @@ class HMRBundle extends BundleBase { }); super.addModule(moduleTransport); + this._sourceMappingURLs.push(this._sourceMappingURLFn(moduleTransport.sourcePath)); + this._sourceURLs.push(this._sourceURLFn(moduleTransport.sourcePath)); }); } + + getModulesCode() { + return this._modules.map(module => module.code); + } + + getSourceURLs() { + return this._sourceURLs; + } + + getSourceMappingURLs() { + return this._sourceMappingURLs; + } } module.exports = HMRBundle; diff --git a/packager/react-packager/src/Bundler/__tests__/Bundle-test.js b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js index 5e5fca226..b4152a7b2 100644 --- a/packager/react-packager/src/Bundler/__tests__/Bundle-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -214,7 +214,7 @@ describe('Bundle', () => { const sourceMap = otherBundle.getSourceMap({dev: true}); expect(sourceMap).toEqual({ - file: 'bundle.js', + file: 'test_url', version: 3, sections: [ { offset: { line: 0, column: 0 }, map: { name: 'sourcemap foo' } }, @@ -340,7 +340,7 @@ describe('Bundle', () => { function genSourceMap(modules) { - var sourceMapGen = new SourceMapGenerator({file: 'bundle.js', version: 3}); + var sourceMapGen = new SourceMapGenerator({file: 'test_url', version: 3}); var bundleLineNo = 0; for (var i = 0; i < modules.length; i++) { var module = modules[i]; diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index b6ff91333..d2852d447 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -166,9 +166,58 @@ class Bundler { }); } + _sourceHMRURL(platform, path) { + return this._hmrURL( + 'http://localhost:8081', // TODO: (martinb) avoid hardcoding + platform, + 'bundle', + path, + ); + } + + _sourceMappingHMRURL(platform, path) { + // Chrome expects `sourceURL` when eval'ing code + return this._hmrURL( + '\/\/# sourceURL=', + platform, + 'map', + path, + ); + } + + _hmrURL(prefix, platform, extensionOverride, path) { + const matchingRoot = this._projectRoots.find(root => path.startsWith(root)); + + if (!matchingRoot) { + throw new Error('No matching project root for ', path); + } + + const extensionStart = path.lastIndexOf('.'); + let resource = path.substring( + matchingRoot.length, + extensionStart !== -1 ? extensionStart : undefined, + ); + + const extension = extensionStart !== -1 + ? path.substring(extensionStart + 1) + : null; + + return ( + prefix + resource + + '.' + extensionOverride + '?' + + 'platform=' + platform + '&runModule=false&entryModuleOnly=true&hot=true' + ); + } + bundleForHMR(options) { return this._bundle({ - bundle: new HMRBundle(), + bundle: new HMRBundle({ + sourceURLFn: this._sourceHMRURL.bind(this, options.platform), + sourceMappingURLFn: this._sourceMappingHMRURL.bind( + this, + options.platform, + ), + }), hot: true, ...options, }); @@ -185,6 +234,7 @@ class Bundler { platform, unbundle: isUnbundle, hot: hot, + entryModuleOnly, }) { const findEventId = Activity.startEvent('find dependencies'); let transformEventId; @@ -195,18 +245,25 @@ class Bundler { bundle.setMainModuleName(response.mainModuleId); transformEventId = Activity.startEvent('transform'); - const moduleSystemDeps = includeSystemDependencies - ? this._resolver.getModuleSystemDependencies( - { dev: isDev, platform, isUnbundle } - ) - : []; + let dependencies; + if (entryModuleOnly) { + dependencies = response.dependencies.filter(module => + module.path.endsWith(entryFile) + ); + } else { + const moduleSystemDeps = includeSystemDependencies + ? this._resolver.getModuleSystemDependencies( + { dev: isDev, platform, isUnbundle } + ) + : []; - const modulesToProcess = modules || response.dependencies; - const dependencies = moduleSystemDeps.concat(modulesToProcess); + const modulesToProcess = modules || response.dependencies; + const dependencies = moduleSystemDeps.concat(modulesToProcess); - bundle.setNumPrependedModules && bundle.setNumPrependedModules( - response.numPrependedDependencies + moduleSystemDeps.length - ); + bundle.setNumPrependedModules && bundle.setNumPrependedModules( + response.numPrependedDependencies + moduleSystemDeps.length + ); + } let bar; if (process.stdout.isTTY) { diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index 123134f84..668e162f0 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -144,6 +144,7 @@ describe('processRequest', () => { platform: undefined, runBeforeMainModule: ['InitializeJavaScriptAppEngine'], unbundle: false, + entryModuleOnly: false, }); }); }); @@ -165,6 +166,7 @@ describe('processRequest', () => { platform: 'ios', runBeforeMainModule: ['InitializeJavaScriptAppEngine'], unbundle: false, + entryModuleOnly: false, }); }); }); @@ -321,6 +323,7 @@ describe('processRequest', () => { platform: undefined, runBeforeMainModule: ['InitializeJavaScriptAppEngine'], unbundle: false, + entryModuleOnly: false, }) ); }); @@ -341,6 +344,7 @@ describe('processRequest', () => { platform: undefined, runBeforeMainModule: ['InitializeJavaScriptAppEngine'], unbundle: false, + entryModuleOnly: false, }) ); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 99a3472d6..559fb3022 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -118,6 +118,10 @@ const bundleOpts = declareOpts({ type: 'boolean', default: false, }, + entryModuleOnly: { + type: 'boolean', + default: false, + }, }); const dependencyOpts = declareOpts({ @@ -527,6 +531,11 @@ class Server { false ), platform: platform, + entryModuleOnly: this._getBoolOptionFromQuery( + urlObj.query, + 'entryModuleOnly', + false, + ), }; }