diff --git a/debugger.html b/debugger.html index 8d377f64..24f8aea5 100644 --- a/debugger.html +++ b/debugger.html @@ -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 ⌘R 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) {

- React Native JS code runs inside this Chrome tab + React Native JS code runs inside this Chrome tab.

Press ⌘⌥J to open Developer Tools. Enable Pause On Caught Exceptions for a better debugging experience.

-

Status: Loading

+

Status: Loading...

diff --git a/getFlowTypeCheckMiddleware.js b/getFlowTypeCheckMiddleware.js index c7f3e2b1..f3481551 100644 --- a/getFlowTypeCheckMiddleware.js +++ b/getFlowTypeCheckMiddleware.js @@ -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; diff --git a/react-packager/__mocks__/net.js b/react-packager/__mocks__/net.js deleted file mode 100644 index 43f51828..00000000 --- a/react-packager/__mocks__/net.js +++ /dev/null @@ -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; -}; diff --git a/react-packager/example_project/bar.js b/react-packager/example_project/bar.js deleted file mode 100644 index 6653bdf7..00000000 --- a/react-packager/example_project/bar.js +++ /dev/null @@ -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; diff --git a/react-packager/example_project/config.json b/react-packager/example_project/config.json deleted file mode 100644 index 0acdcb51..00000000 --- a/react-packager/example_project/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "port": 3000, - "devPort": 3001, - "publicDir": "./public", - "rootPath": "../example_project", - "moduleOptions": { - "format": "haste", - "main": "index.js" - } -} diff --git a/react-packager/example_project/foo/foo.js b/react-packager/example_project/foo/foo.js deleted file mode 100644 index fe3c8cd1..00000000 --- a/react-packager/example_project/foo/foo.js +++ /dev/null @@ -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); -}; diff --git a/react-packager/example_project/index.js b/react-packager/example_project/index.js deleted file mode 100644 index d63b5193..00000000 --- a/react-packager/example_project/index.js +++ /dev/null @@ -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'); diff --git a/react-packager/example_project/js/Channel.js b/react-packager/example_project/js/Channel.js deleted file mode 100644 index 6cbfce6f..00000000 --- a/react-packager/example_project/js/Channel.js +++ /dev/null @@ -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; diff --git a/react-packager/example_project/js/XHR.js b/react-packager/example_project/js/XHR.js deleted file mode 100644 index bede8ca5..00000000 --- a/react-packager/example_project/js/XHR.js +++ /dev/null @@ -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); -}; diff --git a/react-packager/example_project/js/code.js b/react-packager/example_project/js/code.js deleted file mode 100644 index f99a90c9..00000000 --- a/react-packager/example_project/js/code.js +++ /dev/null @@ -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')); -}); diff --git a/react-packager/example_project/js/main.js b/react-packager/example_project/js/main.js deleted file mode 100644 index 405d015e..00000000 --- a/react-packager/example_project/js/main.js +++ /dev/null @@ -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(); -}); diff --git a/react-packager/example_project/public/css/index.css b/react-packager/example_project/public/css/index.css deleted file mode 100644 index 651f3326..00000000 --- a/react-packager/example_project/public/css/index.css +++ /dev/null @@ -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; -} diff --git a/react-packager/example_project/public/index.html b/react-packager/example_project/public/index.html deleted file mode 100644 index e0e2ce7f..00000000 --- a/react-packager/example_project/public/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/react-packager/index.js b/react-packager/index.js index c47d762a..c3e5829f 100644 --- a/react-packager/index.js +++ b/react-packager/index.js @@ -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) { diff --git a/react-packager/src/Activity/__tests__/Activity-test.js b/react-packager/src/Activity/__tests__/Activity-test.js index e8fda3dc..08254f79 100644 --- a/react-packager/src/Activity/__tests__/Activity-test.js +++ b/react-packager/src/Activity/__tests__/Activity-test.js @@ -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)); }); diff --git a/react-packager/src/Activity/index.js b/react-packager/src/Activity/index.js index 8e593f9f..eccebd28 100644 --- a/react-packager/src/Activity/index.js +++ b/react-packager/src/Activity/index.js @@ -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 + '] ' + ' ' + startAction.eventName + @@ -171,3 +175,4 @@ exports.endEvent = endEvent; exports.signal = signal; exports.startEvent = startEvent; exports.disable = disable; +exports.eventEmitter = _eventEmitter; diff --git a/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 95916c9e..54ef7012 100644 --- a/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -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', diff --git a/react-packager/src/AssetServer/index.js b/react-packager/src/AssetServer/index.js index 2cd365fd..f442f6b8 100644 --- a/react-packager/src/AssetServer/index.js +++ b/react-packager/src/AssetServer/index.js @@ -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; diff --git a/react-packager/src/Bundler/Bundle.js b/react-packager/src/Bundler/Bundle.js new file mode 100644 index 00000000..b7920ebb --- /dev/null +++ b/react-packager/src/Bundler/Bundle.js @@ -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 [ + '

