Files
react-native/packager/react-packager/src/Server/index.js
Martín Bigio f2bdb79782 Make HMR server send full list modules that changed
Summary:
public

Before this diff we were only accepting the module that was modified but the user. This works fine as long as the user doesn't modify the dependencies a module has but once he starts doing so the HMR runtime may fail when updating modules' code because they might might a few dependencies. For instance, if the user changes the `src` a `Image` has to reference an image (using the new asset system) that wasn't on the original bundle the user will get a red box. This diff addresses this by diffing the modules the app currently has with the new ones it should have and including all of them on the HMR update. Note this diffing is only done when the we realize the module that was modified changed it's dependencies so there's no additional overhead on this change.

Reviewed By: vjeux

Differential Revision: D2796325

fb-gh-sync-id: cac95f2e995310634c221bbbb09d9f3e7bc03e8d
2016-01-04 13:02:27 -08:00

530 lines
14 KiB
JavaScript

/**
* 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 Activity = require('../Activity');
const AssetServer = require('../AssetServer');
const FileWatcher = require('../DependencyResolver/FileWatcher');
const getPlatformExtension = require('../DependencyResolver/lib/getPlatformExtension');
const Bundler = require('../Bundler');
const Promise = require('promise');
const _ = require('underscore');
const declareOpts = require('../lib/declareOpts');
const path = require('path');
const url = require('url');
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
},
blacklistRE: {
type: 'object', // typeof regex is object
},
moduleFormat: {
type: 'string',
default: 'haste',
},
polyfillModuleNames: {
type: 'array',
default: [],
},
cacheVersion: {
type: 'string',
default: '1.0',
},
resetCache: {
type: 'boolean',
default: false,
},
transformModulePath: {
type:'string',
required: false,
},
nonPersistent: {
type: 'boolean',
default: false,
},
enableInternalTransforms: {
type: 'boolean',
default: false,
},
assetRoots: {
type: 'array',
required: false,
},
assetExts: {
type: 'array',
default: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp'],
},
transformTimeoutInterval: {
type: 'number',
required: false,
},
getTransformOptionsModulePath: {
type: 'string',
required: false,
}
});
const bundleOpts = declareOpts({
sourceMapUrl: {
type: 'string',
required: false,
},
entryFile: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
minify: {
type: 'boolean',
default: false,
},
runModule: {
type: 'boolean',
default: true,
},
inlineSourceMap: {
type: 'boolean',
default: false,
},
platform: {
type: 'string',
required: true,
},
runBeforeMainModule: {
type: 'array',
default: [
// Ensures essential globals are available and are patched correctly.
'InitializeJavaScriptAppEngine'
],
},
unbundle: {
type: 'boolean',
default: false,
},
hot: {
type: 'boolean',
default: false,
},
});
const dependencyOpts = declareOpts({
platform: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
entryFile: {
type: 'string',
required: true,
},
});
class Server {
constructor(options) {
const opts = validateOpts(options);
this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
this._fileChangeListeners = [];
const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext);
var watchRootConfigs = opts.projectRoots.map(dir => {
return {
dir: dir,
globs: [
'**/*.js',
'**/*.json',
].concat(assetGlobs),
};
});
if (opts.assetRoots != null) {
watchRootConfigs = watchRootConfigs.concat(
opts.assetRoots.map(dir => {
return {
dir: dir,
globs: assetGlobs,
};
})
);
}
this._fileWatcher = options.nonPersistent
? FileWatcher.createDummyWatcher()
: new FileWatcher(watchRootConfigs);
this._assetServer = new AssetServer({
projectRoots: opts.projectRoots,
assetExts: opts.assetExts,
});
const bundlerOpts = Object.create(opts);
bundlerOpts.fileWatcher = this._fileWatcher;
bundlerOpts.assetServer = this._assetServer;
this._bundler = new Bundler(bundlerOpts);
this._fileWatcher.on('all', this._onFileChange.bind(this));
this._debouncedFileChangeHandler = _.debounce(filePath => {
// if Hot Loading is enabled avoid rebuilding bundles and sending live
// updates. Instead, send the HMR updates right away and once that
// finishes, invoke any other file change listener.
if (this._hmrFileChangeListener) {
this._hmrFileChangeListener(filePath);
return;
}
this._rebuildBundles(filePath);
this._informChangeWatchers();
}, 50);
}
end() {
Promise.all([
this._fileWatcher.end(),
this._bundler.kill(),
]);
}
setHMRFileChangeListener(listener) {
this._hmrFileChangeListener = listener;
}
buildBundle(options) {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = bundleOpts(options);
return this._bundler.bundle(opts);
});
}
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);
}
buildBundleForHMR(modules) {
return this._bundler.bundleForHMR(modules);
}
getShallowDependencies(entryFile) {
return this._bundler.getShallowDependencies(entryFile);
}
getModuleForPath(entryFile) {
return this._bundler.getModuleForPath(entryFile);
}
getDependencies(options) {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = dependencyOpts(options);
return this._bundler.getDependencies(
opts.entryFile,
opts.dev,
opts.platform,
);
});
}
getOrderedDependencyPaths(options) {
return Promise.resolve().then(() => {
const opts = dependencyOpts(options);
return this._bundler.getOrderedDependencyPaths(opts);
});
}
_onFileChange(type, filepath, root) {
const absPath = path.join(root, filepath);
this._bundler.invalidateFile(absPath);
// Make sure the file watcher event runs through the system before
// we rebuild the bundles.
this._debouncedFileChangeHandler(absPath);
}
_rebuildBundles() {
const buildBundle = this.buildBundle.bind(this);
const bundles = this._bundles;
Object.keys(bundles).forEach(function(optionsJson) {
const options = JSON.parse(optionsJson);
// Wait for a previous build (if exists) to finish.
bundles[optionsJson] = (bundles[optionsJson] || Promise.resolve()).finally(function() {
// With finally promise callback we can't change the state of the promise
// so we need to reassign the promise.
bundles[optionsJson] = buildBundle(options).then(function(p) {
// Make a throwaway call to getSource to cache the source string.
p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
return p;
});
});
return bundles[optionsJson];
});
}
_informChangeWatchers() {
const watchers = this._changeWatchers;
const headers = {
'Content-Type': 'application/json; charset=UTF-8',
};
watchers.forEach(function(w) {
w.res.writeHead(205, headers);
w.res.end(JSON.stringify({ changed: true }));
});
this._changeWatchers = [];
}
_processDebugRequest(reqUrl, res) {
var ret = '<!doctype html>';
const pathname = url.parse(reqUrl).pathname;
const parts = pathname.split('/').filter(Boolean);
if (parts.length === 1) {
ret += '<div><a href="/debug/bundles">Cached Bundles</a></div>';
ret += '<div><a href="/debug/graph">Dependency Graph</a></div>';
res.end(ret);
} else if (parts[1] === 'bundles') {
ret += '<h1> Cached Bundles </h1>';
Promise.all(Object.keys(this._bundles).map(optionsJson =>
this._bundles[optionsJson].then(p => {
ret += '<div><h2>' + optionsJson + '</h2>';
ret += p.getDebugInfo();
})
)).then(
() => res.end(ret),
e => {
res.writeHead(500);
res.end('Internal Error');
console.log(e.stack);
}
);
} else if (parts[1] === 'graph'){
ret += '<h1> Dependency Graph </h2>';
ret += this._bundler.getGraphDebugInfo();
res.end(ret);
} else {
res.writeHead('404');
res.end('Invalid debug request');
return;
}
}
_processOnChangeRequest(req, res) {
const watchers = this._changeWatchers;
watchers.push({
req: req,
res: res,
});
req.on('close', () => {
for (let i = 0; i < watchers.length; i++) {
if (watchers[i] && watchers[i].req === req) {
watchers.splice(i, 1);
break;
}
}
});
}
_processAssetsRequest(req, res) {
const urlObj = url.parse(req.url, true);
const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/);
const assetEvent = Activity.startEvent(`processing asset request ${assetPath[1]}`);
this._assetServer.get(assetPath[1], urlObj.query.platform)
.then(
data => res.end(data),
error => {
console.error(error.stack);
res.writeHead('404');
res.end('Asset not found');
}
).done(() => Activity.endEvent(assetEvent));
}
processRequest(req, res, next) {
const urlObj = url.parse(req.url, true);
var pathname = urlObj.pathname;
var requestType;
if (pathname.match(/\.bundle$/)) {
requestType = 'bundle';
} else if (pathname.match(/\.map$/)) {
requestType = 'map';
} else if (pathname.match(/\.assets$/)) {
requestType = 'assets';
} else if (pathname.match(/^\/debug/)) {
this._processDebugRequest(req.url, res);
return;
} else if (pathname.match(/^\/onchange\/?$/)) {
this._processOnChangeRequest(req, res);
return;
} else if (pathname.match(/^\/assets\//)) {
this._processAssetsRequest(req, res);
return;
} else {
next();
return;
}
const startReqEventId = Activity.startEvent('request:' + req.url);
const options = this._getOptionsFromUrl(req.url);
const optionsJson = JSON.stringify(options);
const building = this._bundles[optionsJson] || this.buildBundle(options);
this._bundles[optionsJson] = building;
building.then(
p => {
if (requestType === 'bundle') {
var bundleSource = p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
res.setHeader('Content-Type', 'application/javascript');
res.end(bundleSource);
Activity.endEvent(startReqEventId);
} else if (requestType === 'map') {
var sourceMap = p.getSourceMap({
minify: options.minify,
dev: options.dev,
});
if (typeof sourceMap !== 'string') {
sourceMap = JSON.stringify(sourceMap);
}
res.setHeader('Content-Type', 'application/json');
res.end(sourceMap);
Activity.endEvent(startReqEventId);
} else if (requestType === 'assets') {
var assetsList = JSON.stringify(p.getAssets());
res.setHeader('Content-Type', 'application/json');
res.end(assetsList);
Activity.endEvent(startReqEventId);
}
},
this._handleError.bind(this, res, optionsJson)
).done();
}
_handleError(res, bundleID, error) {
res.writeHead(error.status || 500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError' ||
error.type === 'NotFoundError' ||
error.type === 'UnableToResolveError') {
error.errors = [{
description: error.description,
filename: error.filename,
lineNumber: error.lineNumber,
}];
res.end(JSON.stringify(error));
if (error.type === 'NotFoundError') {
delete this._bundles[bundleID];
}
} else {
console.error(error.stack || error);
res.end(JSON.stringify({
type: 'InternalError',
message: 'react-packager has encountered an internal error, ' +
'please check your terminal error output for more details',
}));
}
}
_getOptionsFromUrl(reqUrl) {
// `true` to parse the query param as an object.
const urlObj = url.parse(reqUrl, true);
// node v0.11.14 bug see https://github.com/facebook/react-native/issues/218
urlObj.query = urlObj.query || {};
const pathname = decodeURIComponent(urlObj.pathname);
// Backwards compatibility. Options used to be as added as '.' to the
// entry module name. We can safely remove these options.
const entryFile = pathname.replace(/^\//, '').split('.').filter(part => {
if (part === 'includeRequire' || part === 'runModule' ||
part === 'bundle' || part === 'map' || part === 'assets') {
return false;
}
return true;
}).join('.') + '.js';
const sourceMapUrlObj = _.clone(urlObj);
sourceMapUrlObj.pathname = pathname.replace(/\.bundle$/, '.map');
// try to get the platform from the url
const platform = urlObj.query.platform ||
getPlatformExtension(pathname);
return {
sourceMapUrl: url.format(sourceMapUrlObj),
entryFile: entryFile,
dev: this._getBoolOptionFromQuery(urlObj.query, 'dev', true),
minify: this._getBoolOptionFromQuery(urlObj.query, 'minify'),
hot: this._getBoolOptionFromQuery(urlObj.query, 'hot', false),
runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: this._getBoolOptionFromQuery(
urlObj.query,
'inlineSourceMap',
false
),
platform: platform,
};
}
_getBoolOptionFromQuery(query, opt, defaultVal) {
if (query[opt] == null && defaultVal != null) {
return defaultVal;
}
return query[opt] === 'true' || query[opt] === '1';
}
}
module.exports = Server;