mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: The bundler class had duplicated code and parts that were hard to follow, because functions accepted heterogenous arguments, leading to a lot of conditionals. This commit tries to remove duplication between build steps of different type of bundles, and by accepting less different types of arguments. These differences are now handled further up the call stack. public Reviewed By: sebmarkbage Differential Revision: D2905807 fb-gh-sync-id: ef85ea0d461a9a06a4a64480e014a5324c4ef532
562 lines
15 KiB
JavaScript
562 lines
15 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,
|
|
},
|
|
assetRoots: {
|
|
type: 'array',
|
|
required: false,
|
|
},
|
|
assetExts: {
|
|
type: 'array',
|
|
default: [
|
|
'bmp', 'gif', 'jpg', 'jpeg', 'png', 'psd', 'svg', 'webp', // Image formats
|
|
'm4v', 'mov', 'mp4', 'mpeg', 'mpg', 'webm', // Video formats
|
|
'aac', 'aiff', 'caf', 'm4a', 'mp3', 'wav', // Audio formats
|
|
'html', // Document formats
|
|
],
|
|
},
|
|
transformTimeoutInterval: {
|
|
type: 'number',
|
|
required: false,
|
|
},
|
|
getTransformOptionsModulePath: {
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
disableInternalTransforms: {
|
|
type: 'boolean',
|
|
default: 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,
|
|
},
|
|
entryModuleOnly: {
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const dependencyOpts = declareOpts({
|
|
platform: {
|
|
type: 'string',
|
|
required: true,
|
|
},
|
|
dev: {
|
|
type: 'boolean',
|
|
default: true,
|
|
},
|
|
entryFile: {
|
|
type: 'string',
|
|
required: true,
|
|
},
|
|
recursive: {
|
|
type: 'boolean',
|
|
default: 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 => {
|
|
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.hmrBundle(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,
|
|
opts.recursive,
|
|
);
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
// If Hot Loading is enabled avoid rebuilding bundles and sending live
|
|
// updates. Instead, send the HMR updates right away and clear the bundles
|
|
// cache so that if the user reloads we send them a fresh bundle
|
|
if (this._hmrFileChangeListener) {
|
|
// Clear cached bundles in case user reloads
|
|
this._clearBundles();
|
|
this._hmrFileChangeListener(absPath, this._bundler.stat(absPath));
|
|
return;
|
|
}
|
|
|
|
// Make sure the file watcher event runs through the system before
|
|
// we rebuild the bundles.
|
|
this._debouncedFileChangeHandler(absPath);
|
|
}
|
|
|
|
_clearBundles() {
|
|
this._bundles = Object.create(null);
|
|
}
|
|
|
|
_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.setHeader('ETag', p.getEtag());
|
|
if (req.headers['if-none-match'] === res.getHeader('ETag')){
|
|
res.statusCode = 304;
|
|
res.end();
|
|
} else {
|
|
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,
|
|
entryModuleOnly: this._getBoolOptionFromQuery(
|
|
urlObj.query,
|
|
'entryModuleOnly',
|
|
false,
|
|
),
|
|
};
|
|
}
|
|
|
|
_getBoolOptionFromQuery(query, opt, defaultVal) {
|
|
if (query[opt] == null && defaultVal != null) {
|
|
return defaultVal;
|
|
}
|
|
|
|
return query[opt] === 'true' || query[opt] === '1';
|
|
}
|
|
}
|
|
|
|
module.exports = Server;
|