[react-packager] Modernize `Server` to ES6

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

View File

@ -8,21 +8,20 @@
*/ */
'use strict'; 'use strict';
var url = require('url'); const Activity = require('../Activity');
var path = require('path'); const AssetServer = require('../AssetServer');
var declareOpts = require('../lib/declareOpts'); const FileWatcher = require('../FileWatcher');
var FileWatcher = require('../FileWatcher'); const Bundler = require('../Bundler');
var Bundler = require('../Bundler'); const Promise = require('promise');
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');
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: { projectRoots: {
type: 'array', type: 'array',
required: true, required: true,
@ -64,115 +63,7 @@ var validateOpts = declareOpts({
}, },
}); });
function Server(options) { const bundleOpts = declareOpts({
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({
sourceMapUrl: { sourceMapUrl: {
type: 'string', type: 'string',
required: false, required: false,
@ -199,245 +90,351 @@ var bundleOpts = declareOpts({
}, },
}); });
Server.prototype.buildBundle = function(options) { class Server {
var opts = bundleOpts(options); constructor(options) {
const opts = validateOpts(options);
return this._bundler.bundle( this._projectRoots = opts.projectRoots;
opts.entryFile, this._bundles = Object.create(null);
opts.runModule, this._changeWatchers = [];
opts.sourceMapUrl,
opts.dev
);
};
Server.prototype.buildBundleFromUrl = function(reqUrl) { const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext);
var options = getOptionsFromUrl(reqUrl);
return this.buildBundle(options);
};
Server.prototype.getDependencies = function(main) { var watchRootConfigs = opts.projectRoots.map(dir => {
return this._bundler.getDependencies(main); return {
}; dir: dir,
globs: [
'**/*.js',
'**/*.json',
].concat(assetGlobs),
};
});
Server.prototype._processDebugRequest = function(reqUrl, res) { if (opts.assetRoots != null) {
var ret = '<!doctype html>'; watchRootConfigs = watchRootConfigs.concat(
var pathname = url.parse(reqUrl).pathname; opts.assetRoots.map(dir => {
var parts = pathname.split('/').filter(Boolean); return {
if (parts.length === 1) { dir: dir,
ret += '<div><a href="/debug/bundles">Cached Bundles</a></div>'; globs: assetGlobs,
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;
}
} }
});
};
Server.prototype._processAssetsRequest = function(req, res) { this._fileWatcher = options.nonPersistent
var urlObj = url.parse(req.url, true); ? FileWatcher.createDummyWatcher()
var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); : new FileWatcher(watchRootConfigs);
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();
};
Server.prototype._processProfile = function(req, res) { this._assetServer = new AssetServer({
console.log('Dumping profile information...'); projectRoots: opts.projectRoots,
var dumpName = '/tmp/dump_' + Date.now() + '.json'; assetExts: opts.assetExts,
var prefix = process.env.TRACE_VIEWER_PATH || ''; });
var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName;
fs.writeFileSync(dumpName, req.rawBody); const bundlerOpts = Object.create(opts);
exec(cmd, function (error) { bundlerOpts.fileWatcher = this._fileWatcher;
if (error) { bundlerOpts.assetServer = this._assetServer;
if (error.code === 127) { this._bundler = new Bundler(bundlerOpts);
console.error(
'\n** Failed executing `' + cmd + '` **\n\n' + this._fileWatcher.on('all', this._onFileChange.bind(this));
'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' +
'You can get it at:\n\n' + this._debouncedFileChangeHandler = _.debounce(filePath => {
' https://github.com/google/trace-viewer\n\n' + this._rebuildBundles(filePath);
'If it\'s not in your path, you can set a custom path with:\n\n' + this._informChangeWatchers();
' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + }, 50);
'NOTE: Your profile data was kept at:\n\n' + }
' ' + dumpName
); end() {
} else { Promise.all([
console.error('Unknown error', error); this._fileWatcher.end(),
} this._bundler.kill(),
res.end(); ]);
return; }
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 { } else {
exec('rm ' + dumpName); res.writeHead('404');
exec('open ' + dumpName.replace(/json$/, 'html'), function (error) { res.end('Invalid debug request');
if (error) { return;
console.error(error); }
}
_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(); res.end();
}); return;
} } else {
}); exec('rm ' + dumpName);
}; exec('open ' + dumpName.replace(/json$/, 'html'), err => {
if (err) {
Server.prototype.processRequest = function(req, res, next) { console.error(err);
var urlObj = url.parse(req.url, true); }
var pathname = urlObj.pathname; res.end();
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,
}); });
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;