Updates from Tue 25 Aug

This commit is contained in:
Martin Konicek 2015-08-25 19:21:59 +01:00
commit 39681a6649
54 changed files with 3784 additions and 2315 deletions

View File

@ -23,7 +23,7 @@ window.localStorage.removeItem('sessionID');
window.onbeforeunload = function() {
if (sessionID) {
return 'If you reload this page, it is going to break the debugging session. ' +
'You should ⌘+R in the iOS simulator to reload.';
'You should press ⌘R in simulator to reload.';
}
};
@ -76,10 +76,10 @@ function connectToDebuggerProxy() {
ws.onopen = function() {
if (sessionID) {
setStatus('Debugger session #' + sessionID + ' active');
setStatus('Debugger session #' + sessionID + ' active.');
ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)}));
} else {
setStatus('Waiting, press ⌘R in simulator to reload and connect');
setStatus('Waiting, press <span class="shortcut">⌘R</span> in simulator to reload and connect.');
}
};
@ -126,7 +126,8 @@ function loadScript(src, callback) {
font-weight: 200;
}
.shortcut {
font-family: monospace;
font-family: "Monaco", monospace;
font-size: medium;
color: #eee;
background-color: #333;
padding: 4px;
@ -175,10 +176,10 @@ function loadScript(src, callback) {
</div>
<div class="content">
<p>
React Native JS code runs inside this Chrome tab
React Native JS code runs inside this Chrome tab.
</p>
<p>Press <span class="shortcut">⌘⌥J</span> to open Developer Tools. Enable <a href="http://stackoverflow.com/a/17324511/232122" target="_blank">Pause On Caught Exceptions</a> for a better debugging experience.</p>
<p>Status: <span id="status">Loading</span></p>
<p>Status: <span id="status">Loading...</span></p>
</div>
</body>
</html>

View File

@ -10,17 +10,23 @@
var chalk = require('chalk');
var exec = require('child_process').exec;
var Activity = require('./react-packager/src/Activity');
var url = require('url');
var Activity = require('./react-packager').Activity;
var hasWarned = {};
var DISABLE_FLOW_CHECK = true; // temporarily disable while we figure out versioning issues.
function getFlowTypeCheckMiddleware(options) {
return function(req, res, next) {
var isBundle = req.url.indexOf('.bundle') !== -1;
if (DISABLE_FLOW_CHECK || options.skipflow || !isBundle) {
var reqObj = url.parse(req.url);
var isFlowCheck = (reqObj.path.match(/^\/flow\//));
if (!isFlowCheck) {
return next();
}
if (options.skipflow) {
_endSkipFlow(res);
return;
}
if (options.flowroot || options.projectRoots.length === 1) {
var flowroot = options.flowroot || options.projectRoots[0];
} else {
@ -28,7 +34,8 @@ function getFlowTypeCheckMiddleware(options) {
hasWarned.noRoot = true;
console.warn('flow: No suitable root');
}
return next();
_endFlowBad(res);
return;
}
exec('command -v flow >/dev/null 2>&1', function(error, stdout) {
if (error) {
@ -37,7 +44,8 @@ function getFlowTypeCheckMiddleware(options) {
console.warn(chalk.yellow('flow: Skipping because not installed. Install with ' +
'`brew install flow`.'));
}
return next();
_endFlowBad(res);
return;
} else {
return doFlowTypecheck(res, flowroot, next);
}
@ -51,7 +59,8 @@ function doFlowTypecheck(res, flowroot, next) {
exec(flowCmd, function(flowError, stdout, stderr) {
Activity.endEvent(eventId);
if (!flowError) {
return next();
_endFlowOk(res);
return;
} else {
try {
var flowResponse = JSON.parse(stdout);
@ -73,16 +82,13 @@ function doFlowTypecheck(res, flowroot, next) {
errorNum++;
});
var error = {
status: 500,
status: 200,
message: 'Flow found type errors. If you think these are wrong, ' +
'make sure your flow bin and .flowconfig are up to date, or ' +
'disable with --skipflow.',
type: 'FlowError',
errors: errors,
};
console.error(chalk.yellow('flow: Error running command `' + flowCmd +
'`:\n' + JSON.stringify(error))
);
res.writeHead(error.status, {
'Content-Type': 'application/json; charset=UTF-8',
});
@ -93,6 +99,13 @@ function doFlowTypecheck(res, flowroot, next) {
hasWarned.noConfig = true;
console.warn(chalk.yellow('flow: ' + stderr));
}
_endFlowBad(res);
} else if (flowError.code === 3) {
if (!hasWarned.timeout) {
hasWarned.timeout = true;
console.warn(chalk.yellow('flow: ' + stdout));
}
_endSkipFlow(res);
} else {
if (!hasWarned.brokenFlow) {
hasWarned.brokenFlow = true;
@ -101,11 +114,37 @@ function doFlowTypecheck(res, flowroot, next) {
'`.\n' + 'stderr: `' + stderr + '`'
));
}
_endFlowBad(res);
}
return next();
return;
}
}
});
}
function _endRes(res, message, code, silentError) {
res.writeHead(code, {
'Content-Type': 'application/json; charset=UTF-8',
});
res.end(JSON.stringify({
message: message,
errors: [],
silentError: silentError,
}));
}
function _endFlowOk(res) {
_endRes(res, 'No Flow Error', '200', true);
}
function _endFlowBad(res) {
// we want to show that flow failed
// status 200 is need for the fetch to not be rejected
_endRes(res, 'Flow failed to run! Please look at the console for more details.', '200', false);
}
function _endSkipFlow(res) {
_endRes(res, 'Flow was skipped, check the server options', '200', true);
}
module.exports = getFlowTypeCheckMiddleware;

View File

@ -1,35 +0,0 @@
/**
* 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.
*/
var EventEmitter = require('events').EventEmitter;
var servers = {};
exports.createServer = function(listener) {
var server = {
_listener: listener,
socket: new EventEmitter(),
listen: function(path) {
listener(this.socket);
servers[path] = this;
}
};
server.socket.setEncoding = function() {};
server.socket.write = function(data) {
this.emit('data', data);
};
return server;
};
exports.connect = function(options) {
var server = servers[options.path || options.port];
return server.socket;
};

View File

@ -1,12 +0,0 @@
/**
* 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.
*
* @providesModule bar
*/
module.exports = setInterval;

View File

@ -1,10 +0,0 @@
{
"port": 3000,
"devPort": 3001,
"publicDir": "./public",
"rootPath": "../example_project",
"moduleOptions": {
"format": "haste",
"main": "index.js"
}
}

View File

@ -1,30 +0,0 @@
/**
* 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.
*
* @providesModule foo
*/
var bar = require('bar');
class Logger {
log() {
console.log('youll have to change me lol');
}
}
class SecretLogger extends Logger {
log(secret) {
console.log('logging ', secret);
}
}
module.exports = (secret) => {
if (secret !== 'secret') throw new Error('wrong secret');
bar(new SecretLogger().log.bind(SecretLogger, secret), 400);
};

View File

@ -1,16 +0,0 @@
/**
* 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.
*
* @providesModule index
*/
require('main');
require('code');
var foo = require('foo');
foo('secret');

View File

@ -1,53 +0,0 @@
/**
* 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.
*
* @providesModule Channel
*/
var XHR = require('XHR');
/**
* Client implementation of a server-push channel.
*
* @see Channel.js for full documentation
*/
var channel = null, at = null, delay = 0;
var Channel = {};
Channel.connect = function() {
var url = '/pull';
if (channel) {
url += '?channel=' + channel + '&at=' + at;
}
XHR.get(url, function(err, xhr) {
if (err) {
delay = Math.min(Math.max(1000, delay * 2), 30000);
} else {
var res = xhr.responseText;
res = JSON.parse(res);
delay = 0;
// Cache channel state
channel = res.channel;
at = res.at;
var messages = res.messages;
messages.forEach(function(message) {
var ev = document.createEvent('CustomEvent');
ev.initCustomEvent(message.event, true, true, message.detail);
window.dispatchEvent(ev);
});
}
// Reconnect
setTimeout(Channel.connect, delay);
});
};
module.exports = Channel;

View File

@ -1,29 +0,0 @@
/**
* 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.
*
* @providesModule XHR
*/
function request(method, url, callback) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr);
} else {
callback(new Error('status = ' + xhr.status, xhr));
}
}
};
xhr.send();
}
exports.get = function(url, callback) {
request('GET', url, callback);
};

View File

@ -1,58 +0,0 @@
/**
* 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.
*
* @providesModule code
*/
var XHR = require('XHR');
var $ = function(sel) {return document.querySelector(sel);};
function getListItems(files) {
var items = [];
files.forEach(function(file) {
var displayName = file.name + (file.type == 1 ? '/' : '');
items.push(
React.DOM.li({
className: 'type' + file.type,
key: file.ino
}, displayName)
);
if (file.type === 1) {
items.push(getListItems(file.nodes));
}
});
return React.DOM.ol(null, items);
}
var FileList = React.createClass({
getInitialState: function() {
return {files: []};
},
componentDidMount: function() {
XHR.get(
this.props.source,
function(err, xhr) {
if (err) {throw err;}
var files = JSON.parse(xhr.responseText);
this.setState({files: files});
}.bind(this)
);
},
render: function() {
return getListItems(this.state.files);
}
});
window.addEventListener('load', function() {
React.render(React.createElement(FileList, {source: '/files'}),
$('#code'));
});

View File

@ -1,64 +0,0 @@
/**
* 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.
*
* @providesModule main
*/
var Channel = require('Channel');
function toArray(arr) {return Array.prototype.slice.apply(arr);}
function $(sel) {return document.querySelector(sel);}
function $$(sel) {return toArray(document.querySelectorAll(sel));}
window.addEventListener('load', function() {
function channelLog() {
var args = Array.prototype.slice.apply(arguments);
var ts = new Date();
var el = document.createElement('li');
args.unshift(ts.getHours() + ':' +
('0' + ts.getMinutes()).substr(0,2) + ':' +
('0' + ts.getSeconds()).substr(0,2));
el.className = 'console-entry';
el.innerHTML = args.join(' ');
$('#console').appendChild(el);
el.scrollIntoView();
}
global.addEventListener('ChannelInit', function(event) {
$('#console').innerHTML = '';
channelLog(event.type);
});
global.addEventListener('ChannelLog', function(event) {
channelLog.apply(null, event.detail);
});
// Tab pane support
function showTab(paneId) {
paneId = paneId.replace(/\W/g, '');
if (paneId) {
$$('#nav-panes > div').forEach(function(pane) {
pane.classList.toggle('active', pane.id === paneId);
});
$$('#nav-tabs li').forEach(function(tab) {
tab.classList.toggle('active',
tab.getAttribute('data-pane') === paneId);
});
global.history.replaceState(null, null, '#' + paneId);
}
}
$('#nav-tabs').onclick = function(e) {
showTab(e.target.getAttribute('data-pane'));
};
// Show current pane
showTab(location.hash);
// Connect to server-push channel
Channel.connect();
});

View File

@ -1,104 +0,0 @@
/**
* 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.
*/
html {
font-family: sans-serif;
}
body {
margin-right: 200px
}
#nav-tabs {
margin: 0;
padding: 0;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
background-color: #eee;
border-bottom: solid 1px black;
font-size: 10pt;
font-weight: bold;
vertical-align: bottom;
line-height: 20px;
height: 29px;
}
#nav-tabs li {
padding: 0 10px;
margin: 0;
border-bottom-width: 0;
display:inline-block;
cursor: pointer;
line-height: 29px;
}
#nav-tabs li:first-child {
color: #666;
}
#nav-tabs li.active {
background-color: #fff;
}
#nav-panes {
position: absolute;
top: 30px;
left: 0px;
right: 0px;
bottom: 0px;
scroll: auto;
overflow: auto;
background-color: #fff;
}
#nav-panes .pane {
display: none;
}
#nav-panes .active {
display: block;
}
.pane {
padding: 10px;
}
#console {
padding-left: 5px;
}
#console li {
font-size: 10pt;
font-family: monospace;
white-space: nowrap;
margin: 0;
list-style: none;
}
#code > ol {
font-size: 10pt;
font-family: monospace;
margin: 0;
padding: 0;
cursor: pointer;
}
#code ol ol {
margin-left: 1em;
padding-left: 1em;
border-left: dashed 1px #ddd;
}
#code li {
color: #000;
font-weight: normal;
list-style: none;
line-height: 1.2em;
}
#code .type1 {
color: #009;
}
#code .type2 {
color: #909;
}

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<!--
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.
-->
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/index.css">
</head>
<body>
<ul id="nav-tabs">
<li data-pane="main">JS App Server</li>
<li data-pane="console">Console</li>
<li data-pane="code">Code</li>
<li data-pane="activity">Activity</li>
</ul>
<div id="nav-panes">
<div id="main" class="pane">
<p>Welcome to the react packager project.</p>
<a href="http://localhost:3000/resource?path=index.js">Get example index.js package</a>
</div>
<div id="console" class="pane"></div>
<div id="code" class="pane"></div>
<div id="activity" class="pane">react packager tasks in progress</div>
</div>
<script src="http://fb.me/react-0.12.0.js"></script>
<script src="http://localhost:3000/resource?path=index.js"></script>
<script type="text/javascript">require('index');</script>
</body>
</html>

View File

