2015-02-05 updates

- [ReactServer] Fix newly introduced bug | Amjad Masad
- [ReactServer] Last sync from github | Amjad Masad
- [RFC-ReactNative] Subscribable overhaul, clean up AppState/Reachability | Eric Vicenti
- [ReactKit] Enable tappable <Text /> subnodes | Alex Akers
This commit is contained in:
Christopher Chedeau 2015-02-06 15:45:28 -08:00
parent ddaccf41d8
commit 4fb7489ee9
14 changed files with 351 additions and 166 deletions

View File

@ -1,100 +1,19 @@
var Packager = require('./src/Packager'); 'use strict';
var Activity = require('./src/Activity'); var Activity = require('./src/Activity');
var url = require('url'); var Server = require('./src/Server');
exports.middleware = function(options) {
var server = new Server(options);
return server.processRequest.bind(server);
};
exports.buildPackageFromUrl = function(options, reqUrl) { exports.buildPackageFromUrl = function(options, reqUrl) {
Activity.disable(); Activity.disable();
var packager = createPackager(options); var server = new Server(options);
var params = getOptionsFromPath(url.parse(reqUrl).pathname); return server.buildPackageFromUrl(reqUrl)
return packager.package( .then(function(p) {
params.main, server.kill();
params.runModule,
params.sourceMapUrl
).then(function(p) {
packager.kill();
return p; return p;
}); });
}; };
exports.catalystMiddleware = function(options) {
var packager = createPackager(options);
return function(req, res, next) {
var options;
if (req.url.match(/\.bundle$/)) {
options = getOptionsFromPath(url.parse(req.url).pathname);
packager.package(
options.main,
options.runModule,
options.sourceMapUrl
).then(
function(package) {
res.end(package.getSource());
},
function(error) {
handleError(res, error);
}
).done();
} else if (req.url.match(/\.map$/)) {
options = getOptionsFromPath(url.parse(req.url).pathname);
packager.package(
options.main,
options.runModule,
options.sourceMapUrl
).then(
function(package) {
res.end(JSON.stringify(package.getSourceMap()));
},
function(error) {
handleError(res, error);
}
).done();
} else {
next();
}
};
};
function getOptionsFromPath(pathname) {
var parts = pathname.split('.');
// Remove the leading slash.
var main = parts[0].slice(1) + '.js';
return {
runModule: parts.slice(1).some(function(part) {
return part === 'runModule';
}),
main: main,
sourceMapUrl: parts.slice(0, -1).join('.') + '.map'
};
}
function handleError(res, error) {
res.writeHead(500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError') {
res.end(JSON.stringify(error));
} 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 createPackager(options) {
return new Packager({
projectRoot: options.projectRoot,
blacklistRE: options.blacklistRE,
polyfillModuleNames: options.polyfillModuleNames || [],
runtimeCode: options.runtimeCode,
cacheVersion: options.cacheVersion,
resetCache: options.resetCache,
dev: options.dev,
});
}
exports.kill = Packager.kill;

View File

@ -1,6 +1,10 @@
{ {
"name": "react-packager", "name": "react-packager",
"version": "0.0.1", "version": "0.0.2",
"description": "", "description": "",
"main": "index.js" "main": "index.js",
"jest": {
"unmockedModulePathPatterns": ["source-map"],
"testPathIgnorePatterns": ["JSAppServer/node_modules"]
}
} }

View File

@ -131,11 +131,12 @@ function _writeAction(action) {
case 'endEvent': case 'endEvent':
var startAction = _eventStarts[action.eventId]; var startAction = _eventStarts[action.eventId];
var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : '';
console.log( console.log(
'[' + fmtTime + '] ' + '[' + fmtTime + '] ' +
'<END> ' + startAction.eventName + '<END> ' + startAction.eventName +
'(' + (action.tstamp - startAction.tstamp) + 'ms)' + '(' + (action.tstamp - startAction.tstamp) + 'ms)' +
data startData
); );
delete _eventStarts[action.eventId]; delete _eventStarts[action.eventId];
break; break;

View File

@ -29,7 +29,7 @@ function HasteDependencyResolver(config) {
return filepath.indexOf('__tests__') !== -1 || return filepath.indexOf('__tests__') !== -1 ||
(config.blacklistRE && config.blacklistRE.test(filepath)); (config.blacklistRE && config.blacklistRE.test(filepath));
}, },
fileWatcher: new FileWatcher(config) fileWatcher: new FileWatcher(config.projectRoot)
}); });
this._polyfillModuleNames = [ this._polyfillModuleNames = [

View File

@ -17,14 +17,14 @@ describe('FileWatcher', function() {
}); });
pit('it should get the watcher instance when ready', function() { pit('it should get the watcher instance when ready', function() {
var fileWatcher = new FileWatcher({projectRoot: 'rootDir'}); var fileWatcher = new FileWatcher('rootDir');
return fileWatcher.getWatcher().then(function(watcher) { return fileWatcher.getWatcher().then(function(watcher) {
expect(watcher instanceof Watcher).toBe(true); expect(watcher instanceof Watcher).toBe(true);
}); });
}); });
pit('it should end the watcher', function() { pit('it should end the watcher', function() {
var fileWatcher = new FileWatcher({projectRoot: 'rootDir'}); var fileWatcher = new FileWatcher('rootDir');
Watcher.prototype.close.mockImplementation(function(callback) { Watcher.prototype.close.mockImplementation(function(callback) {
callback(); callback();
}); });

View File

@ -22,15 +22,15 @@ var MAX_WAIT_TIME = 3000;
var memoizedInstances = Object.create(null); var memoizedInstances = Object.create(null);
function FileWatcher(projectConfig) { function FileWatcher(projectRoot) {
if (memoizedInstances[projectConfig.projectRoot]) { if (memoizedInstances[projectRoot]) {
return memoizedInstances[projectConfig.projectRoot]; return memoizedInstances[projectRoot];
} else { } else {
memoizedInstances[projectConfig.projectRoot] = this; memoizedInstances[projectRoot] = this;
} }
this._loadingWatcher = detectingWatcherClass.then(function(Watcher) { this._loadingWatcher = detectingWatcherClass.then(function(Watcher) {
var watcher = new Watcher(projectConfig.projectRoot, {glob: '**/*.js'}); var watcher = new Watcher(projectRoot, {glob: '**/*.js'});
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var rejectTimeout = setTimeout(function() { var rejectTimeout = setTimeout(function() {

View File

@ -4,7 +4,6 @@ var path = require('path');
var version = require('../../package.json').version; var version = require('../../package.json').version;
var tmpdir = require('os').tmpDir(); var tmpdir = require('os').tmpDir();
var pathUtils = require('../fb-path-utils'); var pathUtils = require('../fb-path-utils');
var FileWatcher = require('../FileWatcher');
var fs = require('fs'); var fs = require('fs');
var _ = require('underscore'); var _ = require('underscore');
var q = require('q'); var q = require('q');
@ -34,16 +33,6 @@ function Cache(projectConfig) {
this._persistCache.bind(this), this._persistCache.bind(this),
2000 2000
); );
this._fileWatcher = new FileWatcher(projectConfig);
this._fileWatcher
.getWatcher()
.done(function(watcher) {
watcher.on('all', function(type, filepath) {
var absPath = path.join(projectConfig.projectRoot, filepath);
delete data[absPath];
});
}.bind(this));
} }
Cache.prototype.get = function(filepath, loaderCb) { Cache.prototype.get = function(filepath, loaderCb) {
@ -75,11 +64,14 @@ Cache.prototype._set = function(filepath, loaderPromise) {
}.bind(this)); }.bind(this));
}; };
Cache.prototype.invalidate = function(filepath){
if(this._has(filepath)) {
delete this._data[filepath];
}
}
Cache.prototype.end = function() { Cache.prototype.end = function() {
return q.all([ return this._persistCache();
this._persistCache(),
this._fileWatcher.end()
]);
}; };
Cache.prototype._persistCache = function() { Cache.prototype._persistCache = function() {

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = function (data, callback) {
callback(null, {});
};

View File

@ -19,13 +19,6 @@ describe('JSTransformer Cache', function() {
}); });
Cache = require('../Cache'); Cache = require('../Cache');
var FileWatcher = require('../../FileWatcher');
FileWatcher.prototype.getWatcher = function() {
return q({
on: function() {}
});
};
}); });
describe('getting/settig', function() { describe('getting/settig', function() {
@ -76,41 +69,6 @@ describe('JSTransformer Cache', function() {
}); });
}); });
}); });
pit('it invalidates cache after a file has changed', function() {
require('fs').stat.mockImpl(function(file, callback) {
callback(null, {
mtime: {
getTime: function() {}
}
});
});
var FileWatcher = require('../../FileWatcher');
var triggerChangeFile;
FileWatcher.prototype.getWatcher = function() {
return q({
on: function(type, callback) {
triggerChangeFile = callback;
}
});
};
var cache = new Cache({projectRoot: '/rootDir'});
var loaderCb = jest.genMockFn().mockImpl(function() {
return q('lol');
});
return cache.get('/rootDir/someFile', loaderCb).then(function(value) {
expect(value).toBe('lol');
triggerChangeFile('change', 'someFile');
var loaderCb2 = jest.genMockFn().mockImpl(function() {
return q('lol2');
});
return cache.get('/rootDir/someFile', loaderCb2).then(function(value2) {
expect(value2).toBe('lol2');
});
});
});
}); });
describe('loading cache from disk', function() { describe('loading cache from disk', function() {

View File

@ -25,6 +25,12 @@ Transformer.prototype.kill = function() {
return this._cache.end(); return this._cache.end();
}; };
Transformer.prototype.invalidateFile = function(filePath) {
this._cache.invalidate(filePath);
//TODO: We can read the file and put it into the cache right here
// This would simplify some caching logic as we can be sure that the cache is up to date
}
Transformer.prototype.loadFileAndTransform = function( Transformer.prototype.loadFileAndTransform = function(
transformSets, transformSets,
filePath, filePath,

View File

@ -42,9 +42,11 @@ Package.prototype.finalize = function(options) {
}; };
Package.prototype.getSource = function() { Package.prototype.getSource = function() {
return _.pluck(this._modules, 'transformedCode').join('\n') + '\n' + return this._source || (
'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) + ';\n' + this._source = _.pluck(this._modules, 'transformedCode').join('\n') + '\n' +
'\/\/@ sourceMappingURL=' + this._sourceMapUrl; 'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) +
';\n' + '\/\/@ sourceMappingURL=' + this._sourceMapUrl
);
}; };
Package.prototype.getSourceMap = function(options) { Package.prototype.getSourceMap = function(options) {

View File

@ -97,6 +97,10 @@ Packager.prototype.package = function(main, runModule, sourceMapUrl) {
}); });
}; };
Packager.prototype.invalidateFile = function(filePath){
this._transformer.invalidateFile(filePath);
}
Packager.prototype._transformModule = function(module) { Packager.prototype._transformModule = function(module) {
var resolver = this._resolver; var resolver = this._resolver;
return this._transformer.loadFileAndTransform( return this._transformer.loadFileAndTransform(

View File

@ -0,0 +1,166 @@
'use strict';
jest.dontMock('worker-farm')
.dontMock('q')
.dontMock('os')
.dontMock('errno/custom')
.dontMock('path')
.dontMock('url')
.dontMock('../');
var server = require('../');
var q = require('q');
describe('processRequest', function(){
var server;
var Activity;
var Packager;
var FileWatcher;
var options = {
projectRoot: 'root',
blacklistRE: null,
cacheVersion: null,
polyfillModuleNames: null
};
var makeRequest = function(requestHandler, requrl){
var deferred = q.defer();
requestHandler({
url: requrl
},{
end: function(res){
deferred.resolve(res);
}
},{
next: function(){}
}
);
return deferred.promise;
};
var invalidatorFunc = jest.genMockFunction();
var watcherFunc = jest.genMockFunction();
var requestHandler;
beforeEach(function(){
Activity = require('../../Activity');
Packager = require('../../Packager');
FileWatcher = require('../../FileWatcher')
Packager.prototype.package = function(main, runModule, sourceMapUrl) {
return q({
getSource: function(){
return "this is the source"
},
getSourceMap: function(){
return "this is the source map"
}
})
};
FileWatcher.prototype.getWatcher = function() {
return q({
on: watcherFunc
});
};
Packager.prototype.invalidateFile = invalidatorFunc;
var Server = require('../');
server = new Server(options);
requestHandler = server.processRequest.bind(server);
});
pit('returns JS bundle source on request of *.bundle',function(){
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
return result.then(function(response){
expect(response).toEqual("this is the source");
});
});
pit('returns sourcemap on request of *.map', function(){
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle.map');
return result.then(function(response){
expect(response).toEqual('"this is the source map"');
});
});
pit('watches all files in projectRoot', function(){
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
return result.then(function(response){
expect(watcherFunc.mock.calls[0][0]).toEqual('all');
expect(watcherFunc.mock.calls[0][1]).not.toBe(null);
})
});
describe('file changes', function() {
var triggerFileChange;
beforeEach(function() {
FileWatcher.prototype.getWatcher = function() {
return q({
on: function(eventType, callback) {
if (eventType !== 'all') {
throw new Error('Can only handle "all" event in watcher.');
}
triggerFileChange = callback;
return this;
}
});
};
});
pit('invalides files in package when file is updated', function() {
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
return result.then(function(response){
var onFileChange = watcherFunc.mock.calls[0][1];
onFileChange('all','path/file.js');
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
});
});
pit('rebuilds the packages that contain a file when that file is changed', function() {
var packageFunc = jest.genMockFunction();
packageFunc
.mockReturnValueOnce(
q({
getSource: function(){
return "this is the first source"
},
getSourceMap: function(){},
})
)
.mockReturnValue(
q({
getSource: function(){
return "this is the rebuilt source"
},
getSourceMap: function(){},
})
);
Packager.prototype.package = packageFunc;
var Server = require('../../Server');
var server = new Server(options);
requestHandler = server.processRequest.bind(server);
return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle')
.then(function(response){
expect(response).toEqual("this is the first source");
expect(packageFunc.mock.calls.length).toBe(1);
triggerFileChange('all','path/file.js');
})
.then(function(){
expect(packageFunc.mock.calls.length).toBe(2);
return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle')
.then(function(response){
expect(response).toEqual("this is the rebuilt source");
});
});
});
});
});

128
react-packager/src/Server/index.js vendored Normal file
View File

@ -0,0 +1,128 @@
var url = require('url');
var path = require('path');
var FileWatcher = require('../FileWatcher')
var Packager = require('../Packager');
var Activity = require('../Activity');
var q = require('q');
module.exports = Server;
function Server(options) {
this._projectRoot = options.projectRoot;
this._packages = Object.create(null);
this._packager = new Packager({
projectRoot: options.projectRoot,
blacklistRE: options.blacklistRE,
polyfillModuleNames: options.polyfillModuleNames || [],
runtimeCode: options.runtimeCode,
cacheVersion: options.cacheVersion,
resetCache: options.resetCache,
dev: options.dev,
});
this._fileWatcher = new FileWatcher(options.projectRoot);
var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.getWatcher().done(function(watcher) {
watcher.on('all', onFileChange);
});
}
Server.prototype._onFileChange = function(type, filepath) {
var absPath = path.join(this._projectRoot, filepath);
this._packager.invalidateFile(absPath);
this._rebuildPackages(absPath);
};
Server.prototype._rebuildPackages = function(filepath) {
var buildPackage = this._buildPackage.bind(this);
var packages = this._packages;
Object.keys(packages).forEach(function(key) {
var options = getOptionsFromPath(url.parse(key).pathname);
packages[key] = buildPackage(options).then(function(p) {
// Make a throwaway call to getSource to cache the source string.
p.getSource();
return p;
});
});
};
Server.prototype.kill = function() {
q.all([
this._fileWatcher.end(),
this._packager.kill(),
]);
};
Server.prototype._buildPackage = function(options) {
return this._packager.package(
options.main,
options.runModule,
options.sourceMapUrl
);
};
Server.prototype.buildPackageFromUrl = function(reqUrl) {
var options = getOptionsFromPath(url.parse(reqUrl).pathname);
return this._buildPackage(options);
};
Server.prototype.processRequest = function(req, res, next) {
var requestType;
if (req.url.match(/\.bundle$/)) {
requestType = 'bundle';
} else if (req.url.match(/\.map$/)) {
requestType = 'map';
} else {
return next();
}
var startReqEventId = Activity.startEvent('request:' + req.url);
var options = getOptionsFromPath(url.parse(req.url).pathname);
var building = this._packages[req.url] || this._buildPackage(options)
this._packages[req.url] = building;
building.then(
function(p) {
if (requestType === 'bundle') {
res.end(p.getSource());
Activity.endEvent(startReqEventId);
} else if (requestType === 'map') {
res.end(JSON.stringify(p.getSourceMap()));
Activity.endEvent(startReqEventId);
}
},
function(error) {
handleError(res, error);
}
).done();
};
function getOptionsFromPath(pathname) {
var parts = pathname.split('.');
// Remove the leading slash.
var main = parts[0].slice(1) + '.js';
return {
runModule: parts.slice(1).some(function(part) {
return part === 'runModule';
}),
main: main,
sourceMapUrl: parts.slice(0, -1).join('.') + '.map'
};
}
function handleError(res, error) {
res.writeHead(500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError') {
res.end(JSON.stringify(error));
} 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',
}));
}
}