[react-packager] Modernize `Server` to ES6

This commit is contained in:
Martín Bigio 2015-08-14 12:08:46 -07:00
parent 0623371814
commit 3be2b1d966
1 changed files with 346 additions and 349 deletions

View File

@ -8,21 +8,20 @@
*/
'use strict';
var url = require('url');
var path = require('path');
var declareOpts = require('../lib/declareOpts');
var FileWatcher = require('../FileWatcher');
var Bundler = require('../Bundler');
var Activity = require('../Activity');
var AssetServer = require('../AssetServer');
var Promise = require('promise');
var _ = require('underscore');
var exec = require('child_process').exec;
var fs = require('fs');
const Activity = require('../Activity');
const AssetServer = require('../AssetServer');
const FileWatcher = require('../FileWatcher');
const Bundler = require('../Bundler');
const Promise = require('promise');
module.exports = Server;
const _ = require('underscore');
const declareOpts = require('../lib/declareOpts');
const exec = require('child_process').exec;
const fs = require('fs');
const path = require('path');
const url = require('url');
var validateOpts = declareOpts({
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
@ -64,115 +63,7 @@ var validateOpts = declareOpts({
},
});
function Server(options) {
var opts = validateOpts(options);
this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
var assetGlobs = opts.assetExts.map(function(ext) {
return '**/*.' + ext;
});
var watchRootConfigs = opts.projectRoots.map(function(dir) {
return {
dir: dir,
globs: [
'**/*.js',
'**/*.json',
].concat(assetGlobs),
};
});
if (opts.assetRoots != null) {
watchRootConfigs = watchRootConfigs.concat(
opts.assetRoots.map(function(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,
});
var bundlerOpts = Object.create(opts);
bundlerOpts.fileWatcher = this._fileWatcher;
bundlerOpts.assetServer = this._assetServer;
this._bundler = new Bundler(bundlerOpts);
var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.on('all', onFileChange);
var self = this;
this._debouncedFileChangeHandler = _.debounce(function(filePath) {
self._rebuildBundles(filePath);
self._informChangeWatchers();
}, 50);
}
Server.prototype._onFileChange = function(type, filepath, root) {
var 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);
};
Server.prototype._rebuildBundles = function() {
var buildBundle = this.buildBundle.bind(this);
var bundles = this._bundles;
Object.keys(bundles).forEach(function(optionsJson) {
var 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,
});
return p;
});
});
return bundles[optionsJson];
});
};
Server.prototype._informChangeWatchers = function() {
var watchers = this._changeWatchers;
var 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 = [];
};
Server.prototype.end = function() {
Promise.all([
this._fileWatcher.end(),
this._bundler.kill(),
]);
};
var bundleOpts = declareOpts({
const bundleOpts = declareOpts({
sourceMapUrl: {
type: 'string',
required: false,
@ -199,245 +90,351 @@ var bundleOpts = declareOpts({
},
});
Server.prototype.buildBundle = function(options) {
var opts = bundleOpts(options);
class Server {
constructor(options) {
const opts = validateOpts(options);
return this._bundler.bundle(
opts.entryFile,
opts.runModule,
opts.sourceMapUrl,
opts.dev
);
};
this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
Server.prototype.buildBundleFromUrl = function(reqUrl) {
var options = getOptionsFromUrl(reqUrl);
return this.buildBundle(options);
};
const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext);
Server.prototype.getDependencies = function(main) {
return this._bundler.getDependencies(main);
};
var watchRootConfigs = opts.projectRoots.map(dir => {
return {
dir: dir,
globs: [
'**/*.js',
'**/*.json',
].concat(assetGlobs),
};
});
Server.prototype._processDebugRequest = function(reqUrl, res) {
var ret = '<!doctype html>';
var pathname = url.parse(reqUrl).pathname;
var 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(function(optionsJson) {
return this._bundles[optionsJson].then(function(p) {
ret += '<div><h2>' + optionsJson + '</h2>';
ret += p.getDebugInfo();
});
}, this)).then(
function() { res.end(ret); },
function(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;
}
};
Server.prototype._processOnChangeRequest = function(req, res) {
var watchers = this._changeWatchers;
watchers.push({
req: req,
res: res,
});
req.on('close', function() {
for (var i = 0; i < watchers.length; i++) {
if (watchers[i] && watchers[i].req === req) {
watchers.splice(i, 1);
break;
}
if (opts.assetRoots != null) {
watchRootConfigs = watchRootConfigs.concat(
opts.assetRoots.map(dir => {
return {
dir: dir,
globs: assetGlobs,
};
})
);
}
});
};
Server.prototype._processAssetsRequest = function(req, res) {
var urlObj = url.parse(req.url, true);
var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/);
this._assetServer.get(assetPath[1])
.then(
function(data) {
res.end(data);
},
function(error) {
console.error(error.stack);
res.writeHead('404');
res.end('Asset not found');
}
).done();
};
this._fileWatcher = options.nonPersistent
? FileWatcher.createDummyWatcher()
: new FileWatcher(watchRootConfigs);
Server.prototype._processProfile = function(req, res) {
console.log('Dumping profile information...');
var dumpName = '/tmp/dump_' + Date.now() + '.json';
var prefix = process.env.TRACE_VIEWER_PATH || '';
var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName;
fs.writeFileSync(dumpName, req.rawBody);
exec(cmd, function (error) {
if (error) {
if (error.code === 127) {
console.error(
'\n** Failed executing `' + cmd + '` **\n\n' +
'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' +
'You can get it at:\n\n' +
' https://github.com/google/trace-viewer\n\n' +
'If it\'s not in your path, you can set a custom path with:\n\n' +
' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' +
'NOTE: Your profile data was kept at:\n\n' +
' ' + dumpName
);
} else {
console.error('Unknown error', error);
}
res.end();
return;
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(),
]);
}
buildBundle(options) {
const opts = bundleOpts(options);
return this._bundler.bundle(
opts.entryFile,
opts.runModule,
opts.sourceMapUrl,
opts.dev
);
}
buildBundleFromUrl(reqUrl) {
const options = this._getOptionsFromUrl(reqUrl);
return this.buildBundle(options);
}
getDependencies(main) {
return this._bundler.getDependencies(main);
}
_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,
});
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 {
exec('rm ' + dumpName);
exec('open ' + dumpName.replace(/json$/, 'html'), function (error) {
if (error) {
console.error(error);
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\/(.+)$/);
this._assetServer.get(assetPath[1])
.then(
data => res.end(data),
error => {
console.error(error.stack);
res.writeHead('404');
res.end('Asset not found');
}
).done();
}
_processProfile(req, res) {
console.log('Dumping profile information...');
const dumpName = '/tmp/dump_' + Date.now() + '.json';
const prefix = process.env.TRACE_VIEWER_PATH || '';
const cmd = path.join(prefix, 'trace2html') + ' ' + dumpName;
fs.writeFileSync(dumpName, req.rawBody);
exec(cmd, error => {
if (error) {
if (error.code === 127) {
console.error(
'\n** Failed executing `' + cmd + '` **\n\n' +
'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' +
'You can get it at:\n\n' +
' https://github.com/google/trace-viewer\n\n' +
'If it\'s not in your path, you can set a custom path with:\n\n' +
' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' +
'NOTE: Your profile data was kept at:\n\n' +
' ' + dumpName
);
} else {
console.error('Unknown error', error);
}
res.end();
});
}
});
};
Server.prototype.processRequest = function(req, res, next) {
var 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(/^\/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 if (pathname.match(/^\/profile\/?$/)) {
this._processProfile(req, res);
return;
} else {
next();
return;
}
var startReqEventId = Activity.startEvent('request:' + req.url);
var options = getOptionsFromUrl(req.url);
var optionsJson = JSON.stringify(options);
var building = this._bundles[optionsJson] || this.buildBundle(options);
this._bundles[optionsJson] = building;
building.then(
function(p) {
if (requestType === 'bundle') {
var bundleSource = p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
return;
} else {
exec('rm ' + dumpName);
exec('open ' + dumpName.replace(/json$/, 'html'), err => {
if (err) {
console.error(err);
}
res.end();
});
res.setHeader('Content-Type', 'application/javascript');
res.end(bundleSource);
Activity.endEvent(startReqEventId);
} else if (requestType === 'map') {
var sourceMap = JSON.stringify(p.getSourceMap());
res.setHeader('Content-Type', 'application/json');
res.end(sourceMap);
Activity.endEvent(startReqEventId);
}
},
this._handleError.bind(this, res, optionsJson)
).done();
};
Server.prototype._handleError = function(res, bundleID, error) {
res.writeHead(error.status || 500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError' || error.type === 'NotFoundError') {
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',
}));
}
};
function getOptionsFromUrl(reqUrl) {
// `true` to parse the query param as an object.
var urlObj = url.parse(reqUrl, true);
// node v0.11.14 bug see https://github.com/facebook/react-native/issues/218
urlObj.query = urlObj.query || {};
var pathname = decodeURIComponent(urlObj.pathname);
// Backwards compatibility. Options used to be as added as '.' to the
// entry module name. We can safely remove these options.
var entryFile = pathname.replace(/^\//, '').split('.').filter(function(part) {
if (part === 'includeRequire' || part === 'runModule' ||
part === 'bundle' || part === 'map') {
return false;
}
return true;
}).join('.') + '.js';
return {
sourceMapUrl: pathname.replace(/\.bundle$/, '.map'),
entryFile: entryFile,
dev: getBoolOptionFromQuery(urlObj.query, 'dev', true),
minify: getBoolOptionFromQuery(urlObj.query, 'minify'),
runModule: getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: getBoolOptionFromQuery(
urlObj.query,
'inlineSourceMap',
false
),
};
}
function getBoolOptionFromQuery(query, opt, defaultVal) {
if (query[opt] == null && defaultVal != null) {
return defaultVal;
});
}
return query[opt] === 'true' || query[opt] === '1';
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(/^\/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 if (pathname.match(/^\/profile\/?$/)) {
this._processProfile(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,
});
res.setHeader('Content-Type', 'application/javascript');
res.end(bundleSource);
Activity.endEvent(startReqEventId);
} else if (requestType === 'map') {
var sourceMap = JSON.stringify(p.getSourceMap());
res.setHeader('Content-Type', 'application/json');
res.end(sourceMap);
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.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') {
return false;
}
return true;
}).join('.') + '.js';
return {
sourceMapUrl: pathname.replace(/\.bundle$/, '.map'),
entryFile: entryFile,
dev: this._getBoolOptionFromQuery(urlObj.query, 'dev', true),
minify: this._getBoolOptionFromQuery(urlObj.query, 'minify'),
runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: this._getBoolOptionFromQuery(
urlObj.query,
'inlineSourceMap',
false
),
};
}
_getBoolOptionFromQuery(query, opt, defaultVal) {
if (query[opt] == null && defaultVal != null) {
return defaultVal;
}
return query[opt] === 'true' || query[opt] === '1';
}
}
module.exports = Server;