@ -16,24 +16,31 @@ useGracefulFs();
var Activity = require('./src/Activity');
var Server = require('./src/Server');
var SocketInterface = require('./src/SocketInterface');
exports.middleware = function(options) {
var server = new Server(options);
return server.processRequest.bind(server);
};
exports.buildPackage = function(options, packageOptions) {
exports.Activity = Activity;
// Renamed "package" to "bundle". But maintain backwards
// compat.
exports.buildPackage =
exports.buildBundle = function(options, bundleOptions) {
var server = createServer(options);
return server.buildPackage(packageOptions)
return server.buildBundle(bundleOptions)
.then(function(p) {
server.end();
return p;
});
};
exports.buildPackageFromUrl = function(options, reqUrl) {
exports.buildPackageFromUrl =
exports.buildBundleFromUrl = function(options, reqUrl) {
var server = createServer(options);
return server.buildPackageFromUrl(reqUrl)
return server.buildBundleFromUrl(reqUrl)
.then(function(p) {
server.end();
return p;
@ -49,17 +56,44 @@ exports.getDependencies = function(options, main) {
});
};
exports.createClientFor = function(options) {
return SocketInterface.getOrCreateSocketFor(options);
};
process.on('message', function(m) {
if (m && m.type && m.type === 'createSocketServer') {
console.log('server got ipc message', m);
var options = m.data.options;
// regexp doesn't naturally serialize to json.
options.blacklistRE = new RegExp(options.blacklistRE.source);
SocketInterface.createSocketServer(
m.data.sockPath,
m.data.options
).then(
function() {
console.log('succesfully created server', m);
process.send({ type: 'createdServer' });
},
function(error) {
console.log('error creating server', error.code);
if (error.code === 'EADDRINUSE') {
// Server already listening, this may happen if multiple
// clients where started in quick succussion (buck).
process.send({ type: 'createdServer' });
} else {
throw error;
}
}
).done();
}
});
function useGracefulFs() {
var fs = require('fs');
var gracefulFs = require('graceful-fs');
// A bit sneaky but it's not straightforward to update all the
// modules we depend on.
Object.keys(fs).forEach(function(method) {
if (typeof fs[method] === 'function' && gracefulFs[method]) {
fs[method] = gracefulFs[method];
}
});
gracefulFs.gracefulify(fs);
}
function createServer(options) {

View File

@ -13,64 +13,63 @@ jest.setMock('chalk', {
dim: function(s) { return s; },
});
describe('Activity', function() {
var Activity;
describe('Activity', () => {
const origConsoleLog = console.log;
let Activity;
var origConsoleLog = console.log;
beforeEach(function() {
beforeEach(() => {
console.log = jest.genMockFn();
Activity = require('../');
jest.runOnlyPendingTimers();
});
afterEach(function() {
afterEach(() => {
console.log = origConsoleLog;
});
describe('startEvent', function() {
it('writes a START event out to the console', function() {
var EVENT_NAME = 'EVENT_NAME';
var DATA = {someData: 42};
describe('startEvent', () => {
it('writes a START event out to the console', () => {
const EVENT_NAME = 'EVENT_NAME';
const DATA = {someData: 42};
Activity.startEvent(EVENT_NAME, DATA);
jest.runOnlyPendingTimers();
expect(console.log.mock.calls.length).toBe(1);
var consoleMsg = console.log.mock.calls[0][0];
const consoleMsg = console.log.mock.calls[0][0];
expect(consoleMsg).toContain('START');
expect(consoleMsg).toContain(EVENT_NAME);
expect(consoleMsg).toContain(JSON.stringify(DATA));
});
});
describe('endEvent', function() {
it('writes an END event out to the console', function() {
var EVENT_NAME = 'EVENT_NAME';
var DATA = {someData: 42};
describe('endEvent', () => {
it('writes an END event out to the console', () => {
const EVENT_NAME = 'EVENT_NAME';
const DATA = {someData: 42};
var eventID = Activity.startEvent(EVENT_NAME, DATA);
const eventID = Activity.startEvent(EVENT_NAME, DATA);
Activity.endEvent(eventID);
jest.runOnlyPendingTimers();
expect(console.log.mock.calls.length).toBe(2);
var consoleMsg = console.log.mock.calls[1][0];
const consoleMsg = console.log.mock.calls[1][0];
expect(consoleMsg).toContain('END');
expect(consoleMsg).toContain(EVENT_NAME);
expect(consoleMsg).toContain(JSON.stringify(DATA));
});
it('throws when called with an invalid eventId', function() {
expect(function() {
Activity.endEvent(42);
}).toThrow('event(42) is not a valid event id!');
it('throws when called with an invalid eventId', () => {
expect(() => Activity.endEvent(42)).toThrow(
'event(42) is not a valid event id!',
);
});
it('throws when called with an expired eventId', function() {
var eid = Activity.startEvent('', '');
it('throws when called with an expired eventId', () => {
const eid = Activity.startEvent('', '');
Activity.endEvent(eid);
expect(function() {
expect(() => {
Activity.endEvent(eid);
}).toThrow('event(3) has already ended!');
@ -78,17 +77,16 @@ describe('Activity', function() {
});
});
describe('signal', function() {
it('writes a SIGNAL event out to the console', function() {
var EVENT_NAME = 'EVENT_NAME';
var DATA = {someData: 42};
describe('signal', () => {
it('writes a SIGNAL event out to the console', () => {
const EVENT_NAME = 'EVENT_NAME';
const DATA = {someData: 42};
Activity.signal(EVENT_NAME, DATA);
jest.runOnlyPendingTimers();
expect(console.log.mock.calls.length).toBe(1);
var consoleMsg = console.log.mock.calls[0][0];
const consoleMsg = console.log.mock.calls[0][0];
expect(consoleMsg).toContain(EVENT_NAME);
expect(consoleMsg).toContain(JSON.stringify(DATA));
});

View File

@ -8,19 +8,22 @@
*/
'use strict';
var chalk = require('chalk');
const chalk = require('chalk');
const events = require('events');
var COLLECTION_PERIOD = 1000;
const COLLECTION_PERIOD = 1000;
var _endedEvents = Object.create(null);
var _eventStarts = Object.create(null);
var _queuedActions = [];
var _scheduledCollectionTimer = null;
var _uuid = 1;
var _enabled = true;
const _endedEvents = Object.create(null);
const _eventStarts = Object.create(null);
const _queuedActions = [];
const _eventEmitter = new events.EventEmitter();
let _scheduledCollectionTimer = null;
let _uuid = 1;
let _enabled = true;
function endEvent(eventId) {
var eventEndTime = Date.now();
const eventEndTime = Date.now();
if (!_eventStarts[eventId]) {
_throw('event(' + eventId + ') is not a valid event id!');
@ -39,7 +42,7 @@ function endEvent(eventId) {
}
function signal(eventName, data) {
var signalTime = Date.now();
const signalTime = Date.now();
if (eventName == null) {
_throw('No event name specified');
@ -58,7 +61,7 @@ function signal(eventName, data) {
}
function startEvent(eventName, data) {
var eventStartTime = Date.now();
const eventStartTime = Date.now();
if (eventName == null) {
_throw('No event name specified');
@ -68,8 +71,8 @@ function startEvent(eventName, data) {
data = null;
}
var eventId = _uuid++;
var action = {
const eventId = _uuid++;
const action = {
action: 'startEvent',
data: data,
eventId: eventId,
@ -88,7 +91,7 @@ function disable() {
function _runCollection() {
/* jshint -W084 */
var action;
let action;
while ((action = _queuedActions.shift())) {
_writeAction(action);
}
@ -98,6 +101,7 @@ function _runCollection() {
function _scheduleAction(action) {
_queuedActions.push(action);
_eventEmitter.emit(action.action, action);
if (_scheduledCollectionTimer === null) {
_scheduledCollectionTimer = setTimeout(_runCollection, COLLECTION_PERIOD);
@ -114,10 +118,10 @@ function _scheduleAction(action) {
* won't be adding such a non-trivial optimization anytime soon)
*/
function _throw(msg) {
var err = new Error(msg);
const err = new Error(msg);
// Strip off the call to _throw()
var stack = err.stack.split('\n');
const stack = err.stack.split('\n');
stack.splice(1, 1);
err.stack = stack.join('\n');
@ -129,8 +133,8 @@ function _writeAction(action) {
return;
}
var data = action.data ? ': ' + JSON.stringify(action.data) : '';
var fmtTime = new Date(action.tstamp).toLocaleTimeString();
const data = action.data ? ': ' + JSON.stringify(action.data) : '';
const fmtTime = new Date(action.tstamp).toLocaleTimeString();
switch (action.action) {
case 'startEvent':
@ -142,8 +146,8 @@ function _writeAction(action) {
break;
case 'endEvent':
var startAction = _eventStarts[action.eventId];
var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : '';
const startAction = _eventStarts[action.eventId];
const startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : '';
console.log(chalk.dim(
'[' + fmtTime + '] ' +
'<END> ' + startAction.eventName +
@ -171,3 +175,4 @@ exports.endEvent = endEvent;
exports.signal = signal;
exports.startEvent = startEvent;
exports.disable = disable;
exports.eventEmitter = _eventEmitter;

View File

@ -8,22 +8,22 @@ jest
.mock('crypto')
.mock('fs');
var Promise = require('promise');
const Promise = require('promise');
describe('AssetServer', function() {
var AssetServer;
var crypto;
var fs;
describe('AssetServer', () => {
let AssetServer;
let crypto;
let fs;
beforeEach(function() {
beforeEach(() => {
AssetServer = require('../');
crypto = require('crypto');
fs = require('fs');
});
describe('assetServer.get', function() {
pit('should work for the simple case', function() {
var server = new AssetServer({
describe('assetServer.get', () => {
pit('should work for the simple case', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
@ -40,15 +40,15 @@ describe('AssetServer', function() {
return Promise.all([
server.get('imgs/b.png'),
server.get('imgs/b@1x.png'),
]).then(function(resp) {
resp.forEach(function(data) {
expect(data).toBe('b image');
});
});
]).then(resp =>
resp.forEach(data =>
expect(data).toBe('b image')
)
);
});
pit('should work for the simple case with jpg', function() {
var server = new AssetServer({
pit('should work for the simple case with jpg', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpg'],
});
@ -65,16 +65,16 @@ describe('AssetServer', function() {
return Promise.all([
server.get('imgs/b.jpg'),
server.get('imgs/b.png'),
]).then(function(data) {
]).then(data =>
expect(data).toEqual([
'jpeg image',
'png image',
]);
});
])
);
});
pit('should pick the bigger one', function() {
var server = new AssetServer({
pit('should pick the bigger one', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
@ -90,13 +90,13 @@ describe('AssetServer', function() {
}
});
return server.get('imgs/b@3x.png').then(function(data) {
expect(data).toBe('b4 image');
});
return server.get('imgs/b@3x.png').then(data =>
expect(data).toBe('b4 image')
);
});
pit('should support multiple project roots', function() {
var server = new AssetServer({
pit('should support multiple project roots', () => {
const server = new AssetServer({
projectRoots: ['/root', '/root2'],
assetExts: ['png'],
});
@ -116,27 +116,23 @@ describe('AssetServer', function() {
},
});
return server.get('newImages/imgs/b.png').then(function(data) {
expect(data).toBe('b1 image');
});
return server.get('newImages/imgs/b.png').then(data =>
expect(data).toBe('b1 image')
);
});
});
describe('assetSerer.getAssetData', function() {
pit('should get assetData', function() {
var hash = {
describe('assetSerer.getAssetData', () => {
pit('should get assetData', () => {
const hash = {
update: jest.genMockFn(),
digest: jest.genMockFn(),
};
hash.digest.mockImpl(function() {
return 'wow such hash';
});
crypto.createHash.mockImpl(function() {
return hash;
});
hash.digest.mockImpl(() => 'wow such hash');
crypto.createHash.mockImpl(() => hash);
var server = new AssetServer({
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
@ -152,7 +148,7 @@ describe('AssetServer', function() {
}
});
return server.getAssetData('imgs/b.png').then(function(data) {
return server.getAssetData('imgs/b.png').then(data => {
expect(hash.update.mock.calls.length).toBe(4);
expect(data).toEqual({
type: 'png',
@ -163,20 +159,16 @@ describe('AssetServer', function() {
});
});
pit('should get assetData for non-png images', function() {
var hash = {
pit('should get assetData for non-png images', () => {
const hash = {
update: jest.genMockFn(),
digest: jest.genMockFn(),
};
hash.digest.mockImpl(function() {
return 'wow such hash';
});
crypto.createHash.mockImpl(function() {
return hash;
});
hash.digest.mockImpl(() => 'wow such hash');
crypto.createHash.mockImpl(() => hash);
var server = new AssetServer({
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpeg'],
});
@ -192,7 +184,7 @@ describe('AssetServer', function() {
}
});
return server.getAssetData('imgs/b.jpg').then(function(data) {
return server.getAssetData('imgs/b.jpg').then(data => {
expect(hash.update.mock.calls.length).toBe(4);
expect(data).toEqual({
type: 'jpg',

View File

@ -8,20 +8,20 @@
*/
'use strict';
var declareOpts = require('../lib/declareOpts');
var getAssetDataFromName = require('../lib/getAssetDataFromName');
var path = require('path');
var Promise = require('promise');
var fs = require('fs');
var crypto = require('crypto');
const Promise = require('promise');
var stat = Promise.denodeify(fs.stat);
var readDir = Promise.denodeify(fs.readdir);
var readFile = Promise.denodeify(fs.readFile);
const crypto = require('crypto');
const declareOpts = require('../lib/declareOpts');
const fs = require('fs');
const getAssetDataFromName = require('../lib/getAssetDataFromName');
const path = require('path');
module.exports = AssetServer;
const stat = Promise.denodeify(fs.stat);
const readDir = Promise.denodeify(fs.readdir);
const readFile = Promise.denodeify(fs.readFile);
var validateOpts = declareOpts({
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
@ -32,135 +32,136 @@ var validateOpts = declareOpts({
},
});
function AssetServer(options) {
var opts = validateOpts(options);
this._roots = opts.projectRoots;
this._assetExts = opts.assetExts;
}
class AssetServer {
constructor(options) {
const opts = validateOpts(options);
this._roots = opts.projectRoots;
this._assetExts = opts.assetExts;
}
/**
* Given a request for an image by path. That could contain a resolution
* postfix, we need to find that image (or the closest one to it's resolution)
* in one of the project roots:
*
* 1. We first parse the directory of the asset
* 2. We check to find a matching directory in one of the project roots
* 3. We then build a map of all assets and their scales in this directory
* 4. Then pick the closest resolution (rounding up) to the requested one
*/
AssetServer.prototype._getAssetRecord = function(assetPath) {
var filename = path.basename(assetPath);
return findRoot(
this._roots,
path.dirname(assetPath)
).then(function(dir) {
return Promise.all([
dir,
readDir(dir),
]);
}).then(function(res) {
var dir = res[0];
var files = res[1];
var assetData = getAssetDataFromName(filename);
var map = buildAssetMap(dir, files);
var record = map[assetData.assetName];
if (!record) {
throw new Error('Asset not found');
}
return record;
});
};
AssetServer.prototype.get = function(assetPath) {
var assetData = getAssetDataFromName(assetPath);
return this._getAssetRecord(assetPath).then(function(record) {
for (var i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
get(assetPath) {
const assetData = getAssetDataFromName(assetPath);
return this._getAssetRecord(assetPath).then(record => {
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
}
return readFile(record.files[record.files.length - 1]);
});
};
return readFile(record.files[record.files.length - 1]);
});
}
AssetServer.prototype.getAssetData = function(assetPath) {
var nameData = getAssetDataFromName(assetPath);
var data = {
name: nameData.name,
type: nameData.type,
};
getAssetData(assetPath) {
const nameData = getAssetDataFromName(assetPath);
const data = {
name: nameData.name,
type: nameData.type,
};
return this._getAssetRecord(assetPath).then(function(record) {
data.scales = record.scales;
return this._getAssetRecord(assetPath).then(record => {
data.scales = record.scales;
return Promise.all(
record.files.map(function(file) {
return stat(file);
return Promise.all(
record.files.map(file => stat(file))
);
}).then(stats => {
const hash = crypto.createHash('md5');
stats.forEach(fstat =>
hash.update(fstat.mtime.getTime().toString())
);
data.hash = hash.digest('hex');
return data;
});
}
/**
* Given a request for an image by path. That could contain a resolution
* postfix, we need to find that image (or the closest one to it's resolution)
* in one of the project roots:
*
* 1. We first parse the directory of the asset
* 2. We check to find a matching directory in one of the project roots
* 3. We then build a map of all assets and their scales in this directory
* 4. Then pick the closest resolution (rounding up) to the requested one
*/
_getAssetRecord(assetPath) {
const filename = path.basename(assetPath);
return (
this._findRoot(
this._roots,
path.dirname(assetPath)
)
.then(dir => Promise.all([
dir,
readDir(dir),
]))
.then(res => {
const dir = res[0];
const files = res[1];
const assetData = getAssetDataFromName(filename);
const map = this._buildAssetMap(dir, files);
const record = map[assetData.assetName];
if (!record) {
throw new Error('Asset not found');
}
return record;
})
);
}).then(function(stats) {
var hash = crypto.createHash('md5');
}
stats.forEach(function(fstat) {
hash.update(fstat.mtime.getTime().toString());
});
data.hash = hash.digest('hex');
return data;
});
};
function findRoot(roots, dir) {
return Promise.all(
roots.map(function(root) {
var absPath = path.join(root, dir);
return stat(absPath).then(function(fstat) {
return {path: absPath, isDirectory: fstat.isDirectory()};
}, function (err) {
return {path: absPath, isDirectory: false};
});
})
).then(
function(stats) {
for (var i = 0; i < stats.length; i++) {
_findRoot(roots, dir) {
return Promise.all(
roots.map(root => {
const absPath = path.join(root, dir);
return stat(absPath).then(fstat => {
return {path: absPath, isDirectory: fstat.isDirectory()};
}, err => {
return {path: absPath, isDirectory: false};
});
})
).then(stats => {
for (let i = 0; i < stats.length; i++) {
if (stats[i].isDirectory) {
return stats[i].path;
}
}
throw new Error('Could not find any directories');
}
);
}
});
}
function buildAssetMap(dir, files) {
var assets = files.map(getAssetDataFromName);
var map = Object.create(null);
assets.forEach(function(asset, i) {
var file = files[i];
var record = map[asset.assetName];
if (!record) {
record = map[asset.assetName] = {
scales: [],
files: [],
};
}
var insertIndex;
var length = record.scales.length;
for (insertIndex = 0; insertIndex < length; insertIndex++) {
if (asset.resolution < record.scales[insertIndex]) {
break;
_buildAssetMap(dir, files) {
const assets = files.map(getAssetDataFromName);
const map = Object.create(null);
assets.forEach(function(asset, i) {
const file = files[i];
let record = map[asset.assetName];
if (!record) {
record = map[asset.assetName] = {
scales: [],
files: [],
};
}
}
record.scales.splice(insertIndex, 0, asset.resolution);
record.files.splice(insertIndex, 0, path.join(dir, file));
});
return map;
let insertIndex;
const length = record.scales.length;
for (insertIndex = 0; insertIndex < length; insertIndex++) {
if (asset.resolution < record.scales[insertIndex]) {
break;
}
}
record.scales.splice(insertIndex, 0, asset.resolution);
record.files.splice(insertIndex, 0, path.join(dir, file));
});
return map;
}
}
module.exports = AssetServer;

337
react-packager/src/Bundler/Bundle.js vendored Normal file
View File

@ -0,0 +1,337 @@
/**
* 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 _ = require('underscore');
const base64VLQ = require('./base64-vlq');
const UglifyJS = require('uglify-js');
const ModuleTransport = require('../lib/ModuleTransport');
const SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL=';
class Bundle {
constructor(sourceMapUrl) {
this._finalized = false;
this._modules = [];
this._assets = [];
this._sourceMapUrl = sourceMapUrl;
this._shouldCombineSourceMaps = false;
}
setMainModuleId(moduleId) {
this._mainModuleId = moduleId;
}
addModule(module) {
if (!(module instanceof ModuleTransport)) {
throw new Error('Expeceted a ModuleTransport object');
}
// If we get a map from the transformer we'll switch to a mode
// were we're combining the source maps as opposed to
if (!this._shouldCombineSourceMaps && module.map != null) {
this._shouldCombineSourceMaps = true;
}
this._modules.push(module);
}
getModules() {
return this._modules;
}
addAsset(asset) {
this._assets.push(asset);
}
finalize(options) {
options = options || {};
if (options.runMainModule) {
const runCode = ';require("' + this._mainModuleId + '");';
this.addModule(new ModuleTransport({
code: runCode,
virtual: true,
sourceCode: runCode,
sourcePath: 'RunMainModule.js'
}));
}
Object.freeze(this._modules);
Object.seal(this._modules);
Object.freeze(this._assets);
Object.seal(this._assets);
this._finalized = true;
}
_assertFinalized() {
if (!this._finalized) {
throw new Error('Bundle needs to be finalized before getting any source');
}
}
_getSource() {
if (this._source == null) {
this._source = _.pluck(this._modules, 'code').join('\n');
}
return this._source;
}
_getInlineSourceMap() {
if (this._inlineSourceMap == null) {
const sourceMap = this.getSourceMap({excludeSource: true});
/*eslint-env node*/
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
}
return this._inlineSourceMap;
}
getSource(options) {
this._assertFinalized();
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap().code;
}
let source = this._getSource();
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap();
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
return source;
}
getMinifiedSourceAndMap() {
this._assertFinalized();
const source = this._getSource();
try {
return UglifyJS.minify(source, {
fromString: true,
outSourceMap: 'bundle.js',
inSourceMap: this.getSourceMap(),
});
} catch(e) {
// Sometimes, when somebody is using a new syntax feature that we
// don't yet have transform for, the untransformed line is sent to
// uglify, and it chokes on it. This code tries to print the line
// and the module for easier debugging
let errorMessage = 'Error while minifying JS\n';
if (e.line) {
errorMessage += 'Transformed code line: "' +
source.split('\n')[e.line - 1] + '"\n';
}
if (e.pos) {
let fromIndex = source.lastIndexOf('__d(\'', e.pos);
if (fromIndex > -1) {
fromIndex += '__d(\''.length;
const toIndex = source.indexOf('\'', fromIndex);
errorMessage += 'Module name (best guess): ' +
source.substring(fromIndex, toIndex) + '\n';
}
}
errorMessage += e.toString();
throw new Error(errorMessage);
}
}
/**
* I found a neat trick in the sourcemap spec that makes it easy
* to concat sourcemaps. The `sections` field allows us to combine
* the sourcemap easily by adding an offset. Tested on chrome.
* Seems like it's not yet in Firefox but that should be fine for
* now.
*/
_getCombinedSourceMaps(options) {
const result = {
version: 3,
file: 'bundle.js',
sections: [],
};
let line = 0;
this._modules.forEach(function(module) {
let map = module.map;
if (module.virtual) {
map = generateSourceMapForVirtualModule(module);
}
if (options.excludeSource) {
map = _.extend({}, map, {sourcesContent: []});
}
result.sections.push({
offset: { line: line, column: 0 },
map: map,
});
line += module.code.split('\n').length;
});
return result;
}
getSourceMap(options) {
this._assertFinalized();
options = options || {};
if (this._shouldCombineSourceMaps) {
return this._getCombinedSourceMaps(options);
}
const mappings = this._getMappings();
const map = {
file: 'bundle.js',
sources: _.pluck(this._modules, 'sourcePath'),
version: 3,
names: [],
mappings: mappings,
sourcesContent: options.excludeSource
? [] : _.pluck(this._modules, 'sourceCode')
};
return map;
}
getAssets() {
return this._assets;
}
_getMappings() {
const modules = this._modules;
// The first line mapping in our package is basically the base64vlq code for
// zeros (A).
const firstLine = 'AAAA';
// Most other lines in our mappings are all zeros (for module, column etc)
// except for the lineno mappinp: curLineno - prevLineno = 1; Which is C.
const line = 'AACA';
const moduleLines = Object.create(null);
let mappings = '';
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const code = module.code;
let lastCharNewLine = false;
moduleLines[module.sourcePath] = 0;
for (let t = 0; t < code.length; t++) {
if (t === 0 && i === 0) {
mappings += firstLine;
} else if (t === 0) {
mappings += 'AC';
// This is the only place were we actually don't know the mapping ahead
// of time. When it's a new module (and not the first) the lineno
// mapping is 0 (current) - number of lines in prev module.
mappings += base64VLQ.encode(
0 - moduleLines[modules[i - 1].sourcePath]
);
mappings += 'A';
} else if (lastCharNewLine) {
moduleLines[module.sourcePath]++;
mappings += line;
}
lastCharNewLine = code[t] === '\n';
if (lastCharNewLine) {
mappings += ';';
}
}
if (i !== modules.length - 1) {
mappings += ';';
}
}
return mappings;
}
getJSModulePaths() {
return this._modules.filter(function(module) {
// Filter out non-js files. Like images etc.
return !module.virtual;
}).map(function(module) {
return module.sourcePath;
});
}
getDebugInfo() {
return [
'<div><h3>Main Module:</h3> ' + this._mainModuleId + '</div>',
'<style>',
'pre.collapsed {',
' height: 10px;',
' width: 100px;',
' display: block;',
' text-overflow: ellipsis;',
' overflow: hidden;',
' cursor: pointer;',
'}',
'</style>',
'<h3> Module paths and transformed code: </h3>',
this._modules.map(function(m) {
return '<div> <h4> Path: </h4>' + m.sourcePath + '<br/> <h4> Source: </h4>' +
'<code><pre class="collapsed" onclick="this.classList.remove(\'collapsed\')">' +
_.escape(m.code) + '</pre></code></div>';
}).join('\n'),
].join('\n');
}
toJSON() {
if (!this._finalized) {
throw new Error('Cannot serialize bundle unless finalized');
}
return {
modules: this._modules,
assets: this._assets,
sourceMapUrl: this._sourceMapUrl,
shouldCombineSourceMaps: this._shouldCombineSourceMaps,
mainModuleId: this._mainModuleId,
};
}
static fromJSON(json) {
const bundle = new Bundle(json.sourceMapUrl);
bundle._mainModuleId = json.mainModuleId;
bundle._assets = json.assets;
bundle._modules = json.modules;
bundle._sourceMapUrl = json.sourceMapUrl;
Object.freeze(bundle._modules);
Object.seal(bundle._modules);
Object.freeze(bundle._assets);
Object.seal(bundle._assets);
bundle._finalized = true;
return bundle;
}
}
function generateSourceMapForVirtualModule(module) {
// All lines map 1-to-1
let mappings = 'AAAA;';
for (let i = 1; i < module.code.split('\n').length; i++) {
mappings += 'AACA;';
}
return {
version: 3,
sources: [ module.sourcePath ],
names: [],
mappings: mappings,
file: module.sourcePath,
sourcesContent: [ module.sourceCode ],
};
}
module.exports = Bundle;

View File

@ -12,35 +12,35 @@ jest.autoMockOff();
var SourceMapGenerator = require('source-map').SourceMapGenerator;
describe('Package', function() {
describe('Bundle', function() {
var ModuleTransport;
var Package;
var ppackage;
var Bundle;
var bundle;
beforeEach(function() {
Package = require('../Package');
Bundle = require('../Bundle');
ModuleTransport = require('../../lib/ModuleTransport');
ppackage = new Package('test_url');
ppackage.getSourceMap = jest.genMockFn().mockImpl(function() {
bundle = new Bundle('test_url');
bundle.getSourceMap = jest.genMockFn().mockImpl(function() {
return 'test-source-map';
});
});
describe('source package', function() {
it('should create a package and get the source', function() {
ppackage.addModule(new ModuleTransport({
describe('source bundle', function() {
it('should create a bundle and get the source', function() {
bundle.addModule(new ModuleTransport({
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path',
}));
ppackage.addModule(new ModuleTransport({
bundle.addModule(new ModuleTransport({
code: 'transformed bar;',
sourceCode: 'source bar',
sourcePath: 'bar path',
}));
ppackage.finalize({});
expect(ppackage.getSource()).toBe([
bundle.finalize({});
expect(bundle.getSource()).toBe([
'transformed foo;',
'transformed bar;',
'\/\/@ sourceMappingURL=test_url'
@ -48,7 +48,7 @@ describe('Package', function() {
});
it('should be ok to leave out the source map url', function() {
var p = new Package();
var p = new Bundle();
p.addModule(new ModuleTransport({
code: 'transformed foo;',
sourceCode: 'source foo',
@ -67,22 +67,22 @@ describe('Package', function() {
].join('\n'));
});
it('should create a package and add run module code', function() {
ppackage.addModule(new ModuleTransport({
it('should create a bundle and add run module code', function() {
bundle.addModule(new ModuleTransport({
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path'
}));
ppackage.addModule(new ModuleTransport({
bundle.addModule(new ModuleTransport({
code: 'transformed bar;',
sourceCode: 'source bar',
sourcePath: 'bar path'
}));
ppackage.setMainModuleId('foo');
ppackage.finalize({runMainModule: true});
expect(ppackage.getSource()).toBe([
bundle.setMainModuleId('foo');
bundle.finalize({runMainModule: true});
expect(bundle.getSource()).toBe([
'transformed foo;',
'transformed bar;',
';require("foo");',
@ -100,19 +100,19 @@ describe('Package', function() {
return minified;
};
ppackage.addModule(new ModuleTransport({
bundle.addModule(new ModuleTransport({
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path'
}));
ppackage.finalize();
expect(ppackage.getMinifiedSourceAndMap()).toBe(minified);
bundle.finalize();
expect(bundle.getMinifiedSourceAndMap()).toBe(minified);
});
});
describe('sourcemap package', function() {
describe('sourcemap bundle', function() {
it('should create sourcemap', function() {
var p = new Package('test_url');
var p = new Bundle('test_url');
p.addModule(new ModuleTransport({
code: [
'transformed foo',
@ -143,11 +143,11 @@ describe('Package', function() {
p.setMainModuleId('foo');
p.finalize({runMainModule: true});
var s = p.getSourceMap();
expect(s).toEqual(genSourceMap(p._modules));
expect(s).toEqual(genSourceMap(p.getModules()));
});
it('should combine sourcemaps', function() {
var p = new Package('test_url');
var p = new Bundle('test_url');
p.addModule(new ModuleTransport({
code: 'transformed foo;\n',
@ -215,7 +215,7 @@ describe('Package', function() {
describe('getAssets()', function() {
it('should save and return asset objects', function() {
var p = new Package('test_url');
var p = new Bundle('test_url');
var asset1 = {};
var asset2 = {};
p.addAsset(asset1);
@ -227,7 +227,7 @@ describe('Package', function() {
describe('getJSModulePaths()', function() {
it('should return module paths', function() {
var p = new Package('test_url');
var p = new Bundle('test_url');
p.addModule(new ModuleTransport({
code: 'transformed foo;\n',
sourceCode: 'source foo',
@ -248,7 +248,7 @@ describe('Package', function() {
function genSourceMap(modules) {
var sourceMapGen = new SourceMapGenerator({file: 'bundle.js', version: 3});
var packageLineNo = 0;
var bundleLineNo = 0;
for (var i = 0; i < modules.length; i++) {
var module = modules[i];
var transformedCode = module.code;
@ -259,7 +259,7 @@ describe('Package', function() {
for (var t = 0; t < transformedCode.length; t++) {
if (t === 0 || lastCharNewLine) {
sourceMapGen.addMapping({
generated: {line: packageLineNo + 1, column: 0},
generated: {line: bundleLineNo + 1, column: 0},
original: {line: transformedLineCount + 1, column: 0},
source: sourcePath
});
@ -267,10 +267,10 @@ describe('Package', function() {
lastCharNewLine = transformedCode[t] === '\n';
if (lastCharNewLine) {
transformedLineCount++;
packageLineNo++;
bundleLineNo++;
}
}
packageLineNo++;
bundleLineNo++;
sourceMapGen.setSourceContent(
sourcePath,
sourceCode

View File

@ -9,7 +9,7 @@
'use strict';
jest
.setMock('worker-farm', function() { return function() {};})
.setMock('worker-farm', () => () => undefined)
.dontMock('underscore')
.dontMock('../../lib/ModuleTransport')
.setMock('uglify-js')
@ -20,13 +20,14 @@ jest.mock('fs');
var Promise = require('promise');
describe('Packager', function() {
describe('Bundler', function() {
var getDependencies;
var wrapModule;
var Packager;
var packager;
var Bundler;
var bundler;
var assetServer;
var modules;
var ProgressBar;
beforeEach(function() {
getDependencies = jest.genMockFn();
@ -38,7 +39,7 @@ describe('Packager', function() {
};
});
Packager = require('../');
Bundler = require('../');
require('fs').statSync.mockImpl(function() {
return {
@ -50,38 +51,61 @@ describe('Packager', function() {
callback(null, '{"json":true}');
});
ProgressBar = require('progress');
assetServer = {
getAssetData: jest.genMockFn(),
};
packager = new Packager({
bundler = new Bundler({
projectRoots: ['/root'],
assetServer: assetServer,
});
function createModule({
path,
id,
dependencies,
isAsset,
isAsset_DEPRECATED,
isJSON,
resolution,
}) {
return {
path,
resolution,
getDependencies() { return Promise.resolve(dependencies); },
getName() { return Promise.resolve(id); },
isJSON() { return isJSON; },
isAsset() { return isAsset; },
isAsset_DEPRECATED() { return isAsset_DEPRECATED; },
};
}
modules = [
{id: 'foo', path: '/root/foo.js', dependencies: []},
{id: 'bar', path: '/root/bar.js', dependencies: []},
{
id: 'image!img',
createModule({id: 'foo', path: '/root/foo.js', dependencies: []}),
createModule({id: 'bar', path: '/root/bar.js', dependencies: []}),
createModule({
path: '/root/img/img.png',
id: 'image!img',
isAsset_DEPRECATED: true,
dependencies: [],
resolution: 2,
},
{
}),
createModule({
id: 'new_image.png',
path: '/root/img/new_image.png',
isAsset: true,
resolution: 2,
dependencies: []
},
{
}),
createModule({
id: 'package/file.json',
path: '/root/file.json',
isJSON: true,
dependencies: [],
},
}),
];
getDependencies.mockImpl(function() {
@ -119,8 +143,8 @@ describe('Packager', function() {
});
});
pit('create a package', function() {
return packager.package('/root/foo.js', true, 'source_map_url')
pit('create a bundle', function() {
return bundler.bundle('/root/foo.js', true, 'source_map_url')
.then(function(p) {
expect(p.addModule.mock.calls[0][0]).toEqual({
code: 'lol transformed /root/foo.js lol',
@ -194,51 +218,24 @@ describe('Packager', function() {
{runMainModule: true}
]);
expect(p.addAsset.mock.calls[0]).toEqual([
expect(p.addAsset.mock.calls).toContain([
imgModule_DEPRECATED
]);
expect(p.addAsset.mock.calls[1]).toEqual([
expect(p.addAsset.mock.calls).toContain([
imgModule
]);
// TODO(amasad) This fails with 0 != 5 in OSS
//expect(ProgressBar.prototype.tick.mock.calls.length).toEqual(modules.length);
});
});
pit('gets the list of dependencies', function() {
return packager.getDependencies('/root/foo.js', true)
.then(({dependencies}) => {
expect(dependencies).toEqual([
{
dependencies: [],
id: 'foo',
path: '/root/foo.js',
},
{
dependencies: [],
id: 'bar',
path: '/root/bar.js',
},
{
dependencies: [],
id: 'image!img',
isAsset_DEPRECATED: true,
path: '/root/img/img.png',
resolution: 2,
},
{
dependencies: [],
id: 'new_image.png',
isAsset: true,
path: '/root/img/new_image.png',
resolution: 2,
},
{
dependencies: [],
id: 'package/file.json',
isJSON: true,
path: '/root/file.json',
},
]);
});
pit('gets the list of dependencies from the resolver', function() {
return bundler.getDependencies('/root/foo.js', true)
.then(
() => expect(getDependencies)
.toBeCalledWith('/root/foo.js', { dev: true })
);
});
});

309
react-packager/src/Bundler/index.js vendored Normal file
View File

@ -0,0 +1,309 @@
/**
* 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 assert = require('assert');
const fs = require('fs');
const path = require('path');
const Promise = require('promise');
const ProgressBar = require('progress');
const Cache = require('../Cache');
const Transformer = require('../JSTransformer');
const DependencyResolver = require('../DependencyResolver');
const Bundle = require('./Bundle');
const Activity = require('../Activity');
const ModuleTransport = require('../lib/ModuleTransport');
const declareOpts = require('../lib/declareOpts');
const imageSize = require('image-size');
const sizeOf = Promise.denodeify(imageSize);
const readFile = Promise.denodeify(fs.readFile);
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: ['png'],
},
fileWatcher: {
type: 'object',
required: true,
},
assetServer: {
type: 'object',
required: true,
},
transformTimeoutInterval: {
type: 'number',
required: false,
},
});
class Bundler {
constructor(options) {
const opts = this._opts = validateOpts(options);
opts.projectRoots.forEach(verifyRootExists);
this._cache = new Cache({
resetCache: opts.resetCache,
cacheVersion: opts.cacheVersion,
projectRoots: opts.projectRoots,
transformModulePath: opts.transformModulePath,
});
this._resolver = new DependencyResolver({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
polyfillModuleNames: opts.polyfillModuleNames,
moduleFormat: opts.moduleFormat,
assetRoots: opts.assetRoots,
fileWatcher: opts.fileWatcher,
assetExts: opts.assetExts,
cache: this._cache,
});
this._transformer = new Transformer({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
cache: this._cache,
transformModulePath: opts.transformModulePath,
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
}
kill() {
this._transformer.kill();
return this._cache.end();
}
bundle(main, runModule, sourceMapUrl, isDev, platform) {
const bundle = new Bundle(sourceMapUrl);
const findEventId = Activity.startEvent('find dependencies');
let transformEventId;
return this.getDependencies(main, isDev, platform).then((result) => {
Activity.endEvent(findEventId);
transformEventId = Activity.startEvent('transform');
let bar;
if (process.stdout.isTTY) {
bar = new ProgressBar('transforming [:bar] :percent :current/:total', {
complete: '=',
incomplete: ' ',
width: 40,
total: result.dependencies.length,
});
}
bundle.setMainModuleId(result.mainModuleId);
return Promise.all(
result.dependencies.map(
module => this._transformModule(bundle, module).then(transformed => {
if (bar) {
bar.tick();
}
return transformed;
})
)
);
}).then((transformedModules) => {
Activity.endEvent(transformEventId);
transformedModules.forEach(function(moduleTransport) {
bundle.addModule(moduleTransport);
});
bundle.finalize({ runMainModule: runModule });
return bundle;
});
}
invalidateFile(filePath) {
this._transformer.invalidateFile(filePath);
}
getDependencies(main, isDev, platform) {
return this._resolver.getDependencies(main, { dev: isDev, platform });
}
_transformModule(bundle, module) {
let transform;
if (module.isAsset_DEPRECATED()) {
transform = this.generateAssetModule_DEPRECATED(bundle, module);
} else if (module.isAsset()) {
transform = this.generateAssetModule(bundle, module);
} else if (module.isJSON()) {
transform = generateJSONModule(module);
} else {
transform = this._transformer.loadFileAndTransform(
path.resolve(module.path)
);
}
const resolver = this._resolver;
return transform.then(
transformed => resolver.wrapModule(module, transformed.code).then(
code => new ModuleTransport({
code: code,
map: transformed.map,
sourceCode: transformed.sourceCode,
sourcePath: transformed.sourcePath,
virtual: transformed.virtual,
})
)
);
}
getGraphDebugInfo() {
return this._resolver.getDebugInfo();
}
generateAssetModule_DEPRECATED(bundle, module) {
return Promise.all([
sizeOf(module.path),
module.getName(),
]).then(([dimensions, id]) => {
const img = {
__packager_asset: true,
isStatic: true,
path: module.path,
uri: id.replace(/^[^!]+!/, ''),
width: dimensions.width / module.resolution,
height: dimensions.height / module.resolution,
deprecated: true,
};
bundle.addAsset(img);
const code = 'module.exports = ' + JSON.stringify(img) + ';';
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
generateAssetModule(bundle, module) {
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
return Promise.all([
sizeOf(module.path),
this._assetServer.getAssetData(relPath),
]).then(function(res) {
const dimensions = res[0];
const assetData = res[1];
const img = {
__packager_asset: true,
fileSystemLocation: path.dirname(module.path),
httpServerLocation: path.join('/assets', path.dirname(relPath)),
width: dimensions.width / module.resolution,
height: dimensions.height / module.resolution,
scales: assetData.scales,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
bundle.addAsset(img);
const ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);';
const code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img));
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
}
function generateJSONModule(module) {
return readFile(module.path).then(function(data) {
const code = 'module.exports = ' + data.toString('utf8') + ';';
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
function getPathRelativeToRoot(roots, absPath) {
for (let i = 0; i < roots.length; i++) {
const relPath = path.relative(roots[i], absPath);
if (relPath[0] !== '.') {
return relPath;
}
}
throw new Error(
'Expected root module to be relative to one of the project roots'
);
}
function verifyRootExists(root) {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
class DummyCache {
get(filepath, field, loaderCb) {
return loaderCb();
}
end(){}
invalidate(filepath){}
}
module.exports = Bundler;

View File

@ -0,0 +1,256 @@
/**
* 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';
jest
.dontMock('../index');
const Promise = require('promise');
describe('BundlesLayout', () => {
var BundlesLayout;
var DependencyResolver;
beforeEach(() => {
BundlesLayout = require('../index');
DependencyResolver = require('../../DependencyResolver');
});
describe('generate', () => {
function newBundlesLayout() {
return new BundlesLayout({
dependencyResolver: new DependencyResolver(),
});
}
function isPolyfill() {
return false;
}
function dep(path) {
return {
path: path,
isPolyfill: isPolyfill,
};
}
pit('should bundle sync dependencies', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
asyncDependencies: [],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [],
})
);
});
pit('should separate async dependencies into different bundle', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js')],
asyncDependencies: [['/root/a.js']],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id:'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}],
})
);
});
pit('separate async dependencies of async dependencies', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js')],
asyncDependencies: [['/root/a.js']],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js')],
asyncDependencies: [['/root/b.js']],
});
case '/root/b.js':
return Promise.resolve({
dependencies: [dep('/root/b.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [{
id: 'bundle.0.1.2',
modules: ['/root/b.js'],
children: [],
}],
}],
})
);
});
pit('separate bundle sync dependencies of async ones on same bundle', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js')],
asyncDependencies: [['/root/a.js']],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js'), dep('/root/b.js')],
asyncDependencies: [],
});
case '/root/b.js':
return Promise.resolve({
dependencies: [dep('/root/b.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
expect(bundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/b.js'],
children: [],
}],
})
);
});
pit('separate cache in which bundle is each dependency', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
asyncDependencies: [],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js')],
asyncDependencies: [['/root/b.js']],
});
case '/root/b.js':
return Promise.resolve({
dependencies: [dep('/root/b.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(
bundles => expect(bundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/b.js'],
children: [],
}],
})
);
});
pit('separate cache in which bundle is each dependency', () => {
DependencyResolver.prototype.getDependencies.mockImpl((path) => {
switch (path) {
case '/root/index.js':
return Promise.resolve({
dependencies: [dep('/root/index.js'), dep('/root/a.js')],
asyncDependencies: [['/root/b.js'], ['/root/c.js']],
});
case '/root/a.js':
return Promise.resolve({
dependencies: [dep('/root/a.js')],
asyncDependencies: [],
});
case '/root/b.js':
return Promise.resolve({
dependencies: [dep('/root/b.js')],
asyncDependencies: [['/root/d.js']],
});
case '/root/c.js':
return Promise.resolve({
dependencies: [dep('/root/c.js')],
asyncDependencies: [],
});
case '/root/d.js':
return Promise.resolve({
dependencies: [dep('/root/d.js')],
asyncDependencies: [],
});
default:
throw 'Undefined path: ' + path;
}
});
var layout = newBundlesLayout();
return layout.generateLayout(['/root/index.js']).then(() => {
expect(layout.getBundleIDForModule('/root/index.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/c.js')).toBe('bundle.0.2');
expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3');
});
});
});
});

View File

@ -0,0 +1,612 @@
/**
* 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';
jest
.dontMock('absolute-path')
.dontMock('crypto')
.dontMock('underscore')
.dontMock('path')
.dontMock('../index')
.dontMock('../../lib/getAssetDataFromName')
.dontMock('../../DependencyResolver/crawlers')
.dontMock('../../DependencyResolver/crawlers/node')
.dontMock('../../DependencyResolver/DependencyGraph/docblock')
.dontMock('../../DependencyResolver/fastfs')
.dontMock('../../DependencyResolver/replacePatterns')
.dontMock('../../DependencyResolver')
.dontMock('../../DependencyResolver/DependencyGraph')
.dontMock('../../DependencyResolver/AssetModule_DEPRECATED')
.dontMock('../../DependencyResolver/AssetModule')
.dontMock('../../DependencyResolver/Module')
.dontMock('../../DependencyResolver/Package')
.dontMock('../../DependencyResolver/Polyfill')
.dontMock('../../DependencyResolver/ModuleCache');
const Promise = require('promise');
const path = require('path');
jest.mock('fs');
describe('BundlesLayout', () => {
var BundlesLayout;
var Cache;
var DependencyResolver;
var fileWatcher;
var fs;
const polyfills = [
'polyfills/prelude_dev.js',
'polyfills/prelude.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/String.prototype.es6.js',
'polyfills/Array.prototype.es6.js',
];
const baseFs = getBaseFs();
beforeEach(() => {
fs = require('fs');
BundlesLayout = require('../index');
Cache = require('../../Cache');
DependencyResolver = require('../../DependencyResolver');
fileWatcher = {
on: () => this,
isWatchman: () => Promise.resolve(false)
};
});
describe('generate', () => {
function newBundlesLayout() {
const resolver = new DependencyResolver({
projectRoots: ['/root', '/' + __dirname.split('/')[1]],
fileWatcher: fileWatcher,
cache: new Cache(),
assetExts: ['js', 'png'],
assetRoots: ['/root'],
});
return new BundlesLayout({dependencyResolver: resolver});
}
function stripPolyfills(bundle) {
return Promise
.all(bundle.children.map(childModule => stripPolyfills(childModule)))
.then(children => {
const modules = bundle.modules
.filter(moduleName => { // filter polyfills
for (let p of polyfills) {
if (moduleName.indexOf(p) !== -1) {
return false;
}
}
return true;
});
return {
id: bundle.id,
modules: modules,
children: children,
};
});
}
function setMockFilesystem(mockFs) {
fs.__setMockFilesystem(Object.assign(mockFs, baseFs));
}
pit('should bundle single-module app', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [],
})
)
);
});
pit('should bundle dependant modules', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
require("a");`,
'a.js': `
/**
* @providesModule a
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [],
})
)
);
});
pit('should split bundles for async dependencies', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");`,
'a.js': `
/**,
* @providesModule a
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}],
})
)
);
});
pit('should split into multiple bundles separate async dependencies', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");
System.import("b");`,
'a.js': `
/**,
* @providesModule a
*/`,
'b.js': `
/**
* @providesModule b
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [],
}, {
id: 'bundle.0.2',
modules: ['/root/b.js'],
children: [],
},
],
})
)
);
});
pit('should fully traverse sync dependencies', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
require("a");
System.import("b");`,
'a.js': `
/**,
* @providesModule a
*/`,
'b.js': `
/**
* @providesModule b
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js', '/root/a.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/b.js'],
children: [],
}],
})
)
);
});
pit('should include sync dependencies async dependencies might have', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");`,
'a.js': `
/**,
* @providesModule a
*/,
require("b");`,
'b.js': `
/**
* @providesModule b
*/
require("c");`,
'c.js': `
/**
* @providesModule c
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/b.js', '/root/c.js'],
children: [],
}],
})
)
);
});
pit('should allow duplicated dependencies across bundles', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");
System.import("b");`,
'a.js': `
/**,
* @providesModule a
*/,
require("c");`,
'b.js': `
/**
* @providesModule b
*/
require("c");`,
'c.js': `
/**
* @providesModule c
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/c.js'],
children: [],
},
{
id: 'bundle.0.2',
modules: ['/root/b.js', '/root/c.js'],
children: [],
},
],
})
)
);
});
pit('should put in separate bundles async dependencies of async dependencies', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");`,
'a.js': `
/**,
* @providesModule a
*/,
System.import("b");`,
'b.js': `
/**
* @providesModule b
*/
require("c");`,
'c.js': `
/**
* @providesModule c
*/`,
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js'],
children: [{
id: 'bundle.0.1.2',
modules: ['/root/b.js', '/root/c.js'],
children: [],
}],
},
],
})
)
);
});
pit('should put image dependencies into separate bundles', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");`,
'a.js':`
/**,
* @providesModule a
*/,
require("./img.png");`,
'img.png': '',
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
}],
})
)
);
});
pit('should put image dependencies across bundles', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");
System.import("b");`,
'a.js':`
/**,
* @providesModule a
*/,
require("./img.png");`,
'b.js':`
/**,
* @providesModule b
*/,
require("./img.png");`,
'img.png': '',
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [
{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
},
{
id: 'bundle.0.2',
modules: ['/root/b.js', '/root/img.png'],
children: [],
},
],
})
)
);
});
pit('could async require asset', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("./img.png");`,
'img.png': '',
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/img.png'],
children: [],
}],
})
)
);
});
pit('should include deprecated assets into separate bundles', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("a");`,
'a.js':`
/**,
* @providesModule a
*/,
require("image!img");`,
'img.png': '',
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/a.js', '/root/img.png'],
children: [],
}],
})
)
);
});
pit('could async require deprecated asset', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("image!img");`,
'img.png': '',
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/img.png'],
children: [],
}],
})
)
);
});
pit('should put packages into bundles', () => {
setMockFilesystem({
'root': {
'index.js': `
/**
* @providesModule index
*/
System.import("aPackage");`,
'aPackage': {
'package.json': JSON.stringify({
name: 'aPackage',
main: './main.js',
browser: {
'./main.js': './client.js',
},
}),
'main.js': 'some other code',
'client.js': 'some code',
},
}
});
return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
modules: ['/root/aPackage/client.js'],
children: [],
}],
})
)
);
});
});
function getBaseFs() {
const p = path.join(__dirname, '../../../DependencyResolver/polyfills').substring(1);
const root = {};
let currentPath = root;
p.split('/').forEach(part => {
const child = {};
currentPath[part] = child;
currentPath = child;
});
polyfills.forEach(polyfill =>
currentPath[polyfill.split('/')[1]] = ''
);
return root;
}
});

View File

@ -0,0 +1,117 @@
/**
* 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 _ = require('underscore');
const declareOpts = require('../lib/declareOpts');
const validateOpts = declareOpts({
dependencyResolver: {
type: 'object',
required: true,
},
});
const BUNDLE_PREFIX = 'bundle';
/**
* Class that takes care of separating the graph of dependencies into
* separate bundles
*/
class BundlesLayout {
constructor(options) {
const opts = validateOpts(options);
this._resolver = opts.dependencyResolver;
this._moduleToBundle = Object.create(null);
}
generateLayout(entryPaths, isDev) {
var currentBundleID = 0;
const rootBundle = {
id: BUNDLE_PREFIX + '.' + currentBundleID++,
modules: [],
children: [],
};
var pending = [{paths: entryPaths, bundle: rootBundle}];
return promiseWhile(
() => pending.length > 0,
() => rootBundle,
() => {
const {paths, bundle} = pending.shift();
// pending sync dependencies we still need to explore for the current
// pending dependency
const pendingSyncDeps = paths;
// accum variable for sync dependencies of the current pending
// dependency we're processing
const syncDependencies = Object.create(null);
return promiseWhile(
() => pendingSyncDeps.length > 0,
() => {
const dependencies = Object.keys(syncDependencies);
if (dependencies.length > 0) {
bundle.modules = dependencies;
}
},
index => {
const pendingSyncDep = pendingSyncDeps.shift();
return this._resolver
.getDependencies(pendingSyncDep, {dev: isDev})
.then(deps => {
deps.dependencies.forEach(dep => {
if (dep.path !== pendingSyncDep && !dep.isPolyfill()) {
pendingSyncDeps.push(dep.path);
}
syncDependencies[dep.path] = true;
this._moduleToBundle[dep.path] = bundle.id;
});
deps.asyncDependencies.forEach(asyncDeps => {
const childBundle = {
id: bundle.id + '.' + currentBundleID++,
modules: [],
children: [],
};
bundle.children.push(childBundle);
pending.push({paths: asyncDeps, bundle: childBundle});
});
});
},
);
},
);
}
getBundleIDForModule(path) {
return this._moduleToBundle[path];
}
}
// Runs the body Promise meanwhile the condition callback is satisfied.
// Once it's not satisfied anymore, it returns what the results callback
// indicates
function promiseWhile(condition, result, body) {
return _promiseWhile(condition, result, body, 0);
}
function _promiseWhile(condition, result, body, index) {
if (!condition()) {
return Promise.resolve(result());
}
return body(index).then(() =>
_promiseWhile(condition, result, body, index + 1)
);
}
module.exports = BundlesLayout;

View File

@ -8,13 +8,26 @@
*/
'use strict';
class Cache {
get(filepath, field, cb) {
return cb(filepath);
}
const mockColor = () => {
return {
bold: () => { return { }; },
};
};
invalidate(filepath) { }
end() { }
}
mockColor.bold = function() {
return {};
};
module.exports = Cache;
module.exports = {
dim: s => s,
magenta: mockColor,
white: mockColor,
blue: mockColor,
yellow: mockColor,
green: mockColor,
bold: mockColor,
red: mockColor,
cyan: mockColor,
gray: mockColor,
black: mockColor,
};

View File

@ -215,6 +215,7 @@ class Cache {
hash.update(options.transformModulePath);
var name = 'react-packager-cache-' + hash.digest('hex');
return path.join(tmpdir, name);
}
}

View File

@ -5,6 +5,13 @@ const Promise = require('promise');
const getAssetDataFromName = require('../lib/getAssetDataFromName');
class AssetModule extends Module {
constructor(...args) {
super(...args);
const { resolution, name, type } = getAssetDataFromName(this.path);
this.resolution = resolution;
this._name = name;
this._type = type;
}
isHaste() {
return Promise.resolve(false);
@ -14,33 +21,31 @@ class AssetModule extends Module {
return Promise.resolve([]);
}
getAsyncDependencies() {
return Promise.resolve([]);
}
_read() {
return Promise.resolve({});
}
getName() {
return super.getName().then(id => {
const {name, type} = getAssetDataFromName(this.path);
return id.replace(/\/[^\/]+$/, `/${name}.${type}`);
});
}
getPlainObject() {
return this.getName().then(name => this.addReference({
path: this.path,
isJSON: false,
isAsset: true,
isAsset_DEPRECATED: false,
isPolyfill: false,
resolution: getAssetDataFromName(this.path).resolution,
id: name,
dependencies: [],
}));
return super.getName().then(
id => id.replace(/\/[^\/]+$/, `/${this._name}.${this._type}`)
);
}
hash() {
return `AssetModule : ${this.path}`;
}
isJSON() {
return false;
}
isAsset() {
return true;
}
}
module.exports = AssetModule;

View File

@ -5,36 +5,45 @@ const Promise = require('promise');
const getAssetDataFromName = require('../lib/getAssetDataFromName');
class AssetModule_DEPRECATED extends Module {
constructor(...args) {
super(...args);
const {resolution, name} = getAssetDataFromName(this.path);
this.resolution = resolution;
this.name = name;
}
isHaste() {
return Promise.resolve(false);
}
getName() {
return Promise.resolve(this.name);
return Promise.resolve(`image!${this.name}`);
}
getDependencies() {
return Promise.resolve([]);
}
getPlainObject() {
const {name, resolution} = getAssetDataFromName(this.path);
return Promise.resolve(this.addReference({
path: this.path,
id: `image!${name}`,
resolution,
isAsset_DEPRECATED: true,
dependencies: [],
isJSON: false,
isPolyfill: false,
isAsset: false,
}));
getAsyncDependencies() {
return Promise.resolve([]);
}
hash() {
return `AssetModule_DEPRECATED : ${this.path}`;
}
isJSON() {
return false;
}
isAsset_DEPRECATED() {
return true;
}
resolution() {
return getAssetDataFromName(this.path).resolution;
}
}
module.exports = AssetModule_DEPRECATED;

View File

@ -36,6 +36,24 @@ describe('DependencyGraph', function() {
var fileWatcher;
var fs;
function getOrderedDependenciesAsJSON(dgraph, entry) {
return dgraph.getOrderedDependencies(entry).then(
deps => Promise.all(deps.map(dep => Promise.all([
dep.getName(),
dep.getDependencies(),
]).then(([name, dependencies]) => ({
path: dep.path,
isJSON: dep.isJSON(),
isAsset: dep.isAsset(),
isAsset_DEPRECATED: dep.isAsset_DEPRECATED(),
isPolyfill: dep.isPolyfill(),
resolution: dep.resolution,
id: name,
dependencies
})))
));
}
beforeEach(function() {
fs = require('fs');
Cache = require('../../../Cache');
@ -76,7 +94,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -134,7 +152,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -186,7 +204,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -246,7 +264,7 @@ describe('DependencyGraph', function() {
assetRoots_DEPRECATED: ['/root/imgs'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -298,7 +316,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -355,7 +373,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -433,7 +451,7 @@ describe('DependencyGraph', function() {
assetRoots_DEPRECATED: ['/root/imgs'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -495,7 +513,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -548,7 +566,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -601,7 +619,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -662,7 +680,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -719,7 +737,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -770,7 +788,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -820,7 +838,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -867,7 +885,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -918,7 +936,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -967,7 +985,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1021,7 +1039,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/somedir/somefile.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/somedir/somefile.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1079,7 +1097,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1127,7 +1145,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1174,7 +1192,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1233,7 +1251,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1292,7 +1310,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1371,7 +1389,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1428,7 +1446,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1485,7 +1503,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1542,7 +1560,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1614,7 +1632,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{ id: 'index',
@ -1717,7 +1735,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{ id: 'index',
@ -1798,7 +1816,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1880,7 +1898,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -1963,7 +1981,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2060,7 +2078,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2146,7 +2164,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2251,7 +2269,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2320,7 +2338,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/react-tools/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/react-tools/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2377,7 +2395,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2422,7 +2440,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2483,7 +2501,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2539,7 +2557,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2588,7 +2606,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2672,11 +2690,11 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', '');
triggerFileChange('change', 'index.js', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2737,11 +2755,11 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', '');
triggerFileChange('change', 'index.js', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2802,10 +2820,10 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
delete filesystem.root.foo;
triggerFileChange('delete', 'foo.js', root);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2866,7 +2884,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['bar.js'] = [
'/**',
' * @providesModule bar',
@ -2878,7 +2896,7 @@ describe('DependencyGraph', function() {
filesystem.root.aPackage['main.js'] = 'require("bar")';
triggerFileChange('change', 'aPackage/main.js', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2949,7 +2967,7 @@ describe('DependencyGraph', function() {
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -2968,7 +2986,7 @@ describe('DependencyGraph', function() {
filesystem.root['foo.png'] = '';
triggerFileChange('add', 'foo.png', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
.toEqual([
{
@ -3021,7 +3039,7 @@ describe('DependencyGraph', function() {
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{ id: 'index',
@ -3039,7 +3057,7 @@ describe('DependencyGraph', function() {
filesystem.root['foo.png'] = '';
triggerFileChange('add', 'foo.png', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
.toEqual([
{
@ -3108,7 +3126,7 @@ describe('DependencyGraph', function() {
},
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['bar.js'] = [
'/**',
' * @providesModule bar',
@ -3120,7 +3138,7 @@ describe('DependencyGraph', function() {
filesystem.root.aPackage['main.js'] = 'require("bar")';
triggerFileChange('change', 'aPackage/main.js', root, mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3193,11 +3211,11 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
triggerFileChange('change', 'aPackage', '/root', {
isDirectory: function(){ return true; }
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3264,7 +3282,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root['index.js'] = filesystem.root['index.js'].replace(/aPackage/, 'bPackage');
triggerFileChange('change', 'index.js', root, mockStat);
@ -3274,7 +3292,7 @@ describe('DependencyGraph', function() {
});
triggerFileChange('change', 'package.json', '/root/aPackage', mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3331,7 +3349,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root.aPackage['package.json'] = JSON.stringify({
name: 'aPackage',
main: 'main.js',
@ -3339,7 +3357,7 @@ describe('DependencyGraph', function() {
});
triggerFileChange('change', 'package.json', '/root/aPackage', mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3396,14 +3414,14 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function() {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() {
filesystem.root.aPackage['package.json'] = JSON.stringify({
name: 'bPackage',
main: 'main.js',
});
triggerFileChange('change', 'package.json', '/root/aPackage', mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3459,7 +3477,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
@ -3500,7 +3518,7 @@ describe('DependencyGraph', function() {
filesystem.root.node_modules.foo['main.js'] = 'lol';
triggerFileChange('change', 'main.js', '/root/node_modules/foo', mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
.toEqual([
{
@ -3559,7 +3577,7 @@ describe('DependencyGraph', function() {
assetExts: ['png', 'jpg'],
cache: cache,
});
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
filesystem.root.node_modules.foo['package.json'] = JSON.stringify({
name: 'foo',
main: 'main.js',
@ -3567,7 +3585,7 @@ describe('DependencyGraph', function() {
});
triggerFileChange('change', 'package.json', '/root/node_modules/foo', mockStat);
return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) {
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) {
expect(deps2)
.toEqual([
{

View File

@ -13,7 +13,6 @@ const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED');
const Fastfs = require('../fastfs');
const ModuleCache = require('../ModuleCache');
const Promise = require('promise');
const _ = require('underscore');
const crawl = require('../crawlers');
const debug = require('debug')('DependencyGraph');
const declareOpts = require('../../lib/declareOpts');
@ -70,7 +69,7 @@ class DependencyGraph {
constructor(options) {
this._opts = validateOpts(options);
this._hasteMap = Object.create(null);
this._immediateResolutionCache = Object.create(null);
this._resetResolutionCache();
this._cache = this._opts.cache;
this.load();
}
@ -80,7 +79,8 @@ class DependencyGraph {
return this._loading;
}
const crawlActivity = Activity.startEvent('fs crawl');
const depGraphActivity = Activity.startEvent('Building Dependency Graph');
const crawlActivity = Activity.startEvent('Crawling File System');
const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED);
this._crawling = crawl(allRoots, {
ignore: this._opts.ignoreFilePath,
@ -89,10 +89,15 @@ class DependencyGraph {
});
this._crawling.then((files) => Activity.endEvent(crawlActivity));
this._fastfs = new Fastfs(this._opts.roots, this._opts.fileWatcher, {
ignore: this._opts.ignoreFilePath,
crawling: this._crawling,
});
this._fastfs = new Fastfs(
'JavaScript',
this._opts.roots,
this._opts.fileWatcher,
{
ignore: this._opts.ignoreFilePath,
crawling: this._crawling,
}
);
this._fastfs.on('change', this._processFileChange.bind(this));
@ -102,19 +107,31 @@ class DependencyGraph {
this._fastfs.build()
.then(() => {
const hasteActivity = Activity.startEvent('Building Haste Map');
this._buildHasteMap().then(() => Activity.endEvent(hasteActivity));
return this._buildHasteMap().then(() => Activity.endEvent(hasteActivity));
}),
this._buildAssetMap_DEPRECATED(),
]);
]).then(() =>
Activity.endEvent(depGraphActivity)
);
return this._loading;
}
resolveDependency(fromModule, toModuleName) {
if (fromModule._ref) {
fromModule = fromModule._ref;
setup({ platform }) {
if (platform && this._opts.platforms.indexOf(platform) === -1) {
throw new Error('Unrecognized platform: ' + platform);
}
// TODO(amasad): This is a potential race condition. Mutliple requests could
// interfere with each other. This needs a refactor to fix -- which will
// follow this diff.
if (this._platformExt !== platform) {
this._resetResolutionCache();
}
this._platformExt = platform;
}
resolveDependency(fromModule, toModuleName) {
const resHash = resolutionHash(fromModule.path, toModuleName);
if (this._immediateResolutionCache[resHash]) {
@ -163,33 +180,7 @@ class DependencyGraph {
getOrderedDependencies(entryPath) {
return this.load().then(() => {
const absPath = this._getAbsolutePath(entryPath);
if (absPath == null) {
throw new NotFoundError(
'Could not find source file at %s',
entryPath
);
}
const absolutePath = path.resolve(absPath);
if (absolutePath == null) {
throw new NotFoundError(
'Cannot find entry file %s in any of the roots: %j',
entryPath,
this._opts.roots
);
}
const platformExt = getPlatformExt(entryPath);
if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) {
this._platformExt = platformExt;
} else {
this._platformExt = null;
}
const entry = this._moduleCache.getModule(absolutePath);
const entry = this._getModuleForEntryPath(entryPath);
const deps = [];
const visited = Object.create(null);
visited[entry.hash()] = true;
@ -226,7 +217,22 @@ class DependencyGraph {
};
return collect(entry)
.then(() => Promise.all(deps.map(dep => dep.getPlainObject())));
.then(() => deps);
});
}
getAsyncDependencies(entryPath) {
return this.load().then(() => {
const mod = this._getModuleForEntryPath(entryPath);
return mod.getAsyncDependencies().then(bundles =>
Promise
.all(bundles.map(bundle =>
Promise.all(bundle.map(
dep => this.resolveDependency(mod, dep)
))
))
.then(bs => bs.map(bundle => bundle.map(dep => dep.path)))
);
});
}
@ -246,6 +252,39 @@ class DependencyGraph {
return null;
}
_getModuleForEntryPath(entryPath) {
const absPath = this._getAbsolutePath(entryPath);
if (absPath == null) {
throw new NotFoundError(
'Could not find source file at %s',
entryPath
);
}
const absolutePath = path.resolve(absPath);
if (absolutePath == null) {
throw new NotFoundError(
'Cannot find entry file %s in any of the roots: %j',
entryPath,
this._opts.roots
);
}
// `platformExt` could be set in the `setup` method.
if (!this._platformExt) {
const platformExt = getPlatformExt(entryPath);
if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) {
this._platformExt = platformExt;
} else {
this._platformExt = null;
}
}
return this._moduleCache.getModule(absolutePath);
}
_resolveHasteDependency(fromModule, toModuleName) {
toModuleName = normalizePath(toModuleName);
@ -510,6 +549,7 @@ class DependencyGraph {
this._assetMap_DEPRECATED = Object.create(null);
const fastfs = new Fastfs(
'Assets',
this._opts.assetRoots_DEPRECATED,
this._opts.fileWatcher,
{ ignore: this._opts.ignoreFilePath, crawling: this._crawling }
@ -549,7 +589,7 @@ class DependencyGraph {
_processFileChange(type, filePath, root, fstat) {
// It's really hard to invalidate the right module resolution cache
// so we just blow it up with every file change.
this._immediateResolutionCache = Object.create(null);
this._resetResolutionCache();
const absPath = path.join(root, filePath);
if ((fstat && fstat.isDirectory()) ||
@ -585,6 +625,10 @@ class DependencyGraph {
});
}
}
_resetResolutionCache() {
this._immediateResolutionCache = Object.create(null);
}
}
function assetName(file, ext) {

View File

@ -1,6 +1,13 @@
/**
* 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 Promise = require('promise');
const docblock = require('./DependencyGraph/docblock');
const isAbsolutePath = require('absolute-path');
const path = require('path');
@ -69,6 +76,10 @@ class Module {
this._cache.invalidate(this.path);
}
getAsyncDependencies() {
return this._read().then(data => data.asyncDependencies);
}
_read() {
if (!this._reading) {
this._reading = this._fastfs.readFile(this.path).then(content => {
@ -85,7 +96,9 @@ class Module {
if ('extern' in moduleDocBlock) {
data.dependencies = [];
} else {
data.dependencies = extractRequires(content);
var dependencies = extractRequires(content);
data.dependencies = dependencies.sync;
data.asyncDependencies = dependencies.async;
}
return data;
@ -95,49 +108,73 @@ class Module {
return this._reading;
}
getPlainObject() {
return Promise.all([
this.getName(),
this.getDependencies(),
]).then(([name, dependencies]) => this.addReference({
path: this.path,
isJSON: path.extname(this.path) === '.json',
isAsset: false,
isAsset_DEPRECATED: false,
isPolyfill: false,
resolution: undefined,
id: name,
dependencies
}));
}
hash() {
return `Module : ${this.path}`;
}
addReference(obj) {
Object.defineProperty(obj, '_ref', { value: this });
return obj;
isJSON() {
return path.extname(this.path) === '.json';
}
isAsset() {
return false;
}
isPolyfill() {
return false;
}
isAsset_DEPRECATED() {
return false;
}
toJSON() {
return {
hash: this.hash(),
isJSON: this.isJSON(),
isAsset: this.isAsset(),
isAsset_DEPRECATED: this.isAsset_DEPRECATED(),
type: this.type,
path: this.path,
};
}
}
/**
* Extract all required modules from a `code` string.
*/
var blockCommentRe = /\/\*(.|\n)*?\*\//g;
var lineCommentRe = /\/\/.+(\n|$)/g;
const blockCommentRe = /\/\*(.|\n)*?\*\//g;
const lineCommentRe = /\/\/.+(\n|$)/g;
function extractRequires(code /*: string*/) /*: Array<string>*/ {
var deps = [];
var deps = {
sync: [],
async: [],
};
code
.replace(blockCommentRe, '')
.replace(lineCommentRe, '')
// Parse sync dependencies. See comment below for further detils.
.replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => {
deps.push(dep);
deps.sync.push(dep);
return match;
})
.replace(replacePatterns.REQUIRE_RE, function(match, pre, quot, dep, post) {
deps.push(dep);
// Parse the sync dependencies this module has. When the module is
// required, all it's sync dependencies will be loaded into memory.
// Sync dependencies can be defined either using `require` or the ES6
// `import` syntax:
// var dep1 = require('dep1');
.replace(replacePatterns.REQUIRE_RE, (match, pre, quot, dep, post) => {
deps.sync.push(dep);
})
// Parse async dependencies this module has. As opposed to what happens
// with sync dependencies, when the module is required, it's async
// dependencies won't be loaded into memory. This is deferred till the
// code path gets to the import statement:
// System.import('dep1')
.replace(replacePatterns.SYSTEM_IMPORT_RE, (match, pre, quot, dep, post) => {
deps.async.push([dep]);
return match;
});
return deps;

View File

@ -0,0 +1,38 @@
'use strict';
const Promise = require('promise');
const Module = require('./Module');
class Polyfill extends Module {
constructor({ path, id, dependencies }) {
super(path);
this._id = id;
this._dependencies = dependencies;
}
isHaste() {
return Promise.resolve(false);
}
getName() {
return Promise.resolve(this._id);
}
getPackage() {
return null;
}
getDependencies() {
return Promise.resolve(this._dependencies);
}
isJSON() {
return false;
}
isPolyfill() {
return true;
}
}
module.exports = Polyfill;

View File

@ -9,23 +9,24 @@
'use strict';
jest.dontMock('../')
.dontMock('q')
.dontMock('../replacePatterns')
.setMock('chalk', { dim: function(s) { return s; } });
.dontMock('underscore')
.dontMock('../replacePatterns');
jest.mock('path');
var Promise = require('promise');
var _ = require('underscore');
describe('HasteDependencyResolver', function() {
var HasteDependencyResolver;
function createModule(o) {
o.getPlainObject = () => Promise.resolve(o);
return o;
}
var Module;
var Polyfill;
beforeEach(function() {
Module = require('../Module');
Polyfill = require('../Polyfill');
Polyfill.mockClear();
// For the polyfillDeps
require('path').join.mockImpl(function(a, b) {
return b;
@ -33,12 +34,16 @@ describe('HasteDependencyResolver', function() {
HasteDependencyResolver = require('../');
});
function createModule(id, dependencies) {
var module = new Module();
module.getName.mockImpl(() => Promise.resolve(id));
module.getDependencies.mockImpl(() => Promise.resolve(dependencies));
return module;
}
describe('getDependencies', function() {
pit('should get dependencies with polyfills', function() {
var module = createModule({
id: 'index',
path: '/root/index.js', dependencies: ['a']
});
var module = createModule('index');
var deps = [module];
var depResolver = new HasteDependencyResolver({
@ -57,7 +62,8 @@ describe('HasteDependencyResolver', function() {
return depResolver.getDependencies('/root/index.js', { dev: false })
.then(function(result) {
expect(result.mainModuleId).toEqual('index');
expect(result.dependencies).toEqual([
expect(result.dependencies[result.dependencies.length - 1]).toBe(module);
expect(_.pluck(Polyfill.mock.calls, 0)).toEqual([
{ path: 'polyfills/prelude.js',
id: 'polyfills/prelude.js',
isPolyfill: true,
@ -115,18 +121,12 @@ describe('HasteDependencyResolver', function() {
'polyfills/String.prototype.es6.js',
],
},
module
]);
});
});
pit('should get dependencies with polyfills', function() {
var module = createModule({
id: 'index',
path: '/root/index.js',
dependencies: ['a'],
});
var module = createModule('index');
var deps = [module];
var depResolver = new HasteDependencyResolver({
@ -145,75 +145,15 @@ describe('HasteDependencyResolver', function() {
return depResolver.getDependencies('/root/index.js', { dev: true })
.then(function(result) {
expect(result.mainModuleId).toEqual('index');
expect(result.dependencies).toEqual([
{ path: 'polyfills/prelude_dev.js',
id: 'polyfills/prelude_dev.js',
isPolyfill: true,
dependencies: []
},
{ path: 'polyfills/require.js',
id: 'polyfills/require.js',
isPolyfill: true,
dependencies: ['polyfills/prelude_dev.js']
},
{ path: 'polyfills/polyfills.js',
id: 'polyfills/polyfills.js',
isPolyfill: true,
dependencies: ['polyfills/prelude_dev.js', 'polyfills/require.js']
},
{ id: 'polyfills/console.js',
isPolyfill: true,
path: 'polyfills/console.js',
dependencies: [
'polyfills/prelude_dev.js',
'polyfills/require.js',
'polyfills/polyfills.js'
],
},
{ id: 'polyfills/error-guard.js',
isPolyfill: true,
path: 'polyfills/error-guard.js',
dependencies: [
'polyfills/prelude_dev.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js'
],
},
{ id: 'polyfills/String.prototype.es6.js',
isPolyfill: true,
path: 'polyfills/String.prototype.es6.js',
dependencies: [
'polyfills/prelude_dev.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js'
],
},
{ id: 'polyfills/Array.prototype.es6.js',
isPolyfill: true,
path: 'polyfills/Array.prototype.es6.js',
dependencies: [
'polyfills/prelude_dev.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/String.prototype.es6.js'
],
},
module
]);
expect(depGraph.getOrderedDependencies).toBeCalledWith('/root/index.js');
expect(result.dependencies[0]).toBe(Polyfill.mock.instances[0]);
expect(result.dependencies[result.dependencies.length - 1])
.toBe(module);
});
});
pit('should pass in more polyfills', function() {
var module = createModule({
id: 'index',
path: '/root/index.js',
dependencies: ['a']
});
var module = createModule('index');
var deps = [module];
var depResolver = new HasteDependencyResolver({
@ -231,66 +171,9 @@ describe('HasteDependencyResolver', function() {
});
return depResolver.getDependencies('/root/index.js', { dev: false })
.then(function(result) {
.then((result) => {
expect(result.mainModuleId).toEqual('index');
expect(result.dependencies).toEqual([
{ path: 'polyfills/prelude.js',
id: 'polyfills/prelude.js',
isPolyfill: true,
dependencies: []
},
{ path: 'polyfills/require.js',
id: 'polyfills/require.js',
isPolyfill: true,
dependencies: ['polyfills/prelude.js']
},
{ path: 'polyfills/polyfills.js',
id: 'polyfills/polyfills.js',
isPolyfill: true,
dependencies: ['polyfills/prelude.js', 'polyfills/require.js']
},
{ id: 'polyfills/console.js',
isPolyfill: true,
path: 'polyfills/console.js',
dependencies: [
'polyfills/prelude.js',
'polyfills/require.js',
'polyfills/polyfills.js'
],
},
{ id: 'polyfills/error-guard.js',
isPolyfill: true,
path: 'polyfills/error-guard.js',
dependencies: [
'polyfills/prelude.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js'
],
},
{ id: 'polyfills/String.prototype.es6.js',
isPolyfill: true,
path: 'polyfills/String.prototype.es6.js',
dependencies: [
'polyfills/prelude.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js'
],
},
{ id: 'polyfills/Array.prototype.es6.js',
isPolyfill: true,
path: 'polyfills/Array.prototype.es6.js',
dependencies: [
'polyfills/prelude.js',
'polyfills/require.js',
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/String.prototype.es6.js',
],
},
expect(Polyfill.mock.calls[result.dependencies.length - 2]).toEqual([
{ path: 'some module',
id: 'some module',
isPolyfill: true,
@ -304,7 +187,6 @@ describe('HasteDependencyResolver', function() {
'polyfills/Array.prototype.es6.js'
]
},
module
]);
});
});
@ -463,25 +345,21 @@ describe('HasteDependencyResolver', function() {
depGraph.resolveDependency.mockImpl(function(fromModule, toModuleName) {
if (toModuleName === 'x') {
return Promise.resolve(createModule({
id: 'changed'
}));
return Promise.resolve(createModule('changed'));
} else if (toModuleName === 'y') {
return Promise.resolve(createModule({ id: 'Y' }));
return Promise.resolve(createModule('Y'));
}
return Promise.resolve(null);
});
return depResolver.wrapModule({
id: 'test module',
path: '/root/test.js',
dependencies: dependencies
}, code).then(processedCode => {
return depResolver.wrapModule(
createModule('test module', ['x', 'y']),
code
).then(processedCode => {
expect(processedCode).toEqual([
'__d(\'test module\',["changed","Y"],function(global,' +
' require, requireDynamic, requireLazy, module, exports) { ' +
'__d(\'test module\',["changed","Y"],function(global, require,' +
' module, exports) { ' +
"import'x';",
"import 'changed';",
"import 'changed' ;",

View File

@ -0,0 +1,104 @@
/**
* 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';
jest
.dontMock('absolute-path')
.dontMock('../fastfs')
.dontMock('../replacePatterns')
.dontMock('../DependencyGraph/docblock')
.dontMock('../../FileWatcher')
.dontMock('../Module');
jest
.mock('fs');
describe('Module', () => {
var Fastfs;
var Module;
var ModuleCache;
var Promise;
var fs;
const FileWatcher = require('../../FileWatcher');
const fileWatcher = new FileWatcher(['/root']);
beforeEach(function() {
Fastfs = require('../fastfs');
Module = require('../Module');
ModuleCache = require('../ModuleCache');
Promise = require('promise');
fs = require('fs');
});
describe('Async Dependencies', () => {
function expectAsyncDependenciesToEqual(expected) {
var fastfs = new Fastfs(
'test',
['/root'],
fileWatcher,
{crawling: Promise.resolve(['/root/index.js']), ignore: []},
);
return fastfs.build().then(() => {
var module = new Module('/root/index.js', fastfs, new ModuleCache(fastfs));
return module.getAsyncDependencies().then(actual =>
expect(actual).toEqual(expected)
);
});
}
pit('should recognize single dependency', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.import("dep1")',
}
});
return expectAsyncDependenciesToEqual([['dep1']]);
});
pit('should parse single quoted dependencies', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.import(\'dep1\')',
}
});
return expectAsyncDependenciesToEqual([['dep1']]);
});
pit('should parse multiple async dependencies on the same module', () => {
fs.__setMockFilesystem({
'root': {
'index.js': [
'System.import("dep1")',
'System.import("dep2")',
].join('\n'),
}
});
return expectAsyncDependenciesToEqual([
['dep1'],
['dep2'],
]);
});
pit('parse fine new lines', () => {
fs.__setMockFilesystem({
'root': {
'index.js': 'System.import(\n"dep1"\n)',
}
});
return expectAsyncDependenciesToEqual([['dep1']]);
});
});
});

View File

@ -14,8 +14,9 @@ const stat = Promise.denodeify(fs.stat);
const hasOwn = Object.prototype.hasOwnProperty;
class Fastfs extends EventEmitter {
constructor(roots, fileWatcher, {ignore, crawling}) {
constructor(name, roots, fileWatcher, {ignore, crawling}) {
super();
this._name = name;
this._fileWatcher = fileWatcher;
this._ignore = ignore;
this._roots = roots.map(root => new File(root, { isDir: true }));
@ -29,7 +30,7 @@ class Fastfs extends EventEmitter {
);
return this._crawling.then(files => {
const fastfsActivity = Activity.startEvent('Building in-memory fs');
const fastfsActivity = Activity.startEvent('Building in-memory fs for ' + this._name);
files.forEach(filePath => {
if (filePath.match(rootsPattern)) {
const newFile = new File(filePath, { isDir: false });
@ -155,7 +156,6 @@ class Fastfs extends EventEmitter {
this._getAndAssertRoot(file.path).addChild(file);
}
_processFileChange(type, filePath, root, fstat) {
const absPath = path.join(root, filePath);
if (this._ignore(absPath) || (fstat && fstat.isDirectory())) {

View File

@ -11,6 +11,7 @@
var path = require('path');
var DependencyGraph = require('./DependencyGraph');
var replacePatterns = require('./replacePatterns');
var Polyfill = require('./Polyfill');
var declareOpts = require('../lib/declareOpts');
var Promise = require('promise');
@ -26,10 +27,6 @@ var validateOpts = declareOpts({
type: 'array',
default: [],
},
nonPersistent: {
type: 'boolean',
default: false,
},
moduleFormat: {
type: 'string',
default: 'haste',
@ -76,6 +73,10 @@ var getDependenciesValidateOpts = declareOpts({
type: 'boolean',
default: true,
},
platform: {
type: 'string',
required: false,
},
});
HasteDependencyResolver.prototype.getDependencies = function(main, options) {
@ -83,18 +84,24 @@ HasteDependencyResolver.prototype.getDependencies = function(main, options) {
var depGraph = this._depGraph;
var self = this;
return depGraph.load().then(
() => depGraph.getOrderedDependencies(main).then(
dependencies => {
const mainModuleId = dependencies[0].id;
depGraph.setup({ platform: opts.platform });
return Promise.all([
depGraph.getOrderedDependencies(main),
depGraph.getAsyncDependencies(main),
]).then(
([dependencies, asyncDependencies]) => dependencies[0].getName().then(
mainModuleId => {
self._prependPolyfillDependencies(
dependencies,
opts.dev
opts.dev,
);
return {
mainModuleId: mainModuleId,
dependencies: dependencies
mainModuleId,
dependencies,
asyncDependencies,
};
}
)
@ -118,7 +125,7 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function(
].concat(this._polyfillModuleNames);
var polyfillModules = polyfillModuleNames.map(
(polyfillModuleName, idx) => ({
(polyfillModuleName, idx) => new Polyfill({
path: polyfillModuleName,
id: polyfillModuleName,
dependencies: polyfillModuleNames.slice(0, idx),
@ -130,23 +137,26 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function(
};
HasteDependencyResolver.prototype.wrapModule = function(module, code) {
if (module.isPolyfill) {
if (module.isPolyfill()) {
return Promise.resolve(code);
}
const resolvedDeps = Object.create(null);
const resolvedDepsArr = [];
return Promise.all(
module.dependencies.map(depName => {
return this._depGraph.resolveDependency(module, depName)
.then((dep) => dep && dep.getPlainObject().then(mod => {
if (mod) {
resolvedDeps[depName] = mod.id;
resolvedDepsArr.push(mod.id);
}
}));
})
return module.getDependencies().then(
dependencies => Promise.all(dependencies.map(
depName => this._depGraph.resolveDependency(module, depName)
.then(depModule => {
if (depModule) {
return depModule.getName().then(name => {
resolvedDeps[depName] = name;
resolvedDepsArr.push(name);
});
}
})
)
)
).then(() => {
const relativizeCode = (codeMatch, pre, quot, depName, post) => {
const depId = resolvedDeps[depName];
@ -157,13 +167,15 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) {
}
};
return defineModuleCode({
code: code
return module.getName().then(
name => defineModuleCode({
code: code
.replace(replacePatterns.IMPORT_RE, relativizeCode)
.replace(replacePatterns.REQUIRE_RE, relativizeCode),
deps: JSON.stringify(resolvedDepsArr),
moduleName: module.id,
});
deps: JSON.stringify(resolvedDepsArr),
moduleName: name,
})
);
});
};
@ -176,8 +188,7 @@ function defineModuleCode({moduleName, code, deps}) {
`__d(`,
`'${moduleName}',`,
`${deps},`,
'function(global, require, ',
'requireDynamic, requireLazy, module, exports) {',
'function(global, require, module, exports) {',
` ${code}`,
'\n});',
].join('');

View File

@ -376,6 +376,12 @@
var str = Array.prototype.map.call(arguments, function(arg) {
return inspect(arg, {depth: 10});
}).join(', ');
if (str.slice(0, 10) === "'Warning: " && level >= LOG_LEVELS.error) {
// React warnings use console.error so that a stack trace is shown,
// but we don't (currently) want these to show a redbox
// (Note: Logic duplicated in ExceptionsManager.js.)
level = LOG_LEVELS.warn;
}
global.nativeLoggingHook(str, level);
};
}

View File

@ -303,6 +303,18 @@
return _totalFactories;
};
/**
* Asynchronously loads any missing dependency and executes the provided
* callback once all of them are satisfied.
*
* Note that the dependencies on the provided array must be string literals
* as the packager uses this information to figure out how the modules are
* packaged into different bundles.
*/
require.ensure = function(dependencies, callback) {
throw '`require.ensure` is still not supported';
};
/**
* The define function conforming to CommonJS proposal:
* http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition
@ -464,56 +476,6 @@
}
}
/**
* Special version of define that executes the factory as soon as all
* dependencies are met.
*
* define() does just that, defines a module. Module's factory will not be
* called until required by other module. This makes sense for most of our
* library modules: we do not want to execute the factory unless it's being
* used by someone.
*
* On the other hand there are modules, that you can call "entrance points".
* You want to run the "factory" method for them as soon as all dependencies
* are met.
*
* @example
*
* define('BaseClass', [], function() { return ... });
* // ^^ factory for BaseClass was just stored in modulesMap
*
* define('SubClass', ['BaseClass'], function() { ... });
* // SubClass module is marked as ready (waiting == 0), factory is just
* // stored
*
* define('OtherClass, ['BaseClass'], function() { ... });
* // OtherClass module is marked as ready (waiting == 0), factory is just
* // stored
*
* requireLazy(['SubClass', 'ChatConfig'],
* function() { ... });
* // ChatRunner is waiting for ChatConfig to come
*
* define('ChatConfig', [], { foo: 'bar' });
* // at this point ChatRunner is marked as ready, and its factory
* // executed + all dependent factories are executed too: BaseClass,
* // SubClass, ChatConfig notice that OtherClass's factory won't be
* // executed unless explicitly required by someone
*
* @param {Array} dependencies
* @param {Object|Function} factory
*/
function requireLazy(dependencies, factory, context) {
return define(
dependencies,
factory,
undefined,
REQUIRE_WHEN_READY,
context,
1
);
}
function _uid() {
return '__mod__' + _counter++;
}
@ -595,12 +557,8 @@
_register('global', global);
_register('require', require);
_register('requireDynamic', require);
_register('requireLazy', requireLazy);
global.require = require;
global.requireDynamic = require;
global.requireLazy = requireLazy;
require.__debug = {
modules: modulesMap,
@ -621,8 +579,7 @@
* out for every module which would be a lot of extra bytes.
*/
global.__d = function(id, deps, factory, _special, _inlineRequires) {
var defaultDeps = ['global', 'require', 'requireDynamic', 'requireLazy',
'module', 'exports'];
var defaultDeps = ['global', 'require', 'module', 'exports'];
define(id, defaultDeps.concat(deps), factory, _special || USED_AS_TRANSPORT,
null, null, _inlineRequires);
};

View File

@ -11,3 +11,5 @@
exports.IMPORT_RE = /(\bimport\s+?(?:.+\s+?from\s+?)?)(['"])([^'"]+)(\2)/g;
exports.REQUIRE_RE = /(\brequire\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;
exports.SYSTEM_IMPORT_RE = /(\bSystem\.import\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;

View File

@ -8,19 +8,24 @@
*/
'use strict';
var fs = require('fs');
var Promise = require('promise');
var workerFarm = require('worker-farm');
var declareOpts = require('../lib/declareOpts');
var util = require('util');
var ModuleTransport = require('../lib/ModuleTransport');
const ModuleTransport = require('../lib/ModuleTransport');
const Promise = require('promise');
const declareOpts = require('../lib/declareOpts');
const fs = require('fs');
const util = require('util');
const workerFarm = require('worker-farm');
var readFile = Promise.denodeify(fs.readFile);
const readFile = Promise.denodeify(fs.readFile);
module.exports = Transformer;
Transformer.TransformError = TransformError;
// Avoid memory leaks caused in workers. This number seems to be a good enough number
// to avoid any memory leak while not slowing down initial builds.
// TODO(amasad): Once we get bundle splitting, we can drive this down a bit more.
const MAX_CALLS_PER_WORKER = 600;
var validateOpts = declareOpts({
// Worker will timeout if one of the callers timeout.
const DEFAULT_MAX_CALL_TIME = 30000;
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
@ -40,51 +45,58 @@ var validateOpts = declareOpts({
type: 'object',
required: true,
},
transformTimeoutInterval: {
type: 'number',
default: DEFAULT_MAX_CALL_TIME,
}
});
function Transformer(options) {
var opts = validateOpts(options);
class Transformer {
constructor(options) {
const opts = this._opts = validateOpts(options);
this._cache = opts.cache;
this._cache = opts.cache;
if (options.transformModulePath != null) {
this._workers = workerFarm(
{autoStart: true, maxConcurrentCallsPerWorker: 1},
options.transformModulePath
);
if (opts.transformModulePath != null) {
this._workers = workerFarm({
autoStart: true,
maxConcurrentCallsPerWorker: 1,
maxCallsPerWorker: MAX_CALLS_PER_WORKER,
maxCallTime: opts.transformTimeoutInterval,
}, opts.transformModulePath);
this._transform = Promise.denodeify(this._workers);
}
}
Transformer.prototype.kill = function() {
this._workers && workerFarm.end(this._workers);
};
Transformer.prototype.invalidateFile = function(filePath) {
this._cache.invalidate(filePath);
};
Transformer.prototype.loadFileAndTransform = function(filePath) {
if (this._transform == null) {
return Promise.reject(new Error('No transfrom module'));
this._transform = Promise.denodeify(this._workers);
}
}
var transform = this._transform;
return this._cache.get(filePath, 'transformedSource', function() {
// TODO: use fastfs to avoid reading file from disk again
return readFile(filePath)
.then(function(buffer) {
var sourceCode = buffer.toString();
kill() {
this._workers && workerFarm.end(this._workers);
}
return transform({
sourceCode: sourceCode,
filename: filePath,
}).then(
function(res) {
invalidateFile(filePath) {
this._cache.invalidate(filePath);
}
loadFileAndTransform(filePath) {
if (this._transform == null) {
return Promise.reject(new Error('No transfrom module'));
}
return this._cache.get(
filePath,
'transformedSource',
// TODO: use fastfs to avoid reading file from disk again
() => readFile(filePath).then(
buffer => {
const sourceCode = buffer.toString('utf8');
return this._transform({
sourceCode,
filename: filePath,
}).then(res => {
if (res.error) {
console.warn(
'Error property on the result value form the transformer',
'Error property on the result value from the transformer',
'module is deprecated and will be removed in future versions.',
'Please pass an error object as the first argument to the callback'
);
@ -97,13 +109,28 @@ Transformer.prototype.loadFileAndTransform = function(filePath) {
sourcePath: filePath,
sourceCode: sourceCode,
});
}
);
}).catch(function(err) {
throw formatError(err, filePath);
});
});
};
}).catch(err => {
if (err.type === 'TimeoutError') {
const timeoutErr = new Error(
`TimeoutError: transforming ${filePath} took longer than ` +
`${this._opts.transformTimeoutInterval / 1000} seconds.\n` +
`You can adjust timeout via the 'transformTimeoutInterval' option`
);
timeoutErr.type = 'TimeoutError';
throw timeoutErr;
}
throw formatError(err, filePath);
});
})
);
}
}
module.exports = Transformer;
Transformer.TransformError = TransformError;
function TransformError() {
Error.captureStackTrace && Error.captureStackTrace(this, TransformError);

View File

@ -1,300 +0,0 @@
/**
* 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';
var _ = require('underscore');
var base64VLQ = require('./base64-vlq');
var UglifyJS = require('uglify-js');
var ModuleTransport = require('../lib/ModuleTransport');
module.exports = Package;
var SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL=';
function Package(sourceMapUrl) {
this._finalized = false;
this._modules = [];
this._assets = [];
this._sourceMapUrl = sourceMapUrl;
this._shouldCombineSourceMaps = false;
}
Package.prototype.setMainModuleId = function(moduleId) {
this._mainModuleId = moduleId;
};
Package.prototype.addModule = function(module) {
if (!(module instanceof ModuleTransport)) {
throw new Error('Expeceted a ModuleTransport object');
}
// If we get a map from the transformer we'll switch to a mode
// were we're combining the source maps as opposed to
if (!this._shouldCombineSourceMaps && module.map != null) {
this._shouldCombineSourceMaps = true;
}
this._modules.push(module);
};
Package.prototype.addAsset = function(asset) {
this._assets.push(asset);
};
Package.prototype.finalize = function(options) {
options = options || {};
if (options.runMainModule) {
var runCode = ';require("' + this._mainModuleId + '");';
this.addModule(new ModuleTransport({
code: runCode,
virtual: true,
sourceCode: runCode,
sourcePath: 'RunMainModule.js'
}));
}
Object.freeze(this._modules);
Object.seal(this._modules);
Object.freeze(this._assets);
Object.seal(this._assets);
this._finalized = true;
};
Package.prototype._assertFinalized = function() {
if (!this._finalized) {
throw new Error('Package need to be finalized before getting any source');
}
};
Package.prototype._getSource = function() {
if (this._source == null) {
this._source = _.pluck(this._modules, 'code').join('\n');
}
return this._source;
};
Package.prototype._getInlineSourceMap = function() {
if (this._inlineSourceMap == null) {
var sourceMap = this.getSourceMap({excludeSource: true});
var encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
}
return this._inlineSourceMap;
};
Package.prototype.getSource = function(options) {
this._assertFinalized();
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap().code;
}
var source = this._getSource();
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap();
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
return source;
};
Package.prototype.getMinifiedSourceAndMap = function() {
this._assertFinalized();
var source = this._getSource();
try {
return UglifyJS.minify(source, {
fromString: true,
outSourceMap: 'bundle.js',
inSourceMap: this.getSourceMap(),
});
} catch(e) {
// Sometimes, when somebody is using a new syntax feature that we
// don't yet have transform for, the untransformed line is sent to
// uglify, and it chokes on it. This code tries to print the line
// and the module for easier debugging
var errorMessage = 'Error while minifying JS\n';
if (e.line) {
errorMessage += 'Transformed code line: "' +
source.split('\n')[e.line - 1] + '"\n';
}
if (e.pos) {
var fromIndex = source.lastIndexOf('__d(\'', e.pos);
if (fromIndex > -1) {
fromIndex += '__d(\''.length;
var toIndex = source.indexOf('\'', fromIndex);
errorMessage += 'Module name (best guess): ' +
source.substring(fromIndex, toIndex) + '\n';
}
}
errorMessage += e.toString();
throw new Error(errorMessage);
}
};
/**
* I found a neat trick in the sourcemap spec that makes it easy
* to concat sourcemaps. The `sections` field allows us to combine
* the sourcemap easily by adding an offset. Tested on chrome.
* Seems like it's not yet in Firefox but that should be fine for
* now.
*/
Package.prototype._getCombinedSourceMaps = function(options) {
var result = {
version: 3,
file: 'bundle.js',
sections: [],
};
var line = 0;
this._modules.forEach(function(module) {
var map = module.map;
if (module.virtual) {
map = generateSourceMapForVirtualModule(module);
}
if (options.excludeSource) {
map = _.extend({}, map, {sourcesContent: []});
}
result.sections.push({
offset: { line: line, column: 0 },
map: map,
});
line += module.code.split('\n').length;
});
return result;
};
Package.prototype.getSourceMap = function(options) {
this._assertFinalized();
options = options || {};
if (this._shouldCombineSourceMaps) {
return this._getCombinedSourceMaps(options);
}
var mappings = this._getMappings();
var map = {
file: 'bundle.js',
sources: _.pluck(this._modules, 'sourcePath'),
version: 3,
names: [],
mappings: mappings,
sourcesContent: options.excludeSource
? [] : _.pluck(this._modules, 'sourceCode')
};
return map;
};
Package.prototype.getAssets = function() {
return this._assets;
};
Package.prototype._getMappings = function() {
var modules = this._modules;
// The first line mapping in our package is basically the base64vlq code for
// zeros (A).
var firstLine = 'AAAA';
// Most other lines in our mappings are all zeros (for module, column etc)
// except for the lineno mappinp: curLineno - prevLineno = 1; Which is C.
var line = 'AACA';
var moduleLines = Object.create(null);
var mappings = '';
for (var i = 0; i < modules.length; i++) {
var module = modules[i];
var code = module.code;
var lastCharNewLine = false;
moduleLines[module.sourcePath] = 0;
for (var t = 0; t < code.length; t++) {
if (t === 0 && i === 0) {
mappings += firstLine;
} else if (t === 0) {
mappings += 'AC';
// This is the only place were we actually don't know the mapping ahead
// of time. When it's a new module (and not the first) the lineno
// mapping is 0 (current) - number of lines in prev module.
mappings += base64VLQ.encode(
0 - moduleLines[modules[i - 1].sourcePath]
);
mappings += 'A';
} else if (lastCharNewLine) {
moduleLines[module.sourcePath]++;
mappings += line;
}
lastCharNewLine = code[t] === '\n';
if (lastCharNewLine) {
mappings += ';';
}
}
if (i !== modules.length - 1) {
mappings += ';';
}
}
return mappings;
};
Package.prototype.getJSModulePaths = function() {
return this._modules.filter(function(module) {
// Filter out non-js files. Like images etc.
return !module.virtual;
}).map(function(module) {
return module.sourcePath;
});
};
Package.prototype.getDebugInfo = function() {
return [
'<div><h3>Main Module:</h3> ' + this._mainModuleId + '</div>',
'<style>',
'pre.collapsed {',
' height: 10px;',
' width: 100px;',
' display: block;',
' text-overflow: ellipsis;',
' overflow: hidden;',
' cursor: pointer;',
'}',
'</style>',
'<h3> Module paths and transformed code: </h3>',
this._modules.map(function(m) {
return '<div> <h4> Path: </h4>' + m.sourcePath + '<br/> <h4> Source: </h4>' +
'<code><pre class="collapsed" onclick="this.classList.remove(\'collapsed\')">' +
_.escape(m.code) + '</pre></code></div>';
}).join('\n'),
].join('\n');
};
function generateSourceMapForVirtualModule(module) {
// All lines map 1-to-1
var mappings = 'AAAA;';
for (var i = 1; i < module.code.split('\n').length; i++) {
mappings += 'AACA;';
}
return {
version: 3,
sources: [ module.sourcePath ],
names: [],
mappings: mappings,
file: module.sourcePath,
sourcesContent: [ module.sourceCode ],
};
}

View File

@ -1,289 +0,0 @@
/**
* 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';
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var Promise = require('promise');
var Cache = require('../Cache');
var Transformer = require('../JSTransformer');
var DependencyResolver = require('../DependencyResolver');
var Package = require('./Package');
var Activity = require('../Activity');
var ModuleTransport = require('../lib/ModuleTransport');
var declareOpts = require('../lib/declareOpts');
var imageSize = require('image-size');
var sizeOf = Promise.denodeify(imageSize);
var readFile = Promise.denodeify(fs.readFile);
var 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: ['png'],
},
fileWatcher: {
type: 'object',
required: true,
},
assetServer: {
type: 'object',
required: true,
}
});
function Packager(options) {
var opts = this._opts = validateOpts(options);
opts.projectRoots.forEach(verifyRootExists);
this._cache = opts.nonPersistent
? new DummyCache()
: new Cache({
resetCache: opts.resetCache,
cacheVersion: opts.cacheVersion,
projectRoots: opts.projectRoots,
transformModulePath: opts.transformModulePath,
});
this._resolver = new DependencyResolver({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
polyfillModuleNames: opts.polyfillModuleNames,
nonPersistent: opts.nonPersistent,
moduleFormat: opts.moduleFormat,
assetRoots: opts.assetRoots,
fileWatcher: opts.fileWatcher,
assetExts: opts.assetExts,
cache: this._cache,
});
this._transformer = new Transformer({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
cache: this._cache,
transformModulePath: opts.transformModulePath,
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
}
Packager.prototype.kill = function() {
this._transformer.kill();
return this._cache.end();
};
Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) {
var ppackage = new Package(sourceMapUrl);
var transformModule = this._transformModule.bind(this, ppackage);
var findEventId = Activity.startEvent('find dependencies');
var transformEventId;
return this.getDependencies(main, isDev)
.then(function(result) {
Activity.endEvent(findEventId);
transformEventId = Activity.startEvent('transform');
ppackage.setMainModuleId(result.mainModuleId);
return Promise.all(
result.dependencies.map(transformModule)
);
})
.then(function(transformedModules) {
Activity.endEvent(transformEventId);
transformedModules.forEach(function(moduleTransport) {
ppackage.addModule(moduleTransport);
});
ppackage.finalize({ runMainModule: runModule });
return ppackage;
});
};
Packager.prototype.invalidateFile = function(filePath) {
this._transformer.invalidateFile(filePath);
};
Packager.prototype.getDependencies = function(main, isDev) {
return this._resolver.getDependencies(main, { dev: isDev });
};
Packager.prototype._transformModule = function(ppackage, module) {
var transform;
if (module.isAsset_DEPRECATED) {
transform = this.generateAssetModule_DEPRECATED(ppackage, module);
} else if (module.isAsset) {
transform = this.generateAssetModule(ppackage, module);
} else if (module.isJSON) {
transform = generateJSONModule(module);
} else {
transform = this._transformer.loadFileAndTransform(
path.resolve(module.path)
);
}
var resolver = this._resolver;
return transform.then(
transformed => resolver.wrapModule(module, transformed.code).then(
code => new ModuleTransport({
code: code,
map: transformed.map,
sourceCode: transformed.sourceCode,
sourcePath: transformed.sourcePath,
virtual: transformed.virtual,
})
)
);
};
Packager.prototype.getGraphDebugInfo = function() {
return this._resolver.getDebugInfo();
};
Packager.prototype.generateAssetModule_DEPRECATED = function(ppackage, module) {
return sizeOf(module.path).then(function(dimensions) {
var img = {
__packager_asset: true,
isStatic: true,
path: module.path,
uri: module.id.replace(/^[^!]+!/, ''),
width: dimensions.width / module.resolution,
height: dimensions.height / module.resolution,
deprecated: true,
};
ppackage.addAsset(img);
var code = 'module.exports = ' + JSON.stringify(img) + ';';
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
};
Packager.prototype.generateAssetModule = function(ppackage, module) {
var relPath = getPathRelativeToRoot(this._projectRoots, module.path);
return Promise.all([
sizeOf(module.path),
this._assetServer.getAssetData(relPath),
]).then(function(res) {
var dimensions = res[0];
var assetData = res[1];
var img = {
__packager_asset: true,
fileSystemLocation: path.dirname(module.path),
httpServerLocation: path.join('/assets', path.dirname(relPath)),
width: dimensions.width / module.resolution,
height: dimensions.height / module.resolution,
scales: assetData.scales,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
ppackage.addAsset(img);
var ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);';
var code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img));
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
};
function generateJSONModule(module) {
return readFile(module.path).then(function(data) {
var code = 'module.exports = ' + data.toString('utf8') + ';';
return new ModuleTransport({
code: code,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
function getPathRelativeToRoot(roots, absPath) {
for (var i = 0; i < roots.length; i++) {
var relPath = path.relative(roots[i], absPath);
if (relPath[0] !== '.') {
return relPath;
}
}
throw new Error(
'Expected root module to be relative to one of the project roots'
);
}
function verifyRootExists(root) {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
class DummyCache {
get(filepath, field, loaderCb) {
return loaderCb();
}
end(){}
invalidate(filepath){}
}
module.exports = Packager;

View File

@ -8,70 +8,55 @@
*/
'use strict';
jest.setMock('worker-farm', function() { return function() {}; })
jest.setMock('worker-farm', function() { return () => {}; })
.dontMock('os')
.dontMock('path')
.dontMock('url')
.setMock('timers', {
setImmediate: function(fn) {
return setTimeout(fn, 0);
}
})
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
.setMock('uglify-js')
.dontMock('../')
.setMock('chalk', { dim: function(s) { return s; } });
var Promise = require('promise');
const Promise = require('promise');
describe('processRequest', function() {
describe('processRequest', () => {
var server;
var Packager;
var Bundler;
var FileWatcher;
var options = {
const options = {
projectRoots: ['root'],
blacklistRE: null,
cacheVersion: null,
polyfillModuleNames: null
};
var makeRequest = function(requestHandler, requrl) {
return new Promise(function(resolve) {
requestHandler(
{ url: requrl },
{
setHeader: jest.genMockFunction(),
end: function(res) {
resolve(res);
}
},
{
next: function() {}
}
);
});
};
const makeRequest = (reqHandler, requrl) => new Promise(resolve =>
reqHandler(
{ url: requrl },
{
setHeader: jest.genMockFunction(),
end: res => resolve(res),
},
{ next: () => {} },
)
);
var invalidatorFunc = jest.genMockFunction();
var watcherFunc = jest.genMockFunction();
const invalidatorFunc = jest.genMockFunction();
const watcherFunc = jest.genMockFunction();
var requestHandler;
var triggerFileChange;
beforeEach(function() {
Packager = require('../../Packager');
beforeEach(() => {
Bundler = require('../../Bundler');
FileWatcher = require('../../FileWatcher');
Packager.prototype.package = jest.genMockFunction().mockImpl(function() {
return Promise.resolve({
getSource: function() {
return 'this is the source';
},
getSourceMap: function() {
return 'this is the source map';
},
});
});
Bundler.prototype.bundle = jest.genMockFunction().mockImpl(() =>
Promise.resolve({
getSource: () => 'this is the source',
getSourceMap: () => 'this is the source map',
})
);
FileWatcher.prototype.on = function(eventType, callback) {
if (eventType !== 'all') {
@ -82,130 +67,141 @@ describe('processRequest', function() {
return this;
};
Packager.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.invalidateFile = invalidatorFunc;
var Server = require('../');
const Server = require('../');
server = new Server(options);
requestHandler = server.processRequest.bind(server);
});
pit('returns JS bundle source on request of *.bundle',function() {
pit('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(function(response) {
expect(response).toEqual('this is the source');
});
).then(response =>
expect(response).toEqual('this is the source')
);
});
pit('returns JS bundle source on request of *.bundle (compat)',function() {
pit('returns JS bundle source on request of *.bundle (compat)', () => {
return makeRequest(
requestHandler,
'mybundle.runModule.bundle'
).then(function(response) {
expect(response).toEqual('this is the source');
});
).then(response =>
expect(response).toEqual('this is the source')
);
});
pit('returns sourcemap on request of *.map', function() {
pit('returns sourcemap on request of *.map', () => {
return makeRequest(
requestHandler,
'mybundle.map?runModule=true'
).then(function(response) {
expect(response).toEqual('"this is the source map"');
});
).then(response =>
expect(response).toEqual('"this is the source map"')
);
});
pit('works with .ios.js extension', function() {
pit('works with .ios.js extension', () => {
return makeRequest(
requestHandler,
'index.ios.includeRequire.bundle'
).then(function(response) {
).then(response => {
expect(response).toEqual('this is the source');
expect(Packager.prototype.package).toBeCalledWith(
expect(Bundler.prototype.bundle).toBeCalledWith(
'index.ios.js',
true,
'index.ios.includeRequire.map',
true
true,
undefined
);
});
});
pit('watches all files in projectRoot', function() {
pit('passes in the platform param', function() {
return makeRequest(
requestHandler,
'index.bundle?platform=ios'
).then(function(response) {
expect(response).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith(
'index.js',
true,
'index.map',
true,
'ios',
);
});
});
pit('watches all files in projectRoot', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(function() {
).then(() => {
expect(watcherFunc.mock.calls[0][0]).toEqual('all');
expect(watcherFunc.mock.calls[0][1]).not.toBe(null);
});
});
describe('file changes', function() {
pit('invalides files in package when file is updated', function() {
describe('file changes', () => {
pit('invalides files in bundle when file is updated', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(function() {
var onFileChange = watcherFunc.mock.calls[0][1];
).then(() => {
const onFileChange = watcherFunc.mock.calls[0][1];
onFileChange('all','path/file.js', options.projectRoots[0]);
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
pit('rebuilds the bundles that contain a file when that file is changed', () => {
const bundleFunc = jest.genMockFunction();
bundleFunc
.mockReturnValueOnce(
Promise.resolve({
getSource: function() {
return 'this is the first source';
},
getSourceMap: function() {},
getSource: () => 'this is the first source',
getSourceMap: () => {},
})
)
.mockReturnValue(
Promise.resolve({
getSource: function() {
return 'this is the rebuilt source';
},
getSourceMap: function() {},
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
})
);
Packager.prototype.package = packageFunc;
Bundler.prototype.bundle = bundleFunc;
var Server = require('../../Server');
const Server = require('../../Server');
server = new Server(options);
requestHandler = server.processRequest.bind(server);
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.then(function(response) {
.then(response => {
expect(response).toEqual('this is the first source');
expect(packageFunc.mock.calls.length).toBe(1);
expect(bundleFunc.mock.calls.length).toBe(1);
triggerFileChange('all','path/file.js', options.projectRoots[0]);
jest.runAllTimers();
jest.runAllTimers();
})
.then(function() {
expect(packageFunc.mock.calls.length).toBe(2);
.then(() => {
expect(bundleFunc.mock.calls.length).toBe(2);
return makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.then(function(response) {
expect(response).toEqual('this is the rebuilt source');
});
.then(response =>
expect(response).toEqual('this is the rebuilt source')
);
});
});
});
describe('/onchange endpoint', function() {
describe('/onchange endpoint', () => {
var EventEmitter;
var req;
var res;
beforeEach(function() {
beforeEach(() => {
EventEmitter = require.requireActual('events').EventEmitter;
req = new EventEmitter();
req.url = '/onchange';
@ -215,14 +211,14 @@ describe('processRequest', function() {
};
});
it('should hold on to request and inform on change', function() {
it('should hold on to request and inform on change', () => {
server.processRequest(req, res);
triggerFileChange('all', 'path/file.js', options.projectRoots[0]);
jest.runAllTimers();
expect(res.end).toBeCalledWith(JSON.stringify({changed: true}));
});
it('should not inform changes on disconnected clients', function() {
it('should not inform changes on disconnected clients', () => {
server.processRequest(req, res);
req.emit('close');
jest.runAllTimers();
@ -232,56 +228,52 @@ describe('processRequest', function() {
});
});
describe('/assets endpoint', function() {
describe('/assets endpoint', () => {
var AssetServer;
beforeEach(function() {
beforeEach(() => {
AssetServer = require('../../AssetServer');
});
it('should serve simple case', function() {
var req = {
url: '/assets/imgs/a.png',
};
var res = {
end: jest.genMockFn(),
};
it('should serve simple case', () => {
const req = {url: '/assets/imgs/a.png'};
const res = {end: jest.genMockFn()};
AssetServer.prototype.get.mockImpl(function() {
return Promise.resolve('i am image');
});
AssetServer.prototype.get.mockImpl(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(res.end).toBeCalledWith('i am image');
});
it('should return 404', function() {
it('should return 404', () => {
});
});
describe('buildPackage(options)', function() {
it('Calls the packager with the correct args', function() {
server.buildPackage({
describe('buildbundle(options)', () => {
it('Calls the bundler with the correct args', () => {
server.buildBundle({
entryFile: 'foo file'
});
expect(Packager.prototype.package).toBeCalledWith(
expect(Bundler.prototype.bundle).toBeCalledWith(
'foo file',
true,
undefined,
true
true,
undefined
);
});
});
describe('buildPackageFromUrl(options)', function() {
it('Calls the packager with the correct args', function() {
server.buildPackageFromUrl('/path/to/foo.bundle?dev=false&runModule=false');
expect(Packager.prototype.package).toBeCalledWith(
describe('buildBundleFromUrl(options)', () => {
it('Calls the bundler with the correct args', () => {
server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false');
expect(Bundler.prototype.bundle).toBeCalledWith(
'path/to/foo.js',
false,
'/path/to/foo.map',
false
false,
undefined
);
});
});

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 Packager = require('../Packager');
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,
@ -62,117 +61,13 @@ var validateOpts = declareOpts({
type: 'array',
default: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp'],
},
transformTimeoutInterval: {
type: 'number',
required: false,
},
});
function Server(options) {
var opts = validateOpts(options);
this._projectRoots = opts.projectRoots;
this._packages = 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 packagerOpts = Object.create(opts);
packagerOpts.fileWatcher = this._fileWatcher;
packagerOpts.assetServer = this._assetServer;
this._packager = new Packager(packagerOpts);
var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.on('all', onFileChange);
var self = this;
this._debouncedFileChangeHandler = _.debounce(function(filePath) {
self._rebuildPackages(filePath);
self._informChangeWatchers();
}, 50);
}
Server.prototype._onFileChange = function(type, filepath, root) {
var absPath = path.join(root, filepath);
this._packager.invalidateFile(absPath);
// Make sure the file watcher event runs through the system before
// we rebuild the packages.
this._debouncedFileChangeHandler(absPath);
};
Server.prototype._rebuildPackages = function() {
var buildPackage = this.buildPackage.bind(this);
var packages = this._packages;
Object.keys(packages).forEach(function(optionsJson) {
var options = JSON.parse(optionsJson);
// Wait for a previous build (if exists) to finish.
packages[optionsJson] = (packages[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.
packages[optionsJson] = buildPackage(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 packages[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._packager.kill(),
]);
};
var packageOpts = declareOpts({
const bundleOpts = declareOpts({
sourceMapUrl: {
type: 'string',
required: false,
@ -197,247 +92,365 @@ var packageOpts = declareOpts({
type: 'boolean',
default: false,
},
platform: {
type: 'string',
required: false,
}
});
Server.prototype.buildPackage = function(options) {
var opts = packageOpts(options);
class Server {
constructor(options) {
const opts = validateOpts(options);
return this._packager.package(
opts.entryFile,
opts.runModule,
opts.sourceMapUrl,
opts.dev
);
};
this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
Server.prototype.buildPackageFromUrl = function(reqUrl) {
var options = getOptionsFromUrl(reqUrl);
return this.buildPackage(options);
};
const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext);
Server.prototype.getDependencies = function(main) {
return this._packager.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/packages">Cached Packages</a></div>';
ret += '<div><a href="/debug/graph">Dependency Graph</a></div>';
res.end(ret);
} else if (parts[1] === 'packages') {
ret += '<h1> Cached Packages </h1>';
Promise.all(Object.keys(this._packages).map(function(optionsJson) {
return this._packages[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._packager.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,
opts.platform
);
}
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._packages[optionsJson] || this.buildPackage(options);
this._packages[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, packageID, 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._packages[packageID];
}
} 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(/\.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 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);
} 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.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';
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
),
platform: urlObj.query.platform,
};
}
_getBoolOptionFromQuery(query, opt, defaultVal) {
if (query[opt] == null && defaultVal != null) {
return defaultVal;
}
return query[opt] === 'true' || query[opt] === '1';
}
}
module.exports = Server;

View File

@ -0,0 +1,101 @@
/**
* 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 Bundle = require('../Bundler/Bundle');
const Promise = require('promise');
const bser = require('bser');
const debug = require('debug')('ReactPackager:SocketClient');
const net = require('net');
class SocketClient {
static create(sockPath) {
return new SocketClient(sockPath).onReady();
}
constructor(sockPath) {
debug('connecting to', sockPath);
this._sock = net.connect(sockPath);
this._ready = new Promise((resolve, reject) => {
this._sock.on('connect', () => resolve(this));
this._sock.on('error', (e) => reject(e));
});
this._resolvers = Object.create(null);
const bunser = new bser.BunserBuf();
this._sock.on('data', (buf) => bunser.append(buf));
bunser.on('value', (message) => this._handleMessage(message));
}
onReady() {
return this._ready;
}
getDependencies(main) {
return this._send({
type: 'getDependencies',
data: main,
});
}
buildBundle(options) {
return this._send({
type: 'buildBundle',
data: options,
}).then(json => Bundle.fromJSON(json));
}
_send(message) {
message.id = uid();
this._sock.write(bser.dumpToBuffer(message));
return new Promise((resolve, reject) => {
this._resolvers[message.id] = {resolve, reject};
});
}
_handleMessage(message) {
if (!(message && message.id && message.type)) {
throw new Error(
'Malformed message from server ' + JSON.stringify(message)
);
}
debug('got message with type', message.type);
const resolver = this._resolvers[message.id];
if (!resolver) {
throw new Error(
'Unrecognized message id (message already resolved or never existed'
);
}
delete this._resolvers[message.id];
if (message.type === 'error') {
// TODO convert to an error
resolver.reject(message.data);
} else {
resolver.resolve(message.data);
}
}
close() {
debug('closing connection');
this._sock.end();
}
}
module.exports = SocketClient;
function uid(len) {
len = len || 7;
return Math.random().toString(35).substr(2, len);
}

View File

@ -0,0 +1,141 @@
/**
* 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 Promise = require('promise');
const Server = require('../Server');
const bser = require('bser');
const debug = require('debug')('ReactPackager:SocketServer');
const fs = require('fs');
const net = require('net');
const MAX_IDLE_TIME = 10 * 60 * 1000;
class SocketServer {
constructor(sockPath, options) {
this._server = net.createServer();
this._server.listen(sockPath);
this._ready = new Promise((resolve, reject) => {
this._server.on('error', (e) => reject(e));
this._server.on('listening', () => {
debug(
'Process %d listening on socket path %s ' +
'for server with options %j',
process.pid,
sockPath,
options
);
resolve(this);
});
});
this._server.on('connection', (sock) => this._handleConnection(sock));
// Disable the file watcher.
options.nonPersistent = true;
this._packagerServer = new Server(options);
this._jobs = 0;
this._dieEventually();
process.on('exit', () => fs.unlinkSync(sockPath));
}
onReady() {
return this._ready;
}
_handleConnection(sock) {
debug('connection to server', process.pid);
const bunser = new bser.BunserBuf();
sock.on('data', (buf) => bunser.append(buf));
bunser.on('value', (m) => this._handleMessage(sock, m));
}
_handleMessage(sock, m) {
if (!m || !m.id || !m.data) {
console.error('SocketServer recieved a malformed message: %j', m);
}
debug('got request', m);
// Debounce the kill timer.
this._dieEventually();
const handleError = (error) => {
debug('request error', error);
this._jobs--;
this._reply(sock, m.id, 'error', error.stack);
};
switch (m.type) {
case 'getDependencies':
this._jobs++;
this._packagerServer.getDependencies(m.data).then(
({ dependencies }) => this._reply(sock, m.id, 'result', dependencies),
handleError,
);
break;
case 'buildBundle':
this._jobs++;
this._packagerServer.buildBundle(m.data).then(
(result) => this._reply(sock, m.id, 'result', result),
handleError,
);
break;
default:
this._reply(sock, m.id, 'error', 'Unknown message type: ' + m.type);
}
}
_reply(sock, id, type, data) {
debug('request finished', type);
this._jobs--;
data = toJSON(data);
sock.write(bser.dumpToBuffer({
id,
type,
data,
}));
}
_dieEventually() {
clearTimeout(this._deathTimer);
this._deathTimer = setTimeout(() => {
if (this._jobs <= 0) {
debug('server dying', process.pid);
process.exit(1);
}
this._dieEventually();
}, MAX_IDLE_TIME);
}
}
module.exports = SocketServer;
// TODO move this to bser code.
function toJSON(object) {
if (!(object && typeof object === 'object')) {
return object;
}
if (object.toJSON) {
return object.toJSON();
}
for (var p in object) {
object[p] = toJSON(object[p]);
}
return object;
}

View File

@ -0,0 +1,112 @@
/**
* 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';
jest.setMock('worker-farm', function() { return () => {}; })
.setMock('uglify-js')
.mock('net')
.dontMock('../SocketClient');
describe('SocketClient', () => {
let SocketClient;
let sock;
let bunser;
beforeEach(() => {
SocketClient = require('../SocketClient');
const {EventEmitter} = require.requireActual('events');
sock = new EventEmitter();
sock.write = jest.genMockFn();
require('net').connect.mockImpl(() => sock);
const bser = require('bser');
bunser = new EventEmitter();
require('bser').BunserBuf.mockImpl(() => bunser);
bser.dumpToBuffer.mockImpl((a) => a);
require('../../Bundler/Bundle').fromJSON.mockImpl((a) => a);
});
pit('create a connection', () => {
const client = new SocketClient('/sock');
sock.emit('connect');
return client.onReady().then(c => {
expect(c).toBe(client);
expect(require('net').connect).toBeCalledWith('/sock');
});
});
pit('buildBundle', () => {
const client = new SocketClient('/sock');
sock.emit('connect');
const options = { entryFile: '/main' };
const promise = client.buildBundle(options);
expect(sock.write).toBeCalled();
const message = sock.write.mock.calls[0][0];
expect(message.type).toBe('buildBundle');
expect(message.data).toEqual(options);
expect(typeof message.id).toBe('string');
bunser.emit('value', {
id: message.id,
type: 'result',
data: { bundle: 'foo' },
});
return promise.then(bundle => expect(bundle).toEqual({ bundle: 'foo' }));
});
pit('getDependencies', () => {
const client = new SocketClient('/sock');
sock.emit('connect');
const main = '/main';
const promise = client.getDependencies(main);
expect(sock.write).toBeCalled();
const message = sock.write.mock.calls[0][0];
expect(message.type).toBe('getDependencies');
expect(message.data).toEqual(main);
expect(typeof message.id).toBe('string');
bunser.emit('value', {
id: message.id,
type: 'result',
data: ['a', 'b', 'c'],
});
return promise.then(result => expect(result).toEqual(['a', 'b', 'c']));
});
pit('handle errors', () => {
const client = new SocketClient('/sock');
sock.emit('connect');
const main = '/main';
const promise = client.getDependencies(main);
expect(sock.write).toBeCalled();
const message = sock.write.mock.calls[0][0];
expect(message.type).toBe('getDependencies');
expect(message.data).toEqual(main);
expect(typeof message.id).toBe('string');
bunser.emit('value', {
id: message.id,
type: 'error',
data: 'some error'
});
return promise.catch(m => expect(m).toBe('some error'));
});
});

View File

@ -0,0 +1,87 @@
/**
* 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';
jest.setMock('worker-farm', function() { return () => {}; })
.setMock('uglify-js')
.mock('child_process')
.dontMock('../');
describe('SocketInterface', () => {
let SocketInterface;
let SocketClient;
beforeEach(() => {
SocketInterface = require('../');
SocketClient = require('../SocketClient');
});
describe('getOrCreateSocketFor', () => {
pit('creates socket path by hashing options', () => {
const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => true);
// Check that given two equivelant server options, we end up with the same
// socket path.
const options1 = { projectRoots: ['/root'], transformModulePath: '/root/foo' };
const options2 = { transformModulePath: '/root/foo', projectRoots: ['/root'] };
const options3 = { projectRoots: ['/root', '/root2'] };
return SocketInterface.getOrCreateSocketFor(options1).then(() => {
expect(SocketClient.create).toBeCalled();
return SocketInterface.getOrCreateSocketFor(options2).then(() => {
expect(SocketClient.create.mock.calls.length).toBe(2);
expect(SocketClient.create.mock.calls[0]).toEqual(SocketClient.create.mock.calls[1]);
return SocketInterface.getOrCreateSocketFor(options3).then(() => {
expect(SocketClient.create.mock.calls.length).toBe(3);
expect(SocketClient.create.mock.calls[1]).not.toEqual(SocketClient.create.mock.calls[2]);
});
});
});
});
pit('should fork a server', () => {
const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => false);
let sockPath;
let callback;
require('child_process').spawn.mockImpl(() => ({
on: (event, cb) => callback = cb,
send: (message) => {
expect(message.type).toBe('createSocketServer');
expect(message.data.options).toEqual({ projectRoots: ['/root'] });
expect(message.data.sockPath).toContain('react-packager');
sockPath = message.data.sockPath;
setImmediate(() => callback({ type: 'createdServer' }));
},
unref: () => undefined,
disconnect: () => undefined,
}));
return SocketInterface.getOrCreateSocketFor({ projectRoots: ['/root'] })
.then(() => {
expect(SocketClient.create).toBeCalledWith(sockPath);
});
});
});
describe('createSocketServer', () => {
pit('creates a server', () => {
require('../SocketServer').mockImpl((sockPath, options) => {
expect(sockPath).toBe('/socket');
expect(options).toEqual({ projectRoots: ['/root'] });
return { onReady: () => Promise.resolve() };
});
return SocketInterface.createSocketServer('/socket', { projectRoots: ['/root'] });
});
});
});

View File

@ -0,0 +1,94 @@
/**
* 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';
jest.setMock('worker-farm', function() { return () => {}; })
.setMock('uglify-js')
.mock('net')
.mock('fs')
.dontMock('../SocketServer');
describe('SocketServer', () => {
let PackagerServer;
let SocketServer;
let netServer;
let bunser;
beforeEach(() => {
SocketServer = require('../SocketServer');
const {EventEmitter} = require.requireActual('events');
netServer = new EventEmitter();
netServer.listen = jest.genMockFn();
require('net').createServer.mockImpl(() => netServer);
const bser = require('bser');
bunser = new EventEmitter();
bser.BunserBuf.mockImpl(() => bunser);
bser.dumpToBuffer.mockImpl((a) => a);
PackagerServer = require('../../Server');
});
pit('create a server', () => {
const server = new SocketServer('/sock', { projectRoots: ['/root'] });
netServer.emit('listening');
return server.onReady().then(s => {
expect(s).toBe(server);
expect(netServer.listen).toBeCalledWith('/sock');
});
});
pit('handles getDependencies message', () => {
const server = new SocketServer('/sock', { projectRoots: ['/root'] });
netServer.emit('listening');
return server.onReady().then(() => {
const sock = { on: jest.genMockFn(), write: jest.genMockFn() };
netServer.emit('connection', sock);
PackagerServer.prototype.getDependencies.mockImpl(
() => Promise.resolve({ dependencies: ['a', 'b', 'c'] })
);
bunser.emit('value', { type: 'getDependencies', id: 1, data: '/main' });
expect(PackagerServer.prototype.getDependencies).toBeCalledWith('/main');
// Run pending promises.
return Promise.resolve().then(() => {
expect(sock.write).toBeCalledWith(
{ id: 1, type: 'result', data: ['a', 'b', 'c']}
);
});
});
});
pit('handles buildBundle message', () => {
const server = new SocketServer('/sock', { projectRoots: ['/root'] });
netServer.emit('listening');
return server.onReady().then(() => {
const sock = { on: jest.genMockFn(), write: jest.genMockFn() };
netServer.emit('connection', sock);
PackagerServer.prototype.buildBundle.mockImpl(
() => Promise.resolve({ bundle: 'foo' })
);
bunser.emit(
'value',
{ type: 'buildBundle', id: 1, data: { options: 'bar' } }
);
expect(PackagerServer.prototype.buildBundle).toBeCalledWith(
{ options: 'bar' }
);
// Run pending promises.
return Promise.resolve().then(() => {
expect(sock.write).toBeCalledWith(
{ id: 1, type: 'result', data: { bundle: 'foo' }}
);
});
});
});
});

View File

@ -0,0 +1,102 @@
/**
* 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 Promise = require('promise');
const SocketClient = require('./SocketClient');
const SocketServer = require('./SocketServer');
const _ = require('underscore');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const tmpdir = require('os').tmpdir();
const {spawn} = require('child_process');
const CREATE_SERVER_TIMEOUT = 10000;
const SocketInterface = {
getOrCreateSocketFor(options) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
Object.keys(options).sort().forEach(key => {
if (options[key] && typeof options[key] !== 'string') {
hash.update(JSON.stringify(options[key]));
} else {
hash.update(options[key]);
}
});
const sockPath = path.join(
tmpdir,
'react-packager-' + hash.digest('hex')
);
if (fs.existsSync(sockPath)) {
resolve(SocketClient.create(sockPath));
return;
}
const logPath = path.join(tmpdir, 'react-packager.log');
const timeout = setTimeout(
() => reject(
new Error(
'Took too long to start server. Server logs: \n' +
fs.readFileSync(logPath, 'utf8')
)
),
CREATE_SERVER_TIMEOUT,
);
const log = fs.openSync(logPath, 'a');
// Enable server debugging by default since it's going to a log file.
const env = _.clone(process.env);
env.DEBUG = 'ReactPackager:SocketServer';
// We have to go through the main entry point to make sure
// we go through the babel require hook.
const child = spawn(
process.execPath,
[path.join(__dirname, '..', '..', 'index.js')],
{
detached: true,
env: env,
stdio: ['ipc', log, log]
}
);
child.unref();
child.on('message', m => {
if (m && m.type && m.type === 'createdServer') {
clearTimeout(timeout);
child.disconnect();
resolve(SocketClient.create(sockPath));
}
});
if (options.blacklistRE) {
options.blacklistRE = { source: options.blacklistRE.source };
}
child.send({
type: 'createSocketServer',
data: { sockPath, options }
});
});
},
createSocketServer(sockPath, options) {
return new SocketServer(sockPath, options).onReady();
}
};
module.exports = SocketInterface;

View File

@ -10,7 +10,14 @@
var fs = jest.genMockFromModule('fs');
function asyncCallback(callback) {
return function() {
setImmediate(() => callback.apply(this, arguments));
};
}
fs.realpath.mockImpl(function(filepath, callback) {
callback = asyncCallback(callback);
var node;
try {
node = getToNode(filepath);
@ -24,6 +31,7 @@ fs.realpath.mockImpl(function(filepath, callback) {
});
fs.readdir.mockImpl(function(filepath, callback) {
callback = asyncCallback(callback);
var node;
try {
node = getToNode(filepath);
@ -42,6 +50,7 @@ fs.readdir.mockImpl(function(filepath, callback) {
});
fs.readFile.mockImpl(function(filepath, encoding, callback) {
callback = asyncCallback(callback);
if (arguments.length === 2) {
callback = encoding;
encoding = null;
@ -60,6 +69,7 @@ fs.readFile.mockImpl(function(filepath, encoding, callback) {
});
fs.stat.mockImpl(function(filepath, callback) {
callback = asyncCallback(callback);
var node;
try {
node = getToNode(filepath);

View File

@ -13,6 +13,12 @@
var babel = require('babel-core');
function transform(srcTxt, filename, options) {
var plugins = [];
if (process.env.NODE_ENV === 'production') {
plugins = plugins.concat(['node-env-inline', 'dunderscore-dev-inline']);
}
var result = babel.transform(srcTxt, {
retainLines: true,
compact: true,
@ -35,6 +41,7 @@ function transform(srcTxt, filename, options) {
'react',
'regenerator',
],
plugins: plugins,
sourceFileName: filename,
sourceMaps: false,
extra: options || {},