Updates from Mon Aug 31st.

This commit is contained in:
Spencer Ahrens 2015-08-31 16:31:41 -07:00
commit 0621018da6
17 changed files with 739 additions and 321 deletions

View File

@ -85,6 +85,10 @@ function connectToDebuggerProxy() {
ws.onmessage = function(message) { ws.onmessage = function(message) {
var object = JSON.parse(message.data); var object = JSON.parse(message.data);
if (!object.method) {
return;
}
var sendReply = function(result) { var sendReply = function(result) {
ws.send(JSON.stringify({replyID: object.id, result: result})); ws.send(JSON.stringify({replyID: object.id, result: result}));
}; };

View File

@ -13,6 +13,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const Promise = require('promise'); const Promise = require('promise');
const ProgressBar = require('progress'); const ProgressBar = require('progress');
const BundlesLayout = require('../BundlesLayout');
const Cache = require('../Cache'); const Cache = require('../Cache');
const Transformer = require('../JSTransformer'); const Transformer = require('../JSTransformer');
const DependencyResolver = require('../DependencyResolver'); const DependencyResolver = require('../DependencyResolver');
@ -104,6 +105,13 @@ class Bundler {
cache: this._cache, cache: this._cache,
}); });
this._bundlesLayout = new BundlesLayout({
dependencyResolver: this._resolver,
resetCache: opts.resetCache,
cacheVersion: opts.cacheVersion,
projectRoots: opts.projectRoots,
});
this._transformer = new Transformer({ this._transformer = new Transformer({
projectRoots: opts.projectRoots, projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE, blacklistRE: opts.blacklistRE,
@ -120,6 +128,10 @@ class Bundler {
return this._cache.end(); return this._cache.end();
} }
getLayout(main, isDev) {
return this._bundlesLayout.generateLayout(main, isDev);
}
bundle(main, runModule, sourceMapUrl, isDev, platform) { bundle(main, runModule, sourceMapUrl, isDev, platform) {
const bundle = new Bundle(sourceMapUrl); const bundle = new Bundle(sourceMapUrl);
const findEventId = Activity.startEvent('find dependencies'); const findEventId = Activity.startEvent('find dependencies');

View File

@ -8,31 +8,35 @@
*/ */
'use strict'; 'use strict';
jest jest.dontMock('../index');
.dontMock('../index'); jest.mock('fs');
const Promise = require('promise'); const Promise = require('promise');
describe('BundlesLayout', () => { describe('BundlesLayout', () => {
var BundlesLayout; let BundlesLayout;
var DependencyResolver; let DependencyResolver;
let loadCacheSync;
beforeEach(() => { beforeEach(() => {
BundlesLayout = require('../index'); BundlesLayout = require('../index');
DependencyResolver = require('../../DependencyResolver'); DependencyResolver = require('../../DependencyResolver');
loadCacheSync = require('../../lib/loadCacheSync');
}); });
describe('generate', () => { function newBundlesLayout(options) {
function newBundlesLayout() { return new BundlesLayout(Object.assign({
return new BundlesLayout({ projectRoots: ['/root'],
dependencyResolver: new DependencyResolver(), dependencyResolver: new DependencyResolver(),
}); }, options));
} }
describe('layout', () => {
function isPolyfill() { function isPolyfill() {
return false; return false;
} }
describe('getLayout', () => {
function dep(path) { function dep(path) {
return { return {
path: path, path: path,
@ -58,7 +62,9 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout({resetCache: true})
.getLayout('/root/index.js')
.then(bundles =>
expect(bundles).toEqual({ expect(bundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'], modules: ['/root/index.js', '/root/a.js'],
@ -85,7 +91,9 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout({resetCache: true})
.getLayout('/root/index.js')
.then(bundles =>
expect(bundles).toEqual({ expect(bundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
modules: ['/root/index.js'], modules: ['/root/index.js'],
@ -121,7 +129,9 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout({resetCache: true})
.getLayout('/root/index.js')
.then(bundles =>
expect(bundles).toEqual({ expect(bundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
modules: ['/root/index.js'], modules: ['/root/index.js'],
@ -161,7 +171,9 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout({resetCache: true})
.getLayout('/root/index.js')
.then(bundles =>
expect(bundles).toEqual({ expect(bundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
modules: ['/root/index.js'], modules: ['/root/index.js'],
@ -197,8 +209,10 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then( return newBundlesLayout({resetCache: true})
bundles => expect(bundles).toEqual({ .getLayout('/root/index.js')
.then(bundles =>
expect(bundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'], modules: ['/root/index.js', '/root/a.js'],
children: [{ children: [{
@ -243,8 +257,8 @@ describe('BundlesLayout', () => {
} }
}); });
var layout = newBundlesLayout(); var layout = newBundlesLayout({resetCache: true});
return layout.generateLayout(['/root/index.js']).then(() => { return layout.getLayout('/root/index.js').then(() => {
expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0'); expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0'); expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1'); expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1');
@ -254,3 +268,60 @@ describe('BundlesLayout', () => {
}); });
}); });
}); });
describe('cache', () => {
beforeEach(() => {
loadCacheSync.mockReturnValue({
'/root/index.js': {
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}],
},
'/root/b.js': {
id: 'bundle.2',
modules: ['/root/b.js'],
children: [],
},
});
});
pit('should load layouts', () => {
const layout = newBundlesLayout({ resetCache: false });
return Promise
.all([
layout.getLayout('/root/index.js'),
layout.getLayout('/root/b.js'),
])
.then(([layoutIndex, layoutB]) => {
expect(layoutIndex).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}],
});
expect(layoutB).toEqual({
id: 'bundle.2',
modules: ['/root/b.js'],
children: [],
});
});
});
it('should load moduleToBundle map', () => {
const layout = newBundlesLayout({ resetCache: false });
expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0.1');
expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.2');
});
});
});

View File

@ -75,7 +75,11 @@ describe('BundlesLayout', () => {
assetRoots: ['/root'], assetRoots: ['/root'],
}); });
return new BundlesLayout({dependencyResolver: resolver}); return new BundlesLayout({
dependencyResolver: resolver,
resetCache: true,
projectRoots: ['/root', '/' + __dirname.split('/')[1]],
});
} }
function stripPolyfills(bundle) { function stripPolyfills(bundle) {
@ -114,7 +118,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -140,7 +144,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -166,7 +170,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -201,7 +205,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -242,7 +246,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -282,7 +286,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -323,7 +327,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -370,7 +374,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -408,7 +412,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -446,7 +450,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -480,7 +484,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -512,7 +516,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -539,7 +543,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',
@ -576,7 +580,7 @@ describe('BundlesLayout', () => {
} }
}); });
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles => stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({ expect(resolvedBundles).toEqual({
id: 'bundle.0', id: 'bundle.0',

View File

@ -8,14 +8,33 @@
*/ */
'use strict'; 'use strict';
const Activity = require('../Activity');
const _ = require('underscore'); const _ = require('underscore');
const declareOpts = require('../lib/declareOpts'); const declareOpts = require('../lib/declareOpts');
const fs = require('fs');
const getCacheFilePath = require('../lib/getCacheFilePath');
const loadCacheSync = require('../lib/loadCacheSync');
const version = require('../../../../package.json').version;
const path = require('path');
const validateOpts = declareOpts({ const validateOpts = declareOpts({
dependencyResolver: { dependencyResolver: {
type: 'object', type: 'object',
required: true, required: true,
}, },
resetCache: {
type: 'boolean',
default: false,
},
cacheVersion: {
type: 'string',
default: '1.0',
},
projectRoots: {
type: 'array',
required: true,
},
}); });
const BUNDLE_PREFIX = 'bundle'; const BUNDLE_PREFIX = 'bundle';
@ -29,19 +48,37 @@ class BundlesLayout {
const opts = validateOpts(options); const opts = validateOpts(options);
this._resolver = opts.dependencyResolver; this._resolver = opts.dependencyResolver;
// Cache in which bundle is each module.
this._moduleToBundle = Object.create(null); this._moduleToBundle = Object.create(null);
// Cache the bundles layouts for each entry point. This entries
// are not evicted unless the user explicitly specifies so as
// computing them is pretty expensive
this._layouts = Object.create(null);
// TODO: watch for file creations and removals to update this caches
this._cacheFilePath = this._getCacheFilePath(opts);
if (!opts.resetCache) {
this._loadCacheSync(this._cacheFilePath);
} else {
this._persistCacheEventually();
}
} }
generateLayout(entryPaths, isDev) { getLayout(entryPath, isDev) {
if (this._layouts[entryPath]) {
return this._layouts[entryPath];
}
var currentBundleID = 0; var currentBundleID = 0;
const rootBundle = { const rootBundle = {
id: BUNDLE_PREFIX + '.' + currentBundleID++, id: BUNDLE_PREFIX + '.' + currentBundleID++,
modules: [], modules: [],
children: [], children: [],
}; };
var pending = [{paths: entryPaths, bundle: rootBundle}]; var pending = [{paths: [entryPath], bundle: rootBundle}];
return promiseWhile( this._layouts[entryPath] = promiseWhile(
() => pending.length > 0, () => pending.length > 0,
() => rootBundle, () => rootBundle,
() => { () => {
@ -62,6 +99,9 @@ class BundlesLayout {
if (dependencies.length > 0) { if (dependencies.length > 0) {
bundle.modules = dependencies; bundle.modules = dependencies;
} }
// persist changes to layouts
this._persistCacheEventually();
}, },
index => { index => {
const pendingSyncDep = pendingSyncDeps.shift(); const pendingSyncDep = pendingSyncDeps.shift();
@ -90,11 +130,71 @@ class BundlesLayout {
); );
}, },
); );
return this._layouts[entryPath];
} }
getBundleIDForModule(path) { getBundleIDForModule(path) {
return this._moduleToBundle[path]; return this._moduleToBundle[path];
} }
_loadCacheSync(cachePath) {
const loadCacheId = Activity.startEvent('Loading bundles layout');
const cacheOnDisk = loadCacheSync(cachePath);
// TODO: create single-module bundles for unexistent modules
// TODO: remove modules that no longer exist
Object.keys(cacheOnDisk).forEach(entryPath => {
this._layouts[entryPath] = Promise.resolve(cacheOnDisk[entryPath]);
this._fillModuleToBundleMap(cacheOnDisk[entryPath]);
});
Activity.endEvent(loadCacheId);
}
_fillModuleToBundleMap(bundle) {
bundle.modules.forEach(module => this._moduleToBundle[module] = bundle.id);
bundle.children.forEach(child => this._fillModuleToBundleMap(child));
}
_persistCacheEventually() {
_.debounce(
this._persistCache.bind(this),
2000,
);
}
_persistCache() {
if (this._persisting !== null) {
return this._persisting;
}
this._persisting = Promise
.all(_.values(this._layouts))
.then(bundlesLayout => {
var json = Object.create(null);
Object.keys(this._layouts).forEach((p, i) =>
json[p] = bundlesLayout[i]
);
return Promise.denodeify(fs.writeFile)(
this._cacheFilepath,
JSON.stringify(json),
);
})
.then(() => this._persisting = null);
return this._persisting;
}
_getCacheFilePath(options) {
return getCacheFilePath(
'react-packager-bundles-cache-',
version,
options.projectRoots.join(',').split(path.sep).join('-'),
options.cacheVersion || '0',
);
}
} }
// Runs the body Promise meanwhile the condition callback is satisfied. // Runs the body Promise meanwhile the condition callback is satisfied.

View File

@ -11,7 +11,9 @@
jest jest
.dontMock('underscore') .dontMock('underscore')
.dontMock('absolute-path') .dontMock('absolute-path')
.dontMock('../'); .dontMock('../')
.dontMock('../../lib/loadCacheSync')
.dontMock('../../lib/getCacheFilePath');
jest jest
.mock('os') .mock('os')

View File

@ -8,17 +8,17 @@
*/ */
'use strict'; 'use strict';
var _ = require('underscore'); const Promise = require('promise');
var crypto = require('crypto'); const _ = require('underscore');
var declareOpts = require('../lib/declareOpts'); const declareOpts = require('../lib/declareOpts');
var fs = require('fs'); const fs = require('fs');
var isAbsolutePath = require('absolute-path'); const getCacheFilePath = require('../lib/getCacheFilePath');
var path = require('path'); const isAbsolutePath = require('absolute-path');
var Promise = require('promise'); const loadCacheSync = require('../lib/loadCacheSync');
var tmpdir = require('os').tmpDir(); const path = require('path');
var version = require('../../../../package.json').version; const version = require('../../../../package.json').version;
var validateOpts = declareOpts({ const validateOpts = declareOpts({
resetCache: { resetCache: {
type: 'boolean', type: 'boolean',
default: false, default: false,
@ -164,21 +164,7 @@ class Cache {
_loadCacheSync(cachePath) { _loadCacheSync(cachePath) {
var ret = Object.create(null); var ret = Object.create(null);
if (!fs.existsSync(cachePath)) { var cacheOnDisk = loadCacheSync(cachePath);
return ret;
}
var cacheOnDisk;
try {
cacheOnDisk = JSON.parse(fs.readFileSync(cachePath));
} catch (e) {
if (e instanceof SyntaxError) {
console.warn('Unable to parse cache file. Will clear and continue.');
fs.unlinkSync(cachePath);
return ret;
}
throw e;
}
// Filter outdated cache and convert to promises. // Filter outdated cache and convert to promises.
Object.keys(cacheOnDisk).forEach(key => { Object.keys(cacheOnDisk).forEach(key => {
@ -203,20 +189,13 @@ class Cache {
} }
_getCacheFilePath(options) { _getCacheFilePath(options) {
var hash = crypto.createHash('md5'); return getCacheFilePath(
hash.update(version); 'react-packager-cache-',
version,
var roots = options.projectRoots.join(',').split(path.sep).join('-'); options.projectRoots.join(',').split(path.sep).join('-'),
hash.update(roots); options.cacheVersion || '0',
options.transformModulePath,
var cacheVersion = options.cacheVersion || '0'; );
hash.update(cacheVersion);
hash.update(options.transformModulePath);
var name = 'react-packager-cache-' + hash.digest('hex');
return path.join(tmpdir, name);
} }
} }

View File

@ -23,7 +23,10 @@ const readFile = Promise.denodeify(fs.readFile);
const MAX_CALLS_PER_WORKER = 600; const MAX_CALLS_PER_WORKER = 600;
// Worker will timeout if one of the callers timeout. // Worker will timeout if one of the callers timeout.
const DEFAULT_MAX_CALL_TIME = 60000; const DEFAULT_MAX_CALL_TIME = 120000;
// How may times can we tolerate failures from the worker.
const MAX_RETRIES = 3;
const validateOpts = declareOpts({ const validateOpts = declareOpts({
projectRoots: { projectRoots: {
@ -63,6 +66,7 @@ class Transformer {
maxConcurrentCallsPerWorker: 1, maxConcurrentCallsPerWorker: 1,
maxCallsPerWorker: MAX_CALLS_PER_WORKER, maxCallsPerWorker: MAX_CALLS_PER_WORKER,
maxCallTime: opts.transformTimeoutInterval, maxCallTime: opts.transformTimeoutInterval,
maxRetries: MAX_RETRIES,
}, opts.transformModulePath); }, opts.transformModulePath);
this._transform = Promise.denodeify(this._workers); this._transform = Promise.denodeify(this._workers);
@ -118,6 +122,13 @@ class Transformer {
); );
timeoutErr.type = 'TimeoutError'; timeoutErr.type = 'TimeoutError';
throw timeoutErr; throw timeoutErr;
} else if (err.type === 'ProcessTerminatedError') {
const uncaughtError = new Error(
'Uncaught error in the transformer worker: ' +
this._opts.transformModulePath
);
uncaughtError.type = 'ProcessTerminatedError';
throw uncaughtError;
} }
throw formatError(err, filePath); throw formatError(err, filePath);

View File

@ -13,6 +13,10 @@ const Promise = require('promise');
const bser = require('bser'); const bser = require('bser');
const debug = require('debug')('ReactPackager:SocketClient'); const debug = require('debug')('ReactPackager:SocketClient');
const net = require('net'); const net = require('net');
const path = require('path');
const tmpdir = require('os').tmpdir();
const LOG_PATH = path.join(tmpdir, 'react-packager.log');
class SocketClient { class SocketClient {
static create(sockPath) { static create(sockPath) {
@ -81,7 +85,9 @@ class SocketClient {
delete this._resolvers[message.id]; delete this._resolvers[message.id];
if (message.type === 'error') { if (message.type === 'error') {
resolver.reject(new Error(message.data)); resolver.reject(new Error(
message.data + '\n' + 'See logs ' + LOG_PATH
));
} else { } else {
resolver.resolve(message.data); resolver.resolve(message.data);
} }

View File

@ -32,6 +32,7 @@ class SocketServer {
options options
); );
resolve(this); resolve(this);
process.on('exit', () => fs.unlinkSync(sockPath));
}); });
}); });
this._server.on('connection', (sock) => this._handleConnection(sock)); this._server.on('connection', (sock) => this._handleConnection(sock));
@ -41,8 +42,6 @@ class SocketServer {
this._packagerServer = new Server(options); this._packagerServer = new Server(options);
this._jobs = 0; this._jobs = 0;
this._dieEventually(); this._dieEventually();
process.on('exit', () => fs.unlinkSync(sockPath));
} }
onReady() { onReady() {
@ -72,6 +71,11 @@ class SocketServer {
debug('request error', error); debug('request error', error);
this._jobs--; this._jobs--;
this._reply(sock, m.id, 'error', error.stack); this._reply(sock, m.id, 'error', error.stack);
// Fatal error from JSTransformer transform workers.
if (error.type === 'ProcessTerminatedError') {
setImmediate(() => process.exit(1));
}
}; };
switch (m.type) { switch (m.type) {
@ -138,12 +142,17 @@ class SocketServer {
process.send({ type: 'createdServer' }); process.send({ type: 'createdServer' });
}, },
error => { error => {
debug('error creating server', error.code);
if (error.code === 'EADDRINUSE') { if (error.code === 'EADDRINUSE') {
// Server already listening, this may happen if multiple // Server already listening, this may happen if multiple
// clients where started in quick succussion (buck). // clients where started in quick succussion (buck).
process.send({ type: 'createdServer' }); process.send({ type: 'createdServer' });
// Kill this server because some other server with the same
// config and socket already started.
debug('server already started');
setImmediate(() => process.exit());
} else { } else {
debug('error creating server', error.code);
throw error; throw error;
} }
} }

View File

@ -107,6 +107,6 @@ describe('SocketClient', () => {
data: 'some error' data: 'some error'
}); });
return promise.catch(m => expect(m.message).toBe('some error')); return promise.catch(m => expect(m.message).toContain('some error'));
}); });
}); });

View File

@ -26,6 +26,17 @@ describe('SocketInterface', () => {
pit('creates socket path by hashing options', () => { pit('creates socket path by hashing options', () => {
const fs = require('fs'); const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => true); fs.existsSync = jest.genMockFn().mockImpl(() => true);
fs.unlinkSync = jest.genMockFn();
let callback;
require('child_process').spawn.mockImpl(() => ({
on: (event, cb) => callback = cb,
send: (message) => {
setImmediate(() => callback({ type: 'createdServer' }));
},
unref: () => undefined,
disconnect: () => undefined,
}));
// Check that given two equivelant server options, we end up with the same // Check that given two equivelant server options, we end up with the same
// socket path. // socket path.
@ -49,6 +60,7 @@ describe('SocketInterface', () => {
pit('should fork a server', () => { pit('should fork a server', () => {
const fs = require('fs'); const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => false); fs.existsSync = jest.genMockFn().mockImpl(() => false);
fs.unlinkSync = jest.genMockFn();
let sockPath; let sockPath;
let callback; let callback;

View File

@ -13,12 +13,14 @@ const SocketClient = require('./SocketClient');
const SocketServer = require('./SocketServer'); const SocketServer = require('./SocketServer');
const _ = require('underscore'); const _ = require('underscore');
const crypto = require('crypto'); const crypto = require('crypto');
const debug = require('debug')('ReactPackager:SocketInterface');
const fs = require('fs'); const fs = require('fs');
const net = require('net');
const path = require('path'); const path = require('path');
const tmpdir = require('os').tmpdir(); const tmpdir = require('os').tmpdir();
const {spawn} = require('child_process'); const {spawn} = require('child_process');
const CREATE_SERVER_TIMEOUT = 30000; const CREATE_SERVER_TIMEOUT = 60000;
const SocketInterface = { const SocketInterface = {
getOrCreateSocketFor(options) { getOrCreateSocketFor(options) {
@ -38,10 +40,32 @@ const SocketInterface = {
); );
if (fs.existsSync(sockPath)) { if (fs.existsSync(sockPath)) {
var sock = net.connect(sockPath);
sock.on('connect', () => {
sock.end();
resolve(SocketClient.create(sockPath)); resolve(SocketClient.create(sockPath));
return; });
sock.on('error', (e) => {
try {
debug('deleting socket for not responding', sockPath);
fs.unlinkSync(sockPath);
} catch (err) {
// Another client might have deleted it first.
} }
createServer(resolve, reject, options, sockPath);
});
} else {
createServer(resolve, reject, options, sockPath);
}
});
},
listenOnServerMessages() {
return SocketServer.listenOnServerIPCMessages();
}
};
function createServer(resolve, reject, options, sockPath) {
const logPath = path.join(tmpdir, 'react-packager.log'); const logPath = path.join(tmpdir, 'react-packager.log');
const timeout = setTimeout( const timeout = setTimeout(
@ -78,11 +102,11 @@ const SocketInterface = {
if (m && m.type && m.type === 'createdServer') { if (m && m.type && m.type === 'createdServer') {
clearTimeout(timeout); clearTimeout(timeout);
child.disconnect(); child.disconnect();
resolve(SocketClient.create(sockPath)); resolve(SocketClient.create(sockPath));
} }
}); });
if (options.blacklistRE) { if (options.blacklistRE) {
options.blacklistRE = { source: options.blacklistRE.source }; options.blacklistRE = { source: options.blacklistRE.source };
} }
@ -91,13 +115,6 @@ const SocketInterface = {
type: 'createSocketServer', type: 'createSocketServer',
data: { sockPath, options } data: { sockPath, options }
}); });
});
},
listenOnServerMessages() {
return SocketServer.listenOnServerIPCMessages();
} }
};
module.exports = SocketInterface; module.exports = SocketInterface;

View File

@ -0,0 +1,25 @@
/**
* 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 crypto = require('crypto');
const path = require('path');
const tmpdir = require('os').tmpDir();
function getCacheFilePath(args) {
args = Array.prototype.slice.call(args);
const prefix = args.shift();
let hash = crypto.createHash('md5');
args.forEach(arg => hash.update(arg));
return path.join(tmpdir, prefix + hash.digest('hex'));
}
module.exports = getCacheFilePath;

30
react-packager/src/lib/loadCacheSync.js vendored Normal file
View File

@ -0,0 +1,30 @@
/**
* 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 fs = require('fs');
function loadCacheSync(cachePath) {
if (!fs.existsSync(cachePath)) {
return Object.create(null);
}
try {
return JSON.parse(fs.readFileSync(cachePath));
} catch (e) {
if (e instanceof SyntaxError) {
console.warn('Unable to parse cache file. Will clear and continue.');
fs.unlinkSync(cachePath);
return Object.create(null);
}
throw e;
}
}
module.exports = loadCacheSync;

View File

@ -0,0 +1,71 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @emails oncall+jsinfra
*/
'use strict';
jest.autoMockOff();
jest.mock('../../../BundlesLayout');
const babel = require('babel-core');
const BundlesLayout = require('../../../BundlesLayout');
const testData = {
isolated: {
input: 'System.import("moduleA");',
output: 'loadBundles(["bundle.0"]);'
},
single: {
input: 'System.import("moduleA").then(function (bundleA) {});',
output: 'loadBundles(["bundle.0"]).then(function (bundleA) {});'
},
multiple: {
input: [
'Promise.all([',
'System.import("moduleA"), System.import("moduleB"),',
']).then(function (bundlesA, bundlesB) {});',
].join('\n'),
output: [
'Promise.all([',
'loadBundles(["bundle.0"]), loadBundles(["bundle.1"])',
']).then(function (bundlesA, bundlesB) {});',
].join(''),
},
};
describe('System.import', () => {
let layout = new BundlesLayout();
BundlesLayout.prototype.getBundleIDForModule.mockImpl(module => {
switch (module) {
case 'moduleA': return 'bundle.0';
case 'moduleB': return 'bundle.1';
}
});
function transform(source) {
return babel.transform(source, {
plugins: [require('../')],
blacklist: ['strict'],
extra: { bundlesLayout: layout },
}).code;
}
function test(data) {
// transform and remove new lines
expect(transform(data.input).replace(/(\r\n|\n|\r)/gm,'')).toEqual(data.output);
}
it('should transform isolated `System.import`', () => {
test(testData.isolated);
});
it('should transform single `System.import`', () => {
test(testData.single);
});
it('should transform multiple `System.import`s', () => {
test(testData.multiple);
});
});

View File

@ -0,0 +1,65 @@
/**
* 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.
*
*/
/*jslint node: true */
'use strict';
var t = require('babel-core').types;
/**
* Transforms asynchronous module importing into a function call
* that includes which bundle needs to be loaded
*
* Transforms:
*
* System.import('moduleA')
*
* to:
*
* loadBundles('bundleA')
*/
module.exports = function systemImportTransform(babel) {
return new babel.Transformer('system-import', {
CallExpression: function(node, parent, scope, state) {
if (!isAppropriateSystemImportCall(node, parent)) {
return node;
}
var bundlesLayout = state.opts.extra.bundlesLayout;
var bundleID = bundlesLayout.getBundleIDForModule(
node.arguments[0].value
);
var bundles = bundleID.split('.');
bundles.splice(0, 1);
bundles = bundles.map(function(id) {
return t.literal('bundle.' + id);
});
return t.callExpression(
t.identifier('loadBundles'),
[t.arrayExpression(bundles)]
);
},
metadata: {
group: 'fb'
}
});
};
function isAppropriateSystemImportCall(node) {
return (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'System' &&
node.callee.property.name === 'import' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'Literal'
);
}