Main Module:

' + this._mainModuleId + '
', + '', + '

Module paths and transformed code:

', + this._modules.map(function(m) { + return '

Path:

' + m.sourcePath + '

Source:

' + + '
'; + }).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; diff --git a/react-packager/src/Packager/__tests__/Package-test.js b/react-packager/src/Bundler/__tests__/Bundle-test.js similarity index 82% rename from react-packager/src/Packager/__tests__/Package-test.js rename to react-packager/src/Bundler/__tests__/Bundle-test.js index d43c65c0..74d18924 100644 --- a/react-packager/src/Packager/__tests__/Package-test.js +++ b/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -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 diff --git a/react-packager/src/Packager/__tests__/Packager-test.js b/react-packager/src/Bundler/__tests__/Bundler-test.js similarity index 76% rename from react-packager/src/Packager/__tests__/Packager-test.js rename to react-packager/src/Bundler/__tests__/Bundler-test.js index 00c76870..f70eb4b5 100644 --- a/react-packager/src/Packager/__tests__/Packager-test.js +++ b/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -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 }) + ); }); }); diff --git a/react-packager/src/Packager/base64-vlq.js b/react-packager/src/Bundler/base64-vlq.js similarity index 100% rename from react-packager/src/Packager/base64-vlq.js rename to react-packager/src/Bundler/base64-vlq.js diff --git a/react-packager/src/Bundler/index.js b/react-packager/src/Bundler/index.js new file mode 100644 index 00000000..d29ab0f8 --- /dev/null +++ b/react-packager/src/Bundler/index.js @@ -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; diff --git a/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js b/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js new file mode 100644 index 00000000..cca9c8a7 --- /dev/null +++ b/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js @@ -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'); + }); + }); + }); +}); diff --git a/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js b/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js new file mode 100644 index 00000000..f834ccf5 --- /dev/null +++ b/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js @@ -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; + } +}); diff --git a/react-packager/src/BundlesLayout/index.js b/react-packager/src/BundlesLayout/index.js new file mode 100644 index 00000000..c6da3157 --- /dev/null +++ b/react-packager/src/BundlesLayout/index.js @@ -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; diff --git a/react-packager/src/Cache/__mocks__/Cache.js b/react-packager/src/Cache/__mocks__/Cache.js index 6f7632f6..1376f275 100644 --- a/react-packager/src/Cache/__mocks__/Cache.js +++ b/react-packager/src/Cache/__mocks__/Cache.js @@ -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, +}; diff --git a/react-packager/src/Cache/index.js b/react-packager/src/Cache/index.js index 2ed3575e..ae6d1aa3 100644 --- a/react-packager/src/Cache/index.js +++ b/react-packager/src/Cache/index.js @@ -215,6 +215,7 @@ class Cache { hash.update(options.transformModulePath); var name = 'react-packager-cache-' + hash.digest('hex'); + return path.join(tmpdir, name); } } diff --git a/react-packager/src/DependencyResolver/AssetModule.js b/react-packager/src/DependencyResolver/AssetModule.js index bfe4b6f8..2e103159 100644 --- a/react-packager/src/DependencyResolver/AssetModule.js +++ b/react-packager/src/DependencyResolver/AssetModule.js @@ -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; diff --git a/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js b/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js index fd4cb708..19817d4b 100644 --- a/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js +++ b/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js @@ -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; diff --git a/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index c5f06032..eab91383 100644 --- a/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -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([ { diff --git a/react-packager/src/DependencyResolver/DependencyGraph/index.js b/react-packager/src/DependencyResolver/DependencyGraph/index.js index 7e718582..f2b77f4d 100644 --- a/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -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) { diff --git a/react-packager/src/DependencyResolver/Module.js b/react-packager/src/DependencyResolver/Module.js index 3f1b13ef..745ab3fb 100644 --- a/react-packager/src/DependencyResolver/Module.js +++ b/react-packager/src/DependencyResolver/Module.js @@ -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*/ { - 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; diff --git a/react-packager/src/DependencyResolver/Polyfill.js b/react-packager/src/DependencyResolver/Polyfill.js new file mode 100644 index 00000000..752b864b --- /dev/null +++ b/react-packager/src/DependencyResolver/Polyfill.js @@ -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; diff --git a/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js b/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js index 2ac854e4..be71b3a0 100644 --- a/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js +++ b/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js @@ -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' ;", diff --git a/react-packager/src/DependencyResolver/__tests__/Module-test.js b/react-packager/src/DependencyResolver/__tests__/Module-test.js new file mode 100644 index 00000000..640157d9 --- /dev/null +++ b/react-packager/src/DependencyResolver/__tests__/Module-test.js @@ -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']]); + }); + }); +}); diff --git a/react-packager/src/DependencyResolver/fastfs.js b/react-packager/src/DependencyResolver/fastfs.js index 88429602..a4c04232 100644 --- a/react-packager/src/DependencyResolver/fastfs.js +++ b/react-packager/src/DependencyResolver/fastfs.js @@ -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())) { diff --git a/react-packager/src/DependencyResolver/index.js b/react-packager/src/DependencyResolver/index.js index eae2e3da..cc7f9468 100644 --- a/react-packager/src/DependencyResolver/index.js +++ b/react-packager/src/DependencyResolver/index.js @@ -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(''); diff --git a/react-packager/src/DependencyResolver/polyfills/console.js b/react-packager/src/DependencyResolver/polyfills/console.js index ff2ff39f..e0459740 100644 --- a/react-packager/src/DependencyResolver/polyfills/console.js +++ b/react-packager/src/DependencyResolver/polyfills/console.js @@ -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); }; } diff --git a/react-packager/src/DependencyResolver/polyfills/require.js b/react-packager/src/DependencyResolver/polyfills/require.js index 04a0bff7..daedb4ea 100644 --- a/react-packager/src/DependencyResolver/polyfills/require.js +++ b/react-packager/src/DependencyResolver/polyfills/require.js @@ -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); }; diff --git a/react-packager/src/DependencyResolver/replacePatterns.js b/react-packager/src/DependencyResolver/replacePatterns.js index cde2d873..c27d7c77 100644 --- a/react-packager/src/DependencyResolver/replacePatterns.js +++ b/react-packager/src/DependencyResolver/replacePatterns.js @@ -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; + diff --git a/react-packager/src/JSTransformer/index.js b/react-packager/src/JSTransformer/index.js index f7884016..6906d69f 100644 --- a/react-packager/src/JSTransformer/index.js +++ b/react-packager/src/JSTransformer/index.js @@ -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); diff --git a/react-packager/src/Packager/Package.js b/react-packager/src/Packager/Package.js deleted file mode 100644 index 6b538946..00000000 --- a/react-packager/src/Packager/Package.js +++ /dev/null @@ -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 [ - '

Main Module:

' + this._mainModuleId + '
', - '', - '

Module paths and transformed code:

', - this._modules.map(function(m) { - return '

Path:

' + m.sourcePath + '

Source:

' + - '
'; - }).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 ], - }; -} diff --git a/react-packager/src/Packager/index.js b/react-packager/src/Packager/index.js deleted file mode 100644 index a718bd26..00000000 --- a/react-packager/src/Packager/index.js +++ /dev/null @@ -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; diff --git a/react-packager/src/Server/__tests__/Server-test.js b/react-packager/src/Server/__tests__/Server-test.js index f0761d31..da8e803f 100644 --- a/react-packager/src/Server/__tests__/Server-test.js +++ b/react-packager/src/Server/__tests__/Server-test.js @@ -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 ); }); }); diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index fdeffc65..1ad6d862 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -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 = ''; - var pathname = url.parse(reqUrl).pathname; - var parts = pathname.split('/').filter(Boolean); - if (parts.length === 1) { - ret += '
Cached Packages
'; - ret += '
Dependency Graph
'; - res.end(ret); - } else if (parts[1] === 'packages') { - ret += '

Cached Packages

'; - Promise.all(Object.keys(this._packages).map(function(optionsJson) { - return this._packages[optionsJson].then(function(p) { - ret += '

' + optionsJson + '

'; - 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 += '

Dependency Graph

'; - 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 = ''; + const pathname = url.parse(reqUrl).pathname; + const parts = pathname.split('/').filter(Boolean); + if (parts.length === 1) { + ret += '
Cached Bundles
'; + ret += '
Dependency Graph
'; + res.end(ret); + } else if (parts[1] === 'bundles') { + ret += '

Cached Bundles

'; + Promise.all(Object.keys(this._bundles).map(optionsJson => + this._bundles[optionsJson].then(p => { + ret += '

' + optionsJson + '

'; + 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 += '

Dependency Graph

'; + 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; diff --git a/react-packager/src/SocketInterface/SocketClient.js b/react-packager/src/SocketInterface/SocketClient.js new file mode 100644 index 00000000..474223ad --- /dev/null +++ b/react-packager/src/SocketInterface/SocketClient.js @@ -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); +} diff --git a/react-packager/src/SocketInterface/SocketServer.js b/react-packager/src/SocketInterface/SocketServer.js new file mode 100644 index 00000000..693c8e0c --- /dev/null +++ b/react-packager/src/SocketInterface/SocketServer.js @@ -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; +} diff --git a/react-packager/src/SocketInterface/__tests__/SocketClient-test.js b/react-packager/src/SocketInterface/__tests__/SocketClient-test.js new file mode 100644 index 00000000..b77d92a0 --- /dev/null +++ b/react-packager/src/SocketInterface/__tests__/SocketClient-test.js @@ -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')); + }); +}); diff --git a/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js b/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js new file mode 100644 index 00000000..f0940023 --- /dev/null +++ b/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js @@ -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'] }); + }); + }); +}); diff --git a/react-packager/src/SocketInterface/__tests__/SocketServer-test.js b/react-packager/src/SocketInterface/__tests__/SocketServer-test.js new file mode 100644 index 00000000..ae3e8e24 --- /dev/null +++ b/react-packager/src/SocketInterface/__tests__/SocketServer-test.js @@ -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' }} + ); + }); + }); + }); +}); diff --git a/react-packager/src/SocketInterface/index.js b/react-packager/src/SocketInterface/index.js new file mode 100644 index 00000000..87837b29 --- /dev/null +++ b/react-packager/src/SocketInterface/index.js @@ -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; diff --git a/react-packager/src/__mocks__/fs.js b/react-packager/src/__mocks__/fs.js index b5251a44..ced46a98 100644 --- a/react-packager/src/__mocks__/fs.js +++ b/react-packager/src/__mocks__/fs.js @@ -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); diff --git a/transformer.js b/transformer.js index a7711931..8f7a48c2 100644 --- a/transformer.js +++ b/transformer.js @@ -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 || {},