commit 382e1553af61fc4e22924b7ad188390958cc33a0 Author: Spencer Ahrens Date: Thu Feb 19 20:10:52 2015 -0800 [react-packager][streamline oss] Move open sourced JS source to react-native-github diff --git a/blacklist.js b/blacklist.js new file mode 100644 index 00000000..2b710af6 --- /dev/null +++ b/blacklist.js @@ -0,0 +1,45 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +// Don't forget to everything listed here to `testConfig.json` +// modulePathIgnorePatterns. +var sharedBlacklist = [ + 'node_modules/JSAppServer', + 'packager/react-packager', + 'node_modules/parse/node_modules/xmlhttprequest/lib/XMLHttpRequest.js', + 'node_modules/react-tools/src/utils/ImmutableObject.js', + 'node_modules/react-tools/src/core/ReactInstanceHandles.js', + 'node_modules/react-tools/src/event/EventPropagators.js', + 'node_modules/jest-cli', +]; + +var webBlacklist = [ + '.ios.js' +]; + +var iosBlacklist = [ + 'node_modules/react-tools/src/browser/ui/React.js', + 'node_modules/react-tools/src/browser/eventPlugins/ResponderEventPlugin.js', + 'node_modules/react-tools/src/browser/ReactTextComponent.js', + // 'node_modules/react-tools/src/vendor/core/ExecutionEnvironment.js', + '.web.js', + '.android.js', +]; + +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); +} + +function blacklist(isWeb) { + return new RegExp('(' + + sharedBlacklist + .concat(isWeb ? webBlacklist : iosBlacklist) + .map(escapeRegExp) + .join('|') + + ')$' + ); +} + +module.exports = blacklist; diff --git a/launchEditor.js b/launchEditor.js new file mode 100644 index 00000000..93db9bfc --- /dev/null +++ b/launchEditor.js @@ -0,0 +1,40 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var fs = require('fs'); +var spawn = require('child_process').spawn; + +var firstLaunch = true; + +function guessEditor() { + if (firstLaunch) { + console.log('When you see Red Box with stack trace, you can click any ' + + 'stack frame to jump to the source file. The packager will launch your ' + + 'editor of choice. It will first look at REACT_EDITOR environment ' + + 'variable, then at EDITOR. To set it up, you can add something like ' + + 'REACT_EDITOR=atom to your .bashrc.'); + firstLaunch = false; + } + + var editor = process.env.REACT_EDITOR || process.env.EDITOR || 'subl'; + return editor; +} + +function launchEditor(fileName, lineNumber) { + if (!fs.existsSync(fileName)) { + return; + } + + var argument = fileName; + if (lineNumber) { + argument += ':' + lineNumber; + } + + var editor = guessEditor(); + console.log('Opening ' + fileName + ' with ' + editor); + spawn(editor, [argument], { stdio: ['pipe', 'pipe', process.stderr] }); +} + +module.exports = launchEditor; diff --git a/launchPackager.command b/launchPackager.command new file mode 100755 index 00000000..dc56d7ff --- /dev/null +++ b/launchPackager.command @@ -0,0 +1,10 @@ +#!/bin/bash + +# Set terminal title +echo -en "\033]0;React Packager\a" +clear + +THIS_DIR=$(dirname "$0") +$THIS_DIR/packager.sh +echo "Process terminated. Press to close the window" +read diff --git a/packager.js b/packager.js new file mode 100644 index 00000000..6d5336ef --- /dev/null +++ b/packager.js @@ -0,0 +1,122 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { + console.log( + '\n' + + 'Could not find dependencies.\n' + + 'Ensure dependencies are installed - ' + + 'run \'npm install\' from project root.\n' + ); + process.exit(); +} + +var ReactPackager = require('./react-packager'); +var blacklist = require('./blacklist.js'); +var connect = require('connect'); +var http = require('http'); +var launchEditor = require('./launchEditor.js'); +var parseCommandLine = require('./parseCommandLine.js'); + +var options = parseCommandLine([{ + command: 'port', + default: 8081, +}, { + command: 'root', + description: 'add another root(s) to be used by the packager in this project', +}]); + +if (!options.projectRoots) { + options.projectRoots = [path.resolve(__dirname, '..')]; +} + +if (options.root) { + if (typeof options.root === 'string') { + options.projectRoots.push(path.resolve(options.root)); + } else { + options.root.forEach(function(root) { + options.projectRoots.push(path.resolve(root)); + }); + } +} + +console.log('\n' + +' ===============================================================\n' + +' | Running packager on port ' + options.port + '. \n' + +' | Keep this packager running while developing on any JS \n' + +' | projects. Feel free to close this tab and run your own \n' + +' | packager instance if you prefer. \n' + +' | \n' + +' | https://github.com/facebook/react-native \n' + +' | \n' + +' ===============================================================\n' +); + +process.on('uncaughtException', function(e) { + console.error(e); + console.error(e.stack); + console.error('\n >>> ERROR: could not create packager - please shut down ' + + 'any existing instances that are already running.\n\n'); +}); + +runServer(options, function() { + console.log('\nReact packager ready.\n'); +}); + +function loadRawBody(req, res, next) { + req.rawBody = ''; + req.setEncoding('utf8'); + + req.on('data', function(chunk) { + req.rawBody += chunk; + }); + + req.on('end', function() { + next(); + }); +} + +function openStackFrameInEditor(req, res, next) { + if (req.url === '/open-stack-frame') { + var frame = JSON.parse(req.rawBody); + launchEditor(frame.file, frame.lineNumber); + res.end('OK'); + } else { + next(); + } +} + +function getAppMiddleware(options) { + return ReactPackager.middleware({ + dev: true, + projectRoots: options.projectRoots, + blacklistRE: blacklist(false), + cacheVersion: '2', + transformModulePath: require.resolve('./transformer.js'), + }); +} + +function runServer( + options, /* {string projectRoot, bool web, bool dev} */ + readyCallback +) { + var app = connect() + .use(loadRawBody) + .use(openStackFrameInEditor) + .use(getAppMiddleware(options)); + + options.projectRoots.forEach(function(root) { + app.use(connect.static(root)); + }); + + app.use(connect.logger()) + .use(connect.compress()) + .use(connect.errorHandler()); + + return http.createServer(app).listen(options.port, readyCallback); +} diff --git a/packager.sh b/packager.sh new file mode 100755 index 00000000..98e42184 --- /dev/null +++ b/packager.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +ulimit -n 4096 + +THIS_DIR=$(dirname "$0") +node $THIS_DIR/packager.js "$@" diff --git a/parseCommandLine.js b/parseCommandLine.js new file mode 100644 index 00000000..5240d37d --- /dev/null +++ b/parseCommandLine.js @@ -0,0 +1,52 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * Wrapper on-top of `optimist` in order to properly support boolean flags + * and have a slightly less akward API. + * + * Usage example: + * var argv = parseCommandLine([{ + * command: 'web', + * description: 'Run in a web browser instead of iOS', + * default: true + * }]) + */ +'use strict'; + +var optimist = require('optimist'); + +function parseCommandLine(config) { + // optimist default API requires you to write the command name three time + // This is a small wrapper to accept an object instead + for (var i = 0; i < config.length; ++i) { + optimist + .boolean(config[i].command) + .default(config[i].command, config[i].default) + .describe(config[i].command, config[i].description); + } + var argv = optimist.argv; + + // optimist doesn't have support for --dev=false, instead it returns 'false' + for (var i = 0; i < config.length; ++i) { + var command = config[i].command; + if (argv[command] === undefined) { + argv[command] = config[i].default; + } + if (argv[command] === 'true') { + argv[command] = true; + } + if (argv[command] === 'false') { + argv[command] = false; + } + } + + // Show --help + if (argv.help || argv.h) { + optimist.showHelp(); + process.exit(); + } + + return argv; +} + +module.exports = parseCommandLine; diff --git a/react-packager/, b/react-packager/, new file mode 100644 index 00000000..e69de29b diff --git a/react-packager/.jshintrc b/react-packager/.jshintrc new file mode 100644 index 00000000..7a3f79a7 --- /dev/null +++ b/react-packager/.jshintrc @@ -0,0 +1,86 @@ +{ + "-W093": true, + "asi": false, + "bitwise": true, + "boss": false, + "browser": false, + "camelcase": true, + "couch": false, + "curly": true, + "debug": false, + "devel": true, + "dojo": false, + "eqeqeq": true, + "eqnull": true, + "esnext": true, + "evil": false, + "expr": true, + "forin": false, + "freeze": true, + "funcscope": true, + "gcl": false, + "globals": { + "Promise": true, + "React": true, + "XMLHttpRequest": true, + "document": true, + "location": true, + "window": true + }, + "globalstrict": true, + "immed": false, + "indent": 2, + "iterator": false, + "jquery": false, + "lastsemic": false, + "latedef": false, + "laxbreak": true, + "laxcomma": false, + "loopfunc": false, + "maxcomplexity": false, + "maxdepth": false, + "maxerr": 50, + "maxlen": 80, + "maxparams": false, + "maxstatements": false, + "mootools": false, + "moz": false, + "multistr": false, + "newcap": true, + "noarg": true, + "node": true, + "noempty": false, + "nonbsp": true, + "nonew": true, + "nonstandard": false, + "notypeof": false, + "noyield": false, + "phantom": false, + "plusplus": false, + "predef": [ + "afterEach", + "beforeEach", + "describe", + "expect", + "it", + "jest", + "pit" + ], + "proto": false, + "prototypejs": false, + "quotmark": true, + "rhino": false, + "scripturl": false, + "shadow": false, + "smarttabs": false, + "strict": false, + "sub": false, + "supernew": false, + "trailing": true, + "undef": true, + "unused": true, + "validthis": false, + "worker": false, + "wsh": false, + "yui": false +} diff --git a/react-packager/.npmignore b/react-packager/.npmignore new file mode 100644 index 00000000..2113f106 --- /dev/null +++ b/react-packager/.npmignore @@ -0,0 +1,8 @@ +*~ +*.swm +*.swn +*.swp +*.DS_STORE +npm-debug.log +.cache +node_modules diff --git a/react-packager/__mocks__/debug.js b/react-packager/__mocks__/debug.js new file mode 100644 index 00000000..d35fffd4 --- /dev/null +++ b/react-packager/__mocks__/debug.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function() { + return function() {}; +}; diff --git a/react-packager/__mocks__/net.js b/react-packager/__mocks__/net.js new file mode 100644 index 00000000..661fb196 --- /dev/null +++ b/react-packager/__mocks__/net.js @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..cc56ce6e --- /dev/null +++ b/react-packager/example_project/bar.js @@ -0,0 +1,5 @@ +/** + * @providesModule bar + */ + + module.exports = setInterval; \ No newline at end of file diff --git a/react-packager/example_project/config.json b/react-packager/example_project/config.json new file mode 100644 index 00000000..0acdcb51 --- /dev/null +++ b/react-packager/example_project/config.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 00000000..c45d9aba --- /dev/null +++ b/react-packager/example_project/foo/foo.js @@ -0,0 +1,23 @@ +/** + * @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 new file mode 100644 index 00000000..2943d187 --- /dev/null +++ b/react-packager/example_project/index.js @@ -0,0 +1,10 @@ +/** + * @providesModule index + * @jsx React.DOM + */ + +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 new file mode 100644 index 00000000..d3cbae1c --- /dev/null +++ b/react-packager/example_project/js/Channel.js @@ -0,0 +1,46 @@ +/** + * @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 new file mode 100644 index 00000000..d9e0563f --- /dev/null +++ b/react-packager/example_project/js/XHR.js @@ -0,0 +1,22 @@ +/** + * @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 new file mode 100644 index 00000000..70067859 --- /dev/null +++ b/react-packager/example_project/js/code.js @@ -0,0 +1,51 @@ +/** + * @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 new file mode 100644 index 00000000..58847092 --- /dev/null +++ b/react-packager/example_project/js/main.js @@ -0,0 +1,57 @@ +/** + * @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 new file mode 100644 index 00000000..7d36bf2c --- /dev/null +++ b/react-packager/example_project/public/css/index.css @@ -0,0 +1,94 @@ +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 new file mode 100644 index 00000000..b19685d5 --- /dev/null +++ b/react-packager/example_project/public/index.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/react-packager/index.js b/react-packager/index.js new file mode 100644 index 00000000..65ae88d8 --- /dev/null +++ b/react-packager/index.js @@ -0,0 +1,39 @@ +'use strict'; + +var Activity = require('./src/Activity'); +var Server = require('./src/Server'); + +exports.middleware = function(options) { + var server = new Server(options); + return server.processRequest.bind(server); +}; + +exports.buildPackageFromUrl = function(options, reqUrl) { + Activity.disable(); + // Don't start the filewatcher or the cache. + if (options.nonPersistent == null) { + options.nonPersistent = true; + } + + var server = new Server(options); + return server.buildPackageFromUrl(reqUrl) + .then(function(p) { + server.end(); + return p; + }); +}; + +exports.getDependencies = function(options, main) { + Activity.disable(); + // Don't start the filewatcher or the cache. + if (options.nonPersistent == null) { + options.nonPersistent = true; + } + + var server = new Server(options); + return server.getDependencies(main) + .then(function(r) { + server.end(); + return r.dependencies; + }); +}; diff --git a/react-packager/package.json b/react-packager/package.json new file mode 100644 index 00000000..ad7b7602 --- /dev/null +++ b/react-packager/package.json @@ -0,0 +1,10 @@ +{ + "name": "react-packager", + "version": "0.1.0", + "description": "", + "main": "index.js", + "jest": { + "unmockedModulePathPatterns": ["source-map"], + "testPathIgnorePatterns": ["JSAppServer/node_modules"] + } +} diff --git a/react-packager/src/Activity/__tests__/Activity-test.js b/react-packager/src/Activity/__tests__/Activity-test.js new file mode 100644 index 00000000..7a2bdf48 --- /dev/null +++ b/react-packager/src/Activity/__tests__/Activity-test.js @@ -0,0 +1,79 @@ +jest.autoMockOff(); + +describe('Activity', function() { + var Activity; + + var origConsoleLog = console.log; + + beforeEach(function() { + console.log = jest.genMockFn(); + Activity = require('../'); + }); + + afterEach(function() { + 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}; + + Activity.startEvent(EVENT_NAME, DATA); + jest.runOnlyPendingTimers(); + + expect(console.log.mock.calls.length).toBe(1); + var 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}; + + var 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]; + 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 expired eventId', function() { + var eid = Activity.startEvent('', ''); + Activity.endEvent(eid); + + expect(function() { + Activity.endEvent(eid); + }).toThrow('event(1) has already ended!'); + }); + }); + + describe('signal', function() { + it('writes a SIGNAL event out to the console', function() { + var EVENT_NAME = 'EVENT_NAME'; + var 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]; + 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 new file mode 100644 index 00000000..a60f87b0 --- /dev/null +++ b/react-packager/src/Activity/index.js @@ -0,0 +1,161 @@ +var COLLECTION_PERIOD = 1000; + +var _endedEvents = Object.create(null); +var _eventStarts = Object.create(null); +var _queuedActions = []; +var _scheduledCollectionTimer = null; +var _uuid = 1; +var _enabled = true; + +function endEvent(eventId) { + var eventEndTime = Date.now(); + + if (!_eventStarts[eventId]) { + _throw('event(' + eventId + ') is not a valid event id!'); + } + + if (_endedEvents[eventId]) { + _throw('event(' + eventId + ') has already ended!'); + } + + _scheduleAction({ + action: 'endEvent', + eventId: eventId, + tstamp: eventEndTime + }); + _endedEvents[eventId] = true; +} + +function signal(eventName, data) { + var signalTime = Date.now(); + + if (eventName == null) { + _throw('No event name specified'); + } + + if (data == null) { + data = null; + } + + _scheduleAction({ + action: 'signal', + data: data, + eventName: eventName, + tstamp: signalTime + }); +} + +function startEvent(eventName, data) { + var eventStartTime = Date.now(); + + if (eventName == null) { + _throw('No event name specified'); + } + + if (data == null) { + data = null; + } + + var eventId = _uuid++; + var action = { + action: 'startEvent', + data: data, + eventId: eventId, + eventName: eventName, + tstamp: eventStartTime, + }; + _scheduleAction(action); + _eventStarts[eventId] = action; + + return eventId; +} + +function disable() { + _enabled = false; +} + +function _runCollection() { + /* jshint -W084 */ + var action; + while ((action = _queuedActions.shift())) { + _writeAction(action); + } + + _scheduledCollectionTimer = null; +} + +function _scheduleAction(action) { + _queuedActions.push(action); + + if (_scheduledCollectionTimer === null) { + _scheduledCollectionTimer = setTimeout(_runCollection, COLLECTION_PERIOD); + } +} + +/** + * This a utility function that throws an error message. + * + * The only purpose of this utility is to make APIs like + * startEvent/endEvent/signal inlineable in the JIT. + * + * (V8 can't inline functions that statically contain a `throw`, and probably + * won't be adding such a non-trivial optimization anytime soon) + */ +function _throw(msg) { + var err = new Error(msg); + + // Strip off the call to _throw() + var stack = err.stack.split('\n'); + stack.splice(1, 1); + err.stack = stack.join('\n'); + + throw err; +} + +function _writeAction(action) { + if (!_enabled) { + return; + } + + var data = action.data ? ': ' + JSON.stringify(action.data) : ''; + var fmtTime = new Date(action.tstamp).toLocaleTimeString(); + + switch (action.action) { + case 'startEvent': + console.log( + '[' + fmtTime + '] ' + + ' ' + action.eventName + + data + ); + break; + + case 'endEvent': + var startAction = _eventStarts[action.eventId]; + var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : ''; + console.log( + '[' + fmtTime + '] ' + + ' ' + startAction.eventName + + '(' + (action.tstamp - startAction.tstamp) + 'ms)' + + startData + ); + delete _eventStarts[action.eventId]; + break; + + case 'signal': + console.log( + '[' + fmtTime + '] ' + + ' ' + action.eventName + '' + + data + ); + break; + + default: + _throw('Unexpected scheduled action type: ' + action.action); + } +} + + +exports.endEvent = endEvent; +exports.signal = signal; +exports.startEvent = startEvent; +exports.disable = disable; diff --git a/react-packager/src/DependencyResolver/ModuleDescriptor.js b/react-packager/src/DependencyResolver/ModuleDescriptor.js new file mode 100644 index 00000000..0898767a --- /dev/null +++ b/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -0,0 +1,34 @@ +function ModuleDescriptor(fields) { + if (!fields.id) { + throw new Error('Missing required fields id'); + } + this.id = fields.id; + + if (!fields.path) { + throw new Error('Missing required fields path'); + } + this.path = fields.path; + + if (!fields.dependencies) { + throw new Error('Missing required fields dependencies'); + } + this.dependencies = fields.dependencies; + + this.resolveDependency = fields.resolveDependency; + + this.entry = fields.entry || false; + + this.isPolyfill = fields.isPolyfill || false; + + this._fields = fields; +} + +ModuleDescriptor.prototype.toJSON = function() { + return { + id: this.id, + path: this.path, + dependencies: this.dependencies + } +}; + +module.exports = ModuleDescriptor; diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js new file mode 100644 index 00000000..de3622d9 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js @@ -0,0 +1,101 @@ +'use strict'; + +var fs = jest.genMockFromModule('fs'); + +fs.realpath.mockImpl(function(filepath, callback) { + var node; + try { + node = getToNode(filepath); + } catch (e) { + return callback(e); + } + if (node && typeof node === 'object' && node.SYMLINK != null) { + return callback(null, node.SYMLINK); + } + callback(null, filepath); +}); + +fs.readdir.mockImpl(function(filepath, callback) { + var node; + try { + node = getToNode(filepath); + if (node && typeof node === 'object' && node.SYMLINK != null) { + node = getToNode(node.SYMLINK); + } + } catch (e) { + return callback(e); + } + + if (!(node && typeof node === 'object' && node.SYMLINK == null)) { + return callback(new Error(filepath + ' is not a directory.')); + } + + callback(null, Object.keys(node)); +}); + +fs.readFile.mockImpl(function(filepath, encoding, callback) { + try { + var node = getToNode(filepath); + // dir check + if (node && typeof node === 'object' && node.SYMLINK == null) { + callback(new Error('Trying to read a dir, ESIDR, or whatever')); + } + return callback(null, node); + } catch (e) { + return callback(e); + } +}); + +fs.lstat.mockImpl(function(filepath, callback) { + var node; + try { + node = getToNode(filepath); + } catch (e) { + return callback(e); + } + + if (node && typeof node === 'object' && node.SYMLINK == null) { + callback(null, { + isDirectory: function() { + return true; + }, + isSymbolicLink: function() { + return false; + } + }); + } else { + callback(null, { + isDirectory: function() { + return false; + }, + isSymbolicLink: function() { + if (typeof node === 'object' && node.SYMLINK) { + return true; + } + return false; + } + }); + } +}); + +var filesystem; + +fs.__setMockFilesystem = function(object) { + filesystem = object; + return filesystem; +}; + +function getToNode(filepath) { + var parts = filepath.split('/'); + if (parts[0] !== '') { + throw new Error('Make sure all paths are absolute.'); + } + var node = filesystem; + parts.slice(1).forEach(function(part) { + node = node[part]; + }); + + return node; +} + +module.exports = fs; diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js new file mode 100644 index 00000000..fe8a18b6 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -0,0 +1,731 @@ +'use strict'; + +jest + .dontMock('../index') + .dontMock('q') + .dontMock('path') + .dontMock('absolute-path') + .dontMock('../../../../fb-path-utils') + .dontMock('../docblock') + .setMock('../../../ModuleDescriptor', function(data) {return data;}); + +var q = require('q'); + +describe('DependencyGraph', function() { + var DependencyGraph; + var fileWatcher; + var fs; + + beforeEach(function() { + fs = require('fs'); + DependencyGraph = require('../index'); + + fileWatcher = { + on: function() { + return this; + } + }; + }); + + describe('getOrderedDependencies', function() { + pit('should get dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")' + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', path: '/root/index.js', dependencies: ['a']}, + {id: 'a', path: '/root/a.js', dependencies: []}, + ]); + }); + }); + + pit('should get recursive dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("index")', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', path: '/root/index.js', dependencies: ['a']}, + {id: 'a', path: '/root/a.js', dependencies: ['index']}, + ]); + }); + }); + + pit('should work with packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', path: '/root/index.js', dependencies: ['aPackage']}, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should ignore malformed packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': 'lol', + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', path: '/root/index.js', dependencies: ['aPackage']}, + ]); + }); + }); + + pit('can have multiple modules with the same name', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("b")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + 'c.js': [ + '/**', + ' * @providesModule c', + ' */', + ].join('\n'), + 'somedir': { + 'somefile.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("c")', + ].join('\n') + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) + .toEqual([ + { id: 'index', + path: '/root/somedir/somefile.js', + dependencies: ['c'] + }, + { id: 'c', + path: '/root/c.js', + dependencies: [] + }, + ]); + }); + }); + + pit('providesModule wins when conflict with package', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule aPackage', + ' */', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage', + path: '/root/b.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should be forgiving with missing requires', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("lolomg")', + ].join('\n') + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['lolomg'] + } + ]); + }); + }); + + pit('should work with packages with subdirs', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot' + } + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'] + }, + { id: 'aPackage/subdir/lolynot', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should work with packages with symlinked subdirs', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'symlinkedPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot' + } + }, + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { SYMLINK: '/symlinkedPackage' }, + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'] + }, + { id: 'aPackage/subdir/lolynot', + path: '/symlinkedPackage/subdir/lolynot.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should work with relative modules in packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'require("./subdir/lolynot")', + 'subdir': { + 'lolynot.js': 'require("../other")' + }, + 'other.js': 'some code' + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: ['./subdir/lolynot'] + }, + { id: 'aPackage/subdir/lolynot', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: ['../other'] + }, + { id: 'aPackage/other', + path: '/root/aPackage/other.js', + dependencies: [] + }, + ]); + }); + }); + }); + + describe('file watch updating', function() { + var fileWatcher; + var triggerFileChange; + + beforeEach(function() { + fileWatcher = { + on: function(eventType, callback) { + if (eventType !== 'all') { + throw new Error('Can only handle "all" event in watcher.'); + } + triggerFileChange = callback; + return this; + } + }; + }); + + pit('updates module dependencies', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [] + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file change', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [] + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file delete', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + delete filesystem.root.foo; + triggerFileChange('delete', 'foo.js', root); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [] + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file add', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + filesystem.root['bar.js'] = [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")' + ].join('\n'); + triggerFileChange('add', 'bar.js', root); + + filesystem.root.aPackage['main.js'] = 'require("bar")'; + triggerFileChange('change', 'aPackage/main.js', root); + + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: ['bar'] + }, + { id: 'bar', + path: '/root/bar.js', + dependencies: ['foo'] + }, + { id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'] + }, + ]); + }); + }); + }); + + pit('runs changes through ignore filter', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + ignoreFilePath: function(filePath) { + if (filePath === '/root/bar.js') { + return true; + } + return false; + } + }); + return dgraph.load().then(function() { + filesystem.root['bar.js'] = [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")' + ].join('\n'); + triggerFileChange('add', 'bar.js', root); + + filesystem.root.aPackage['main.js'] = 'require("bar")'; + triggerFileChange('change', 'aPackage/main.js', root); + + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: ['bar'] + }, + { id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'] + }, + ]); + }); + }); + }); + + pit('should ignore directory updates', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + return dgraph.load().then(function() { + triggerFileChange('change', 'aPackage', '/root', { + isDirectory: function(){ return true; } + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'] + }, + { id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [] + }, + { id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'] + }, + ]); + }); + }); + }); + }); +}); diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js new file mode 100644 index 00000000..52cac03b --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js @@ -0,0 +1,88 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var docblockRe = /^\s*(\/\*\*(.|\r?\n)*?\*\/)/; + +var ltrimRe = /^\s*/; +/** + * @param {String} contents + * @return {String} + */ +function extract(contents) { + var match = contents.match(docblockRe); + if (match) { + return match[0].replace(ltrimRe, '') || ''; + } + return ''; +} + + +var commentStartRe = /^\/\*\*?/; +var commentEndRe = /\*\/$/; +var wsRe = /[\t ]+/g; +var stringStartRe = /(\r?\n|^) *\*/g; +var multilineRe = /(?:^|\r?\n) *(@[^\r\n]*?) *\r?\n *([^@\r\n\s][^@\r\n]+?) *\r?\n/g; +var propertyRe = /(?:^|\r?\n) *@(\S+) *([^\r\n]*)/g; + +/** + * @param {String} contents + * @return {Array} + */ +function parse(docblock) { + docblock = docblock + .replace(commentStartRe, '') + .replace(commentEndRe, '') + .replace(wsRe, ' ') + .replace(stringStartRe, '$1'); + + // Normalize multi-line directives + var prev = ''; + while (prev != docblock) { + prev = docblock; + docblock = docblock.replace(multilineRe, "\n$1 $2\n"); + } + docblock = docblock.trim(); + + var result = []; + var match; + while (match = propertyRe.exec(docblock)) { + result.push([match[1], match[2]]); + } + + return result; +} + +/** + * Same as parse but returns an object of prop: value instead of array of paris + * If a property appers more than once the last one will be returned + * + * @param {String} contents + * @return {Object} + */ +function parseAsObject(docblock) { + var pairs = parse(docblock); + var result = {}; + for (var i = 0; i < pairs.length; i++) { + result[pairs[i][0]] = pairs[i][1]; + } + return result; +} + + +exports.extract = extract; +exports.parse = parse; +exports.parseAsObject = parseAsObject; diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js new file mode 100644 index 00000000..02e6c592 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js @@ -0,0 +1,25 @@ +var path = require('path'); +var DependecyGraph = require('./'); + +var example_project = path.resolve(__dirname, '../../../../example_project'); +var watcher = new (require('../../../FileWatcher'))({projectRoot: example_project}); +var graph = new DependecyGraph({ + fileWatcher: watcher, + root: example_project +}); + +graph.load().then(function() { + var index = path.join(example_project, 'index.js'); + console.log(graph.getOrderedDependencies(index)); +}).done(); + +watcher.getWatcher().then(function(watcher) { + watcher.on('all', function() { + setImmediate(function() { + graph.load().then(function() { + var index = path.join(example_project, 'index.js'); + console.log(graph.getOrderedDependencies(index)); + }); + }) + }); +}); diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js new file mode 100644 index 00000000..9a939620 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -0,0 +1,494 @@ +'use strict'; + +var ModuleDescriptor = require('../../ModuleDescriptor'); +var q = require('q'); +var fs = require('fs'); +var docblock = require('./docblock'); +var path = require('path'); +var isAbsolutePath = require('absolute-path'); +var debug = require('debug')('DependecyGraph'); +var util = require('util'); + +var readFile = q.nfbind(fs.readFile); +var readDir = q.nfbind(fs.readdir); +var lstat = q.nfbind(fs.lstat); +var realpath = q.nfbind(fs.realpath); + +function DependecyGraph(options) { + this._roots = options.roots; + this._ignoreFilePath = options.ignoreFilePath || function(){}; + this._loaded = false; + this._queue = this._roots.slice(); + this._graph = Object.create(null); + this._packageByRoot = Object.create(null); + this._packagesById = Object.create(null); + this._moduleById = Object.create(null); + this._debugUpdateEvents = []; + this._fileWatcher = options.fileWatcher; + + // Kick off the search process to precompute the dependency graph. + this._init(); +} + +DependecyGraph.prototype.load = function() { + return this._loading || (this._loading = this._search()); +}; + +/** + * Given an entry file return an array of all the dependent module descriptors. + */ +DependecyGraph.prototype.getOrderedDependencies = function(entryPath) { + var absolutePath = this._getAbsolutePath(entryPath); + if (absolutePath == null) { + throw new Error('Cannot find entry file in any of the roots: ' + entryPath); + } + + var module = this._graph[absolutePath]; + if (module == null) { + throw new Error('Module with path "' + entryPath + '" is not in graph'); + } + + var self = this; + var deps = []; + var visited = Object.create(null); + + // Node haste sucks. Id's aren't unique. So to make sure our entry point + // is the thing that ends up in our dependency list. + var graphMap = Object.create(this._moduleById); + graphMap[module.id] = module; + + // Recursively collect the dependency list. + function collect(module) { + deps.push(module); + + module.dependencies.forEach(function(name) { + var id = sansExtJs(name); + var dep = self.resolveDependency(module, id); + + if (dep == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`.', + name, + module.id + ); + return; + } + + if (!visited[dep.id]) { + visited[dep.id] = true; + collect(dep); + } + }); + } + + visited[module.id] = true; + collect(module); + + return deps; +}; + +/** + * Given a module descriptor `fromModule` return the module descriptor for + * the required module `depModuleId`. It could be top-level or relative, + * or both. + */ +DependecyGraph.prototype.resolveDependency = function( + fromModule, + depModuleId +) { + var packageJson, modulePath, dep; + + // Package relative modules starts with '.' or '..'. + if (depModuleId[0] !== '.') { + + // 1. `depModuleId` is simply a top-level `providesModule`. + // 2. `depModuleId` is a package module but given the full path from the + // package, i.e. package_name/module_name + if (this._moduleById[sansExtJs(depModuleId)]) { + return this._moduleById[sansExtJs(depModuleId)]; + } + + // 3. `depModuleId` is a package and it's depending on the "main" + // resolution. + packageJson = this._packagesById[depModuleId]; + + // We are being forgiving here and raising an error because we could be + // processing a file that uses it's own require system. + if (packageJson == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`.', + depModuleId, + fromModule.id + ); + return; + } + + var main = packageJson.main || 'index'; + modulePath = withExtJs(path.join(packageJson._root, main)); + dep = this._graph[modulePath]; + if (dep == null) { + throw new Error( + 'Cannot find package main file for pacakge: ' + packageJson._root + ); + } + return dep; + } else { + + // 4. `depModuleId` is a module defined in a package relative to + // `fromModule`. + packageJson = this._lookupPackage(fromModule.path); + + if (packageJson == null) { + throw new Error( + 'Expected relative module lookup from ' + fromModule.id + ' to ' + + depModuleId + ' to be within a package but no package.json found.' + ); + } + + // Example: depModuleId: ../a/b + // fromModule.path: /x/y/z + // modulePath: /x/y/a/b + var dir = path.dirname(fromModule.path); + modulePath = withExtJs(path.join(dir, depModuleId)); + + dep = this._graph[modulePath]; + if (dep == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`.' + + ' Inferred required module path is %s', + depModuleId, + fromModule.id, + modulePath + ); + return null; + } + + return dep; + } +}; + +/** + * Intiates the filewatcher and kicks off the search process. + */ +DependecyGraph.prototype._init = function() { + var processChange = this._processFileChange.bind(this); + var watcher = this._fileWatcher; + + this._loading = this.load().then(function() { + watcher.on('all', processChange); + }); +}; + +/** + * Implements a DFS over the file system looking for modules and packages. + */ +DependecyGraph.prototype._search = function() { + var self = this; + var dir = this._queue.shift(); + + if (dir == null) { + return q.Promise.resolve(this._graph); + } + + // Steps: + // 1. Read a dir and stat all the entries. + // 2. Filter the files and queue up the directories. + // 3. Process any package.json in the files + // 4. recur. + return readDir(dir) + .then(function(files){ + return q.all(files.map(function(filePath) { + return realpath(path.join(dir, filePath)).catch(handleBrokenLink); + })); + }) + .then(function(filePaths) { + filePaths = filePaths.filter(function(filePath) { + if (filePath == null) { + return false + } + + return !self._ignoreFilePath(filePath); + }); + + var statsP = filePaths.map(function(filePath) { + return lstat(filePath).catch(handleBrokenLink); + }); + + return [ + filePaths, + q.all(statsP) + ]; + }) + .spread(function(files, stats) { + var modulePaths = files.filter(function(filePath, i) { + if (stats[i].isDirectory()) { + self._queue.push(filePath); + return false; + } + + if (stats[i].isSymbolicLink()) { + return false; + } + + return filePath.match(/\.js$/); + }); + + var processing = self._findAndProcessPackage(files, dir) + .then(function() { + return q.all(modulePaths.map(self._processModule.bind(self))); + }); + + return q.all([ + processing, + self._search() + ]); + }) + .then(function() { + return self; + }); +}; + +/** + * Given a list of files find a `package.json` file, and if found parse it + * and update indices. + */ +DependecyGraph.prototype._findAndProcessPackage = function(files, root) { + var self = this; + + var packagePath; + for (var i = 0; i < files.length ; i++) { + var file = files[i]; + if (path.basename(file) === 'package.json') { + packagePath = file; + break; + } + } + + if (packagePath != null) { + return readFile(packagePath, 'utf8') + .then(function(content) { + var packageJson; + try { + packageJson = JSON.parse(content); + } catch (e) { + debug('WARNING: malformed package.json: ', packagePath); + return q(); + } + + if (packageJson.name == null) { + debug( + 'WARNING: package.json `%s` is missing a name field', + packagePath + ); + return q(); + } + + packageJson._root = root; + self._packageByRoot[root] = packageJson; + self._packagesById[packageJson.name] = packageJson; + + return packageJson; + }); + } else { + return q(); + } +}; + +/** + * Parse a module and update indices. + */ +DependecyGraph.prototype._processModule = function(modulePath) { + var self = this; + return readFile(modulePath, 'utf8') + .then(function(content) { + var moduleDocBlock = docblock.parseAsObject(content); + var moduleData = { path: path.resolve(modulePath) }; + if (moduleDocBlock.providesModule || moduleDocBlock.provides) { + moduleData.id = + moduleDocBlock.providesModule || moduleDocBlock.provides; + } else { + moduleData.id = self._lookupName(modulePath); + } + moduleData.dependencies = extractRequires(content); + + var module = new ModuleDescriptor(moduleData); + self._updateGraphWithModule(module); + return module; + }); +}; + +/** + * Compute the name of module relative to a package it may belong to. + */ +DependecyGraph.prototype._lookupName = function(modulePath) { + var packageJson = this._lookupPackage(modulePath); + if (packageJson == null) { + return path.resolve(modulePath); + } else { + var relativePath = + sansExtJs(path.relative(packageJson._root, modulePath)); + return path.join(packageJson.name, relativePath); + } +}; + +DependecyGraph.prototype._deleteModule = function(module) { + delete this._graph[module.path]; + + // Others may keep a reference so we mark it as deleted. + module.deleted = true; + + // Haste allows different module to have the same id. + if (this._moduleById[module.id] === module) { + delete this._moduleById[module.id]; + } +}; + +/** + * Update the graph and indices with the module. + */ +DependecyGraph.prototype._updateGraphWithModule = function(module) { + if (this._graph[module.path]) { + this._deleteModule(this._graph[module.path]); + } + + this._graph[module.path] = module; + + if (this._moduleById[module.id]) { + debug( + 'WARNING: Top-level module name conflict `%s`.\n' + + 'module with path `%s` will replace `%s`', + module.id, + module.path, + this._moduleById[module.id].path + ); + } + + this._moduleById[module.id] = module; +}; + +/** + * Find the nearest package to a module. + */ +DependecyGraph.prototype._lookupPackage = function(modulePath) { + var packageByRoot = this._packageByRoot; + + /** + * Auxiliary function to recursively lookup a package. + */ + function lookupPackage(currDir) { + // ideally we stop once we're outside root and this can be a simple child + // dir check. However, we have to support modules that was symlinked inside + // our project root. + if (currDir === '/') { + return null; + } else { + var packageJson = packageByRoot[currDir]; + if (packageJson) { + return packageJson; + } else { + return lookupPackage(path.dirname(currDir)); + } + } + } + + return lookupPackage(path.dirname(modulePath)); +}; + +/** + * Process a filewatcher change event. + */ +DependecyGraph.prototype._processFileChange = function(eventType, filePath, root, stat) { + var absPath = path.join(root, filePath); + if (this._ignoreFilePath(absPath)) { + return; + } + + this._debugUpdateEvents.push({event: eventType, path: filePath}); + + if (eventType === 'delete') { + var module = this._graph[absPath]; + if (module == null) { + return; + } + + this._deleteModule(module); + } else if (!(stat && stat.isDirectory())) { + var self = this; + this._loading = this._loading.then(function() { + return self._processModule(absPath); + }); + } +}; + +DependecyGraph.prototype.getDebugInfo = function() { + return '

FileWatcher Update Events

' + + '
' + util.inspect(this._debugUpdateEvents) + '
' + + '

Graph dump

' + + '
' + util.inspect(this._graph) + '
'; +}; + +/** + * Searches all roots for the file and returns the first one that has file of the same path. + */ +DependecyGraph.prototype._getAbsolutePath = function(filePath) { + if (isAbsolutePath(filePath)) { + return filePath; + } + + for (var i = 0, root; root = this._roots[i]; i++) { + var absPath = path.join(root, filePath); + if (this._graph[absPath]) { + return absPath; + } + } + + return null; +}; + +/** + * Extract all required modules from a `code` string. + */ +var requireRe = /\brequire\s*\(\s*[\'"]([^"\']+)["\']\s*\)/g; +var blockCommentRe = /\/\*(.|\n)*?\*\//g; +var lineCommentRe = /\/\/.+(\n|$)/g; +function extractRequires(code) { + var deps = []; + + code + .replace(blockCommentRe, '') + .replace(lineCommentRe, '') + .replace(requireRe, function(match, dep) { + deps.push(dep); + }); + + return deps; +} + +/** + * `file` without the .js extension. + */ +function sansExtJs(file) { + if (file.match(/\.js$/)) { + return file.slice(0, -3); + } else { + return file; + } +} + +/** + * `file` with the .js extension. + */ +function withExtJs(file) { + if (file.match(/\.js$/)) { + return file; + } else { + return file + '.js'; + } +} + +function handleBrokenLink(e) { + debug('WARNING: error stating, possibly broken symlink', e.message); + return q(); +} + +module.exports = DependecyGraph; diff --git a/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js b/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js new file mode 100644 index 00000000..9b43f97e --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js @@ -0,0 +1,195 @@ + +jest.dontMock('../') + .dontMock('q') + .setMock('../../ModuleDescriptor', function(data) {return data;}); + +var q = require('q'); + +describe('HasteDependencyResolver', function() { + var HasteDependencyResolver; + var DependencyGraph; + + beforeEach(function() { + // For the polyfillDeps + require('path').join.mockImpl(function(a, b) { + return b; + }); + HasteDependencyResolver = require('../'); + DependencyGraph = require('../DependencyGraph'); + }); + + describe('getDependencies', function() { + pit('should get dependencies with polyfills', function() { + var module = {id: 'index', path: '/root/index.js', dependencies: ['a']}; + var deps = [module]; + + var depResolver = new HasteDependencyResolver({ + projectRoot: '/root' + }); + + // Is there a better way? How can I mock the prototype instead? + var depGraph = depResolver._depGraph; + depGraph.getOrderedDependencies.mockImpl(function() { + return deps; + }); + depGraph.load.mockImpl(function() { + return q(); + }); + + return depResolver.getDependencies('/root/index.js') + .then(function(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' + ], + }, + module + ]); + }); + }); + + pit('should pass in more polyfills', function() { + var module = {id: 'index', path: '/root/index.js', dependencies: ['a']}; + var deps = [module]; + + var depResolver = new HasteDependencyResolver({ + projectRoot: '/root', + polyfillModuleNames: ['some module'] + }); + + // Is there a better way? How can I mock the prototype instead? + var depGraph = depResolver._depGraph; + depGraph.getOrderedDependencies.mockImpl(function() { + return deps; + }); + depGraph.load.mockImpl(function() { + return q(); + }); + + return depResolver.getDependencies('/root/index.js') + .then(function(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' + ], + }, + { path: 'some module', + id: 'some module', + isPolyfill: true, + dependencies: [ + 'polyfills/prelude.js', + 'polyfills/require.js', + 'polyfills/polyfills.js', + 'polyfills/console.js', + 'polyfills/error-guard.js', + ] + }, + module + ]); + }); + }); + }); + + describe('wrapModule', function() { + it('should ', function() { + var depResolver = new HasteDependencyResolver({ + projectRoot: '/root' + }); + + var depGraph = depResolver._depGraph; + var dependencies = ['x', 'y', 'z'] + var code = [ + 'require("x")', + 'require("y")', + 'require("z")', + ].join('\n'); + + depGraph.resolveDependency.mockImpl(function(fromModule, toModuleName) { + if (toModuleName === 'x') { + return { + id: 'changed' + }; + } else if (toModuleName === 'y') { + return { id: 'y' }; + } + return null; + }); + + var processedCode = depResolver.wrapModule({ + id: 'test module', + path: '/root/test.js', + dependencies: dependencies + }, code); + + expect(processedCode).toEqual([ + "__d('test module',[\"changed\",\"y\"],function(global," + + " require, requireDynamic, requireLazy, module, exports) {" + + " require('changed')", + "require('y')", + 'require("z")});', + ].join('\n')); + }); + }); +}); diff --git a/react-packager/src/DependencyResolver/haste/index.js b/react-packager/src/DependencyResolver/haste/index.js new file mode 100644 index 00000000..6e2cd6fc --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/index.js @@ -0,0 +1,130 @@ +'use strict'; + +var path = require('path'); +var FileWatcher = require('../../FileWatcher'); +var DependencyGraph = require('./DependencyGraph'); +var ModuleDescriptor = require('../ModuleDescriptor'); + +var DEFINE_MODULE_CODE = + '__d(' + + '\'_moduleName_\',' + + '_deps_,' + + 'function(global, require, requireDynamic, requireLazy, module, exports) {'+ + ' _code_' + + '}' + + ');'; + +var DEFINE_MODULE_REPLACE_RE = /_moduleName_|_code_|_deps_/g; + +var REL_REQUIRE_STMT = /require\(['"]([\.\/0-9A-Z_$\-]*)['"]\)/gi; + +function HasteDependencyResolver(config) { + this._fileWatcher = config.nonPersistent + ? FileWatcher.createDummyWatcher() + : new FileWatcher(config.projectRoots); + + this._depGraph = new DependencyGraph({ + roots: config.projectRoots, + ignoreFilePath: function(filepath) { + return filepath.indexOf('__tests__') !== -1 || + (config.blacklistRE && config.blacklistRE.test(filepath)); + }, + fileWatcher: this._fileWatcher + }); + + this._polyfillModuleNames = [ + config.dev + ? path.join(__dirname, 'polyfills/prelude_dev.js') + : path.join(__dirname, 'polyfills/prelude.js'), + path.join(__dirname, 'polyfills/require.js'), + path.join(__dirname, 'polyfills/polyfills.js'), + path.join(__dirname, 'polyfills/console.js'), + path.join(__dirname, 'polyfills/error-guard.js'), + ].concat( + config.polyfillModuleNames || [] + ); +} + +HasteDependencyResolver.prototype.getDependencies = function(main) { + var depGraph = this._depGraph; + var self = this; + + return depGraph.load() + .then(function() { + var dependencies = depGraph.getOrderedDependencies(main); + var mainModuleId = dependencies[0].id; + + self._prependPolyfillDependencies(dependencies); + + return { + mainModuleId: mainModuleId, + dependencies: dependencies + }; + }); +}; + +HasteDependencyResolver.prototype._prependPolyfillDependencies = function( + dependencies +) { + var polyfillModuleNames = this._polyfillModuleNames; + if (polyfillModuleNames.length > 0) { + var polyfillModules = polyfillModuleNames.map( + function(polyfillModuleName, idx) { + return new ModuleDescriptor({ + path: polyfillModuleName, + id: polyfillModuleName, + dependencies: polyfillModuleNames.slice(0, idx), + isPolyfill: true + }); + } + ); + dependencies.unshift.apply(dependencies, polyfillModules); + } +}; + +HasteDependencyResolver.prototype.wrapModule = function(module, code) { + if (module.isPolyfill) { + return code; + } + + var depGraph = this._depGraph; + var resolvedDeps = Object.create(null); + var resolvedDepsArr = []; + + for (var i = 0; i < module.dependencies.length; i++) { + var depName = module.dependencies[i]; + var dep = this._depGraph.resolveDependency(module, depName); + if (dep) { + resolvedDeps[depName] = dep.id; + resolvedDepsArr.push(dep.id); + } + } + + var relativizedCode = + code.replace(REL_REQUIRE_STMT, function(codeMatch, depName) { + var dep = resolvedDeps[depName]; + if (dep != null) { + return 'require(\'' + dep + '\')'; + } else { + return codeMatch; + } + }); + + return DEFINE_MODULE_CODE.replace(DEFINE_MODULE_REPLACE_RE, function(key) { + return { + '_moduleName_': module.id, + '_code_': relativizedCode, + '_deps_': JSON.stringify(resolvedDepsArr), + }[key]; + }); +}; + +HasteDependencyResolver.prototype.end = function() { + return this._fileWatcher.end(); +}; + +HasteDependencyResolver.prototype.getDebugInfo = function() { + return this._depGraph.getDebugInfo(); +}; + +module.exports = HasteDependencyResolver; diff --git a/react-packager/src/DependencyResolver/haste/polyfills/console.js b/react-packager/src/DependencyResolver/haste/polyfills/console.js new file mode 100644 index 00000000..4c9ddce1 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/console.js @@ -0,0 +1,141 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This pipes all of our console logging functions to native logging so that + * JavaScript errors in required modules show up in Xcode via NSLog. + * + * @provides console + * @polyfill + */ + +(function(global) { + + var OBJECT_COLUMN_NAME = '(index)'; + + function setupConsole(global) { + + if (!global.nativeLoggingHook) { + return; + } + + function doNativeLog() { + var str = Array.prototype.map.call(arguments, function(arg) { + if (arg == null) { + return arg === null ? 'null' : 'undefined'; + } else if (typeof arg === 'string') { + return '"' + arg + '"'; + } else { + // Perform a try catch, just in case the object has a circular + // reference or stringify throws for some other reason. + try { + return JSON.stringify(arg); + } catch (e) { + if (typeof arg.toString === 'function') { + try { + return arg.toString(); + } catch (e) { + return 'unknown'; + } + } + } + } + }).join(', '); + global.nativeLoggingHook(str); + }; + + var repeat = function(element, n) { + return Array.apply(null, Array(n)).map(function() { return element; }); + }; + + function consoleTablePolyfill(rows) { + // convert object -> array + if (!Array.isArray(rows)) { + var data = rows; + rows = []; + for (var key in data) { + if (data.hasOwnProperty(key)) { + var row = data[key]; + row[OBJECT_COLUMN_NAME] = key; + rows.push(row); + } + } + } + if (rows.length === 0) { + global.nativeLoggingHook(''); + return; + } + + var columns = Object.keys(rows[0]).sort(); + var stringRows = []; + var columnWidths = []; + + // Convert each cell to a string. Also + // figure out max cell width for each column + columns.forEach(function(k, i) { + columnWidths[i] = k.length; + for (var j = 0; j < rows.length; j++) { + var cellStr = rows[j][k].toString(); + stringRows[j] = stringRows[j] || []; + stringRows[j][i] = cellStr; + columnWidths[i] = Math.max(columnWidths[i], cellStr.length); + } + }); + + // Join all elements in the row into a single string with | separators + // (appends extra spaces to each cell to make separators | alligned) + var joinRow = function(row, space) { + var cells = row.map(function(cell, i) { + var extraSpaces = repeat(' ', columnWidths[i] - cell.length).join(''); + return cell + extraSpaces; + }); + space = space || ' '; + return cells.join(space + '|' + space); + }; + + var separators = columnWidths.map(function(columnWidth) { + return repeat('-', columnWidth).join(''); + }); + var separatorRow = joinRow(separators, '-'); + var header = joinRow(columns); + var table = [header, separatorRow]; + + for (var i = 0; i < rows.length; i++) { + table.push(joinRow(stringRows[i])); + } + + // Notice extra empty line at the beginning. + // Native logging hook adds "RCTLog >" at the front of every + // logged string, which would shift the header and screw up + // the table + global.nativeLoggingHook('\n' + table.join('\n')); + }; + + global.console = { + error: doNativeLog, + info: doNativeLog, + log: doNativeLog, + warn: doNativeLog, + table: consoleTablePolyfill + }; + + }; + + if (typeof module !== 'undefined') { + module.exports = setupConsole; + } else { + setupConsole(global); + } + +})(this); diff --git a/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js b/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js new file mode 100644 index 00000000..687a4a19 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js @@ -0,0 +1,82 @@ + +/** + * The particular require runtime that we are using looks for a global + * `ErrorUtils` object and if it exists, then it requires modules with the + * error handler specified via ErrorUtils.setGlobalHandler by calling the + * require function with applyWithGuard. Since the require module is loaded + * before any of the modules, this ErrorUtils must be defined (and the handler + * set) globally before requiring anything. + */ +/* eslint global-strict:0 */ +(function(global) { + var ErrorUtils = { + _inGuard: 0, + _globalHandler: null, + setGlobalHandler: function(fun) { + ErrorUtils._globalHandler = fun; + }, + reportError: function(error) { + Error._globalHandler && ErrorUtils._globalHandler(error); + }, + applyWithGuard: function(fun, context, args) { + try { + ErrorUtils._inGuard++; + return fun.apply(context, args); + } catch (e) { + ErrorUtils._globalHandler && ErrorUtils._globalHandler(e); + } finally { + ErrorUtils._inGuard--; + } + }, + applyWithGuardIfNeeded: function(fun, context, args) { + if (ErrorUtils.inGuard()) { + return fun.apply(context, args); + } else { + ErrorUtils.applyWithGuard(fun, context, args); + } + }, + inGuard: function() { + return ErrorUtils._inGuard; + }, + guard: function(fun, name, context) { + if (typeof fun !== "function") { + console.warn('A function must be passed to ErrorUtils.guard, got ', fun); + return null; + } + name = name || fun.name || ''; + function guarded() { + return ( + ErrorUtils.applyWithGuard( + fun, + context || this, + arguments, + null, + name + ) + ); + } + + return guarded; + } + }; + global.ErrorUtils = ErrorUtils; + + /** + * This is the error handler that is called when we encounter an exception + * when loading a module. + */ + function setupErrorGuard() { + var onError = function(e) { + global.console.error( + 'Error: ' + + '\n stack: ' + e.stack + + '\n line: ' + e.line + + '\n message: ' + e.message, + e + ); + }; + global.ErrorUtils.setGlobalHandler(onError); + } + + setupErrorGuard(); +})(this); diff --git a/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js b/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js new file mode 100644 index 00000000..2fd32246 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js @@ -0,0 +1,75 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This pipes all of our console logging functions to native logging so that + * JavaScript errors in required modules show up in Xcode via NSLog. + * + * @provides Object.es6 + * @polyfill + */ + +// WARNING: This is an optimized version that fails on hasOwnProperty checks +// and non objects. It's not spec-compliant. It's a perf optimization. + +Object.assign = function(target, sources) { + if (__DEV__) { + if (target == null) { + throw new TypeError('Object.assign target cannot be null or undefined'); + } + if (typeof target !== 'object' && typeof target !== 'function') { + throw new TypeError( + 'In this environment the target of assign MUST be an object.' + + 'This error is a performance optimization and not spec compliant.' + ); + } + } + + for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) { + var nextSource = arguments[nextIndex]; + if (nextSource == null) { + continue; + } + + if (__DEV__) { + if (typeof nextSource !== 'object' && + typeof nextSource !== 'function') { + throw new TypeError( + 'In this environment the target of assign MUST be an object.' + + 'This error is a performance optimization and not spec compliant.' + ); + } + } + + // We don't currently support accessors nor proxies. Therefore this + // copy cannot throw. If we ever supported this then we must handle + // exceptions and side-effects. + + for (var key in nextSource) { + if (__DEV__) { + var hasOwnProperty = Object.prototype.hasOwnProperty; + if (!hasOwnProperty.call(nextSource, key)) { + throw new TypeError( + 'One of the sources to assign has an enumerable key on the ' + + 'prototype chain. This is an edge case that we do not support. ' + + 'This error is a performance optimization and not spec compliant.' + ); + } + } + target[key] = nextSource[key]; + } + } + + return target; +}; diff --git a/react-packager/src/DependencyResolver/haste/polyfills/prelude.js b/react-packager/src/DependencyResolver/haste/polyfills/prelude.js new file mode 100644 index 00000000..95c66983 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/prelude.js @@ -0,0 +1 @@ +__DEV__ = false; diff --git a/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js b/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js new file mode 100644 index 00000000..a5ca53b7 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js @@ -0,0 +1 @@ +__DEV__ = true; diff --git a/react-packager/src/DependencyResolver/haste/polyfills/require.js b/react-packager/src/DependencyResolver/haste/polyfills/require.js new file mode 100644 index 00000000..3b5d6d87 --- /dev/null +++ b/react-packager/src/DependencyResolver/haste/polyfills/require.js @@ -0,0 +1,626 @@ +(function(global) { + + // avoid redefining require() + if (global.require) { + return; + } + + var __DEV__ = global.__DEV__; + + var toString = Object.prototype.toString; + + /** + * module index: { + * mod1: { + * exports: { ... }, + * id: 'mod1', + * dependencies: ['mod1', 'mod2'], + * factory: function() { ... }, + * waitingMap: { mod1: 1, mod3: 1, mod4: 1 }, + * waiting: 2 + * } + * } + */ + var modulesMap = {}, + /** + * inverse index: { + * mod1: [modules, waiting for mod1], + * mod2: [modules, waiting for mod2] + * } + */ + dependencyMap = {}, + /** + * modules whose reference counts are set out of order + */ + predefinedRefCounts = {}, + + _counter = 0, + + REQUIRE_WHEN_READY = 1, + USED_AS_TRANSPORT = 2, + + hop = Object.prototype.hasOwnProperty; + + function _debugUnresolvedDependencies(names) { + var unresolved = Array.prototype.slice.call(names); + var visited = {}; + var ii, name, module, dependency; + + while (unresolved.length) { + name = unresolved.shift(); + if (visited[name]) { + continue; + } + visited[name] = true; + + module = modulesMap[name]; + if (!module || !module.waiting) { + continue; + } + + for (ii = 0; ii < module.dependencies.length; ii++) { + dependency = module.dependencies[ii]; + if (!modulesMap[dependency] || modulesMap[dependency].waiting) { + unresolved.push(dependency); + } + } + } + + for (name in visited) if (hop.call(visited, name)) { + unresolved.push(name); + } + + var messages = []; + for (ii = 0; ii < unresolved.length; ii++) { + name = unresolved[ii]; + var message = name; + module = modulesMap[name]; + if (!module) { + message += ' is not defined'; + } else if (!module.waiting) { + message += ' is ready'; + } else { + var unresolvedDependencies = []; + for (var jj = 0; jj < module.dependencies.length; jj++) { + dependency = module.dependencies[jj]; + if (!modulesMap[dependency] || modulesMap[dependency].waiting) { + unresolvedDependencies.push(dependency); + } + } + message += ' is waiting for ' + unresolvedDependencies.join(', '); + } + messages.push(message); + } + return messages.join('\n'); + } + + /** + * This is mainly for logging in ModuleErrorLogger. + */ + function ModuleError(msg) { + this.name = 'ModuleError'; + this.message = msg; + this.stack = Error(msg).stack; + this.framesToPop = 2; + } + ModuleError.prototype = Object.create(Error.prototype); + ModuleError.prototype.constructor = ModuleError; + + var _performance = + global.performance || + global.msPerformance || + global.webkitPerformance || {}; + + if (!_performance.now) { + _performance = global.Date; + } + + var _now = _performance ? + _performance.now.bind(_performance) : function(){return 0;}; + + var _factoryStackCount = 0; + var _factoryTime = 0; + var _totalFactories = 0; + + /** + * The require function conforming to CommonJS spec: + * http://wiki.commonjs.org/wiki/Modules/1.1.1 + * + * To define a CommonJS-compliant module add the providesModule + * Haste header to your file instead of @provides. Your file is going + * to be executed in a separate context. Every variable/function you + * define will be local (private) to that module. To export local members + * use "exports" variable or return the exported value at the end of your + * file. Your code will have access to the "module" object. + * The "module" object will have an "id" property that is the id of your + * current module. "module" object will also have "exports" property that + * is the same as "exports" variable passed into your module context. + * You can require other modules using their ids. + * + * Haste will automatically pick dependencies from require() calls. So + * you don't have to manually specify @requires in your header. + * + * You cannot require() modules from non-CommonJS files. Write a legacy stub + * (@providesLegacy) and use @requires instead. + * + * @example + * + * / ** + * * @providesModule math + * * / + * exports.add = function() { + * var sum = 0, i = 0, args = arguments, l = args.length; + * while (i < l) { + * sum += args[i++]; + * } + * return sum; + * }; + * + * / ** + * * @providesModule increment + * * / + * var add = require('math').add; + * return function(val) { + * return add(val, 1); + * }; + * + * / ** + * * @providesModule program + * * / + * var inc = require('increment'); + * var a = 1; + * inc(a); // 2 + * + * module.id == "program"; + * + * + * @param {String} id + * @throws when module is not loaded or not ready to be required + */ + function require(id) { + var module = modulesMap[id], dep, i, msg; + if (module && module.exports) { + // If ref count is 1, this was the last call, so undefine the module. + // The ref count can be null or undefined, but those are never === 1. + if (module.refcount-- === 1) { + delete modulesMap[id]; + } + return module.exports; + } + + if (global.ErrorUtils && !global.ErrorUtils.inGuard()) { + return ErrorUtils.applyWithGuard(require, this, arguments); + } + + if (!module) { + msg = 'Requiring unknown module "' + id + '"'; + if (__DEV__) { + msg += '. It may not be loaded yet. Did you forget to run arc build?'; + } + throw new ModuleError(msg); + } + + if (module.hasError) { + throw new ModuleError( + 'Requiring module "' + id + '" which threw an exception' + ); + } + + if (module.waiting) { + throw new ModuleError( + 'Requiring module "' + id + '" with unresolved dependencies: ' + + _debugUnresolvedDependencies([id]) + ); + } + + var exports = module.exports = {}; + var factory = module.factory; + if (toString.call(factory) === '[object Function]') { + var args = [], + dependencies = module.dependencies, + length = dependencies.length, + ret; + if (module.special & USED_AS_TRANSPORT) { + length = Math.min(length, factory.length); + } + try { + for (i = 0; args.length < length; i++) { + dep = dependencies[i]; + if (!module.inlineRequires[dep]) { + args.push(dep === 'module' ? module : + (dep === 'exports' ? exports : + require.call(null, dep))); + } + } + + ++_totalFactories; + if (_factoryStackCount++ === 0) { + _factoryTime -= _now(); + } + try { + ret = factory.apply(module.context || global, args); + } catch (e) { + if (modulesMap.ex && modulesMap.erx) { + // when ErrorUtils is ready, ex and erx are ready. otherwise, we + // don't append module id to the error message but still throw it + var ex = require.call(null, 'ex'); + var erx = require.call(null, 'erx'); + var messageWithParams = erx(e.message); + if (messageWithParams[0].indexOf(' from module "%s"') < 0) { + messageWithParams[0] += ' from module "%s"'; + messageWithParams[messageWithParams.length] = id; + } + e.message = ex.apply(null, messageWithParams); + } + throw e; + } finally { + if (--_factoryStackCount === 0) { + _factoryTime += _now(); + } + } + } catch (e) { + module.hasError = true; + module.exports = null; + throw e; + } + if (ret) { + if (__DEV__) { + if (typeof ret != 'object' && typeof ret != 'function') { + throw new ModuleError( + 'Factory for module "' + id + '" returned ' + + 'an invalid value "' + ret + '". ' + + 'Returned value should be either a function or an object.' + ); + } + } + module.exports = ret; + } + } else { + module.exports = factory; + } + + // If ref count is 1, this was the last call, so undefine the module. + // The ref count can be null or undefined, but those are never === 1. + if (module.refcount-- === 1) { + delete modulesMap[id]; + } + return module.exports; + } + + require.__getFactoryTime = function() { + return (_factoryStackCount ? _now() : 0) + _factoryTime; + }; + + require.__getTotalFactories = function() { + return _totalFactories; + }; + + /** + * The define function conforming to CommonJS proposal: + * http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition + * + * define() allows you to explicitly state dependencies of your module + * in javascript. It's most useful in non-CommonJS files. + * + * define() is used internally by haste as a transport for CommonJS + * modules. So there's no need to use define() if you use providesModule + * + * @example + * / ** + * * @provides alpha + * * / + * + * // Sets up the module with ID of "alpha", that uses require, + * // exports and the module with ID of "beta": + * define("alpha", ["require", "exports", "beta"], + * function (require, exports, beta) { + * exports.verb = function() { + * return beta.verb(); + * //Or: + * return require("beta").verb(); + * } + * }); + * + * / ** + * * @provides alpha + * * / + * // An anonymous module could be defined (module id derived from filename) + * // that returns an object literal: + * + * define(["alpha"], function (alpha) { + * return { + * verb: function(){ + * return alpha.verb() + 2; + * } + * }; + * }); + * + * / ** + * * @provides alpha + * * / + * // A dependency-free module can define a direct object literal: + * + * define({ + * add: function(x, y){ + * return x + y; + * } + * }); + * + * @param {String} id optional + * @param {Array} dependencies optional + * @param {Object|Function} factory + */ + function define(id, dependencies, factory, + _special, _context, _refCount, _inlineRequires) { + if (dependencies === undefined) { + dependencies = []; + factory = id; + id = _uid(); + } else if (factory === undefined) { + factory = dependencies; + if (toString.call(id) === '[object Array]') { + dependencies = id; + id = _uid(); + } else { + dependencies = []; + } + } + + // Non-standard: we allow modules to be undefined. This is designed for + // temporary modules. + var canceler = { cancel: _undefine.bind(this, id) }; + + var record = modulesMap[id]; + + // Nonstandard hack: we call define with null deps and factory, but a + // non-null reference count (e.g. define('name', null, null, 0, null, 4)) + // when this module is defined elsewhere and we just need to update the + // reference count. We use this hack to avoid having to expose another + // global function to increment ref counts. + if (record) { + if (_refCount) { + record.refcount += _refCount; + } + // Calling define() on a pre-existing module does not redefine it + return canceler; + } else if (!dependencies && !factory && _refCount) { + // If this module hasn't been defined yet, store the ref count. We'll use + // it when the module is defined later. + predefinedRefCounts[id] = (predefinedRefCounts[id] || 0) + _refCount; + return canceler; + } else { + // Defining a new module + record = { id: id }; + record.refcount = (predefinedRefCounts[id] || 0) + (_refCount || 0); + delete predefinedRefCounts[id]; + } + + if (__DEV__) { + if ( + !factory || + (typeof factory != 'object' && typeof factory != 'function' && + typeof factory != 'string')) { + throw new ModuleError( + 'Invalid factory "' + factory + '" for module "' + id + '". ' + + 'Factory should be either a function or an object.' + ); + } + + if (toString.call(dependencies) !== '[object Array]') { + throw new ModuleError( + 'Invalid dependencies for module "' + id + '". ' + + 'Dependencies must be passed as an array.' + ); + } + } + + record.factory = factory; + record.dependencies = dependencies; + record.context = _context; + record.special = _special; + record.inlineRequires = _inlineRequires || {}; + record.waitingMap = {}; + record.waiting = 0; + record.hasError = false; + modulesMap[id] = record; + _initDependencies(id); + + return canceler; + } + + function _undefine(id) { + if (!modulesMap[id]) { + return; + } + + var module = modulesMap[id]; + delete modulesMap[id]; + + for (var dep in module.waitingMap) { + if (module.waitingMap[dep]) { + delete dependencyMap[dep][id]; + } + } + + for (var ii = 0; ii < module.dependencies.length; ii++) { + dep = module.dependencies[ii]; + if (modulesMap[dep]) { + if (modulesMap[dep].refcount-- === 1) { + _undefine(dep); + } + } else if (predefinedRefCounts[dep]) { + predefinedRefCounts[dep]--; + } + // Subtle: we won't account for this one fewer reference if we don't have + // the dependency's definition or reference count yet. + } + } + + /** + * 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++; + } + + function _addDependency(module, dep) { + // do not add duplicate dependencies and circ deps + if (!module.waitingMap[dep] && module.id !== dep) { + module.waiting++; + module.waitingMap[dep] = 1; + dependencyMap[dep] || (dependencyMap[dep] = {}); + dependencyMap[dep][module.id] = 1; + } + } + + function _initDependencies(id) { + var modulesToRequire = []; + var module = modulesMap[id]; + var dep, i, subdep; + + // initialize id's waitingMap + for (i = 0; i < module.dependencies.length; i++) { + dep = module.dependencies[i]; + if (!modulesMap[dep]) { + _addDependency(module, dep); + } else if (modulesMap[dep].waiting) { + for (subdep in modulesMap[dep].waitingMap) { + if (modulesMap[dep].waitingMap[subdep]) { + _addDependency(module, subdep); + } + } + } + } + if (module.waiting === 0 && module.special & REQUIRE_WHEN_READY) { + modulesToRequire.push(id); + } + + // update modules depending on id + if (dependencyMap[id]) { + var deps = dependencyMap[id]; + var submodule; + dependencyMap[id] = undefined; + for (dep in deps) { + submodule = modulesMap[dep]; + + // add all deps of id + for (subdep in module.waitingMap) { + if (module.waitingMap[subdep]) { + _addDependency(submodule, subdep); + } + } + // remove id itself + if (submodule.waitingMap[id]) { + submodule.waitingMap[id] = undefined; + submodule.waiting--; + } + if (submodule.waiting === 0 && + submodule.special & REQUIRE_WHEN_READY) { + modulesToRequire.push(dep); + } + } + } + + // run everything that's ready + for (i = 0; i < modulesToRequire.length; i++) { + require.call(null, modulesToRequire[i]); + } + } + + function _register(id, exports) { + var module = modulesMap[id] = { id: id }; + module.exports = exports; + module.refcount = 0; + } + + // pseudo name used in common-require + // see require() function for more info + _register('module', 0); + _register('exports', 0); + + _register('define', define); + _register('global', global); + _register('require', require); + _register('requireDynamic', require); + _register('requireLazy', requireLazy); + + define.amd = {}; + + global.define = define; + global.require = require; + global.requireDynamic = require; + global.requireLazy = requireLazy; + + require.__debug = { + modules: modulesMap, + deps: dependencyMap, + printDependencyInfo: function() { + if (!global.console) { + return; + } + var names = Object.keys(require.__debug.deps); + global.console.log(_debugUnresolvedDependencies(names)); + } + }; + + /** + * All @providesModule files are wrapped by this function by makehaste. It + * is a convenience function around define() that prepends a bunch of required + * modules (global, require, module, etc) so that we don't have to spit that + * 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']; + define(id, defaultDeps.concat(deps), factory, _special || USED_AS_TRANSPORT, + null, null, _inlineRequires); + }; + +})(this); diff --git a/react-packager/src/DependencyResolver/index.js b/react-packager/src/DependencyResolver/index.js new file mode 100644 index 00000000..79eb48c1 --- /dev/null +++ b/react-packager/src/DependencyResolver/index.js @@ -0,0 +1,12 @@ +var HasteDependencyResolver = require('./haste'); +var NodeDependencyResolver = require('./node'); + +module.exports = function createDependencyResolver(options) { + if (options.moduleFormat === 'haste') { + return new HasteDependencyResolver(options); + } else if (options.moduleFormat === 'node') { + return new NodeDependencyResolver(options); + } else { + throw new Error('unsupported'); + } +}; diff --git a/react-packager/src/DependencyResolver/node/index.js b/react-packager/src/DependencyResolver/node/index.js new file mode 100644 index 00000000..0d3b807e --- /dev/null +++ b/react-packager/src/DependencyResolver/node/index.js @@ -0,0 +1,48 @@ +var Promise = require('q').Promise; +var ModuleDescriptor = require('../ModuleDescriptor'); + +var mdeps = require('module-deps'); +var path = require('path'); +var fs = require('fs'); + +// var REQUIRE_RUNTIME = fs.readFileSync( +// path.join(__dirname, 'require.js') +// ).toString(); + +exports.getRuntimeCode = function() { + return REQUIRE_RUNTIME; +}; + +exports.wrapModule = function(id, source) { + return Promise.resolve( + 'define(' + JSON.stringify(id) + ',' + ' function(exports, module) {\n' + + source + '\n});' + ); +}; + +exports.getDependencies = function(root, fileEntryPath) { + return new Promise(function(resolve, reject) { + fileEntryPath = path.join(process.cwd(), root, fileEntryPath); + + var md = mdeps(); + + md.end({file: fileEntryPath}); + + var deps = []; + + md.on('data', function(data) { + deps.push( + new ModuleDescriptor({ + id: data.id, + deps: data.deps, + path: data.file, + entry: data.entry + }) + ); + }); + + md.on('end', function() { + resolve(deps); + }); + }); +}; diff --git a/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js b/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js new file mode 100644 index 00000000..8baae9e1 --- /dev/null +++ b/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js @@ -0,0 +1,39 @@ +'use strict'; + +jest.dontMock('../') + .dontMock('q') + .setMock('child_process', { exec: function(cmd, cb) { cb(null, '/usr/bin/watchman') } }); + +describe('FileWatcher', function() { + var FileWatcher; + var Watcher; + + beforeEach(function() { + FileWatcher = require('../'); + Watcher = require('sane').WatchmanWatcher; + Watcher.prototype.once.mockImplementation(function(type, callback) { + callback(); + }); + }); + + it('it should get the watcher instance when ready', function() { + var fileWatcher = new FileWatcher(['rootDir']); + return fileWatcher._loading.then(function(watchers) { + watchers.forEach(function(watcher) { + expect(watcher instanceof Watcher).toBe(true); + }); + }); + }); + + pit('it should end the watcher', function() { + var fileWatcher = new FileWatcher(['rootDir']); + Watcher.prototype.close.mockImplementation(function(callback) { + callback(); + }); + + return fileWatcher.end().then(function() { + expect(Watcher.prototype.close).toBeCalled(); + }); + }); + +}); diff --git a/react-packager/src/FileWatcher/index.js b/react-packager/src/FileWatcher/index.js new file mode 100644 index 00000000..f2721d8c --- /dev/null +++ b/react-packager/src/FileWatcher/index.js @@ -0,0 +1,86 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var sane = require('sane'); +var q = require('q'); +var util = require('util'); +var exec = require('child_process').exec; + +var Promise = q.Promise; + +var detectingWatcherClass = new Promise(function(resolve, reject) { + exec('which watchman', function(err, out) { + if (err || out.length === 0) { + resolve(sane.NodeWatcher); + } else { + resolve(sane.WatchmanWatcher); + } + }); +}); + +module.exports = FileWatcher; + +var MAX_WAIT_TIME = 3000; + +function FileWatcher(projectRoots) { + var self = this; + this._loading = q.all( + projectRoots.map(createWatcher) + ).then(function(watchers) { + watchers.forEach(function(watcher) { + watcher.on('all', function(type, filepath, root) { + self.emit('all', type, filepath, root); + }); + }); + return watchers; + }); + this._loading.done(); +} + +util.inherits(FileWatcher, EventEmitter); + +FileWatcher.prototype.end = function() { + return this._loading.then(function(watchers) { + watchers.forEach(function(watcher) { + delete watchersByRoot[watcher._root]; + return q.ninvoke(watcher, 'close'); + }); + }); +}; + +var watchersByRoot = Object.create(null); + +function createWatcher(root) { + if (watchersByRoot[root] != null) { + return Promise.resolve(watchersByRoot[root]); + } + + return detectingWatcherClass.then(function(Watcher) { + var watcher = new Watcher(root, {glob: '**/*.js'}); + + return new Promise(function(resolve, reject) { + var rejectTimeout = setTimeout(function() { + reject(new Error([ + 'Watcher took too long to load', + 'Try running `watchman` from your terminal', + 'https://facebook.github.io/watchman/docs/troubleshooting.html', + ].join('\n'))); + }, MAX_WAIT_TIME); + + watcher.once('ready', function() { + clearTimeout(rejectTimeout); + watchersByRoot[root] = watcher; + watcher._root = root; + resolve(watcher); + }); + }); + }); +} + +FileWatcher.createDummyWatcher = function() { + var ev = new EventEmitter(); + ev.end = function() { + return q(); + }; + return ev; +}; diff --git a/react-packager/src/JSTransformer/Cache.js b/react-packager/src/JSTransformer/Cache.js new file mode 100644 index 00000000..577af696 --- /dev/null +++ b/react-packager/src/JSTransformer/Cache.js @@ -0,0 +1,129 @@ +'use strict'; + +var path = require('path'); +var version = require('../../package.json').version; +var tmpdir = require('os').tmpDir(); +var pathUtils = require('../fb-path-utils'); +var fs = require('fs'); +var _ = require('underscore'); +var q = require('q'); + +var Promise = q.Promise; + +module.exports = Cache; + +function Cache(projectConfig) { + this._cacheFilePath = cacheFilePath(projectConfig); + + var data; + if (!projectConfig.resetCache) { + data = loadCacheSync(this._cacheFilePath); + } else { + data = Object.create(null); + } + this._data = data; + + this._has = Object.prototype.hasOwnProperty.bind(data); + this._persistEventually = _.debounce( + this._persistCache.bind(this), + 2000 + ); +} + +Cache.prototype.get = function(filepath, loaderCb) { + if (!pathUtils.isAbsolutePath(filepath)) { + throw new Error('Use absolute paths'); + } + + var recordP = this._has(filepath) + ? this._data[filepath] + : this._set(filepath, loaderCb(filepath)); + + return recordP.then(function(record) { + return record.data; + }); +}; + +Cache.prototype._set = function(filepath, loaderPromise) { + return this._data[filepath] = loaderPromise.then(function(data) { + return [ + data, + q.nfbind(fs.stat)(filepath) + ]; + }).spread(function(data, stat) { + this._persistEventually(); + return { + data: data, + mtime: stat.mtime.getTime(), + }; + }.bind(this)); +}; + +Cache.prototype.invalidate = function(filepath){ + if(this._has(filepath)) { + delete this._data[filepath]; + } +} + +Cache.prototype.end = function() { + return this._persistCache(); +}; + +Cache.prototype._persistCache = function() { + if (this._persisting != null) { + return this._persisting; + } + + var data = this._data; + var cacheFilepath = this._cacheFilePath; + + return this._persisting = q.all(_.values(data)) + .then(function(values) { + var json = Object.create(null); + Object.keys(data).forEach(function(key, i) { + json[key] = values[i]; + }); + return q.nfbind(fs.writeFile)(cacheFilepath, JSON.stringify(json)); + }) + .then(function() { + this._persisting = null; + return true; + }.bind(this)); +}; + +function loadCacheSync(cacheFilepath) { + var ret = Object.create(null); + if (!fs.existsSync(cacheFilepath)) { + return ret; + } + + var cacheOnDisk = JSON.parse(fs.readFileSync(cacheFilepath)); + + // Filter outdated cache and convert to promises. + Object.keys(cacheOnDisk).forEach(function(key) { + if (!fs.existsSync(key)) { + return; + } + var value = cacheOnDisk[key]; + var stat = fs.statSync(key); + if (stat.mtime.getTime() === value.mtime) { + ret[key] = Promise.resolve(value); + } + }); + + return ret; +} + +function cacheFilePath(projectConfig) { + var roots = projectConfig.projectRoots.join(',').split(path.sep).join('-'); + var cacheVersion = projectConfig.cacheVersion || '0'; + return path.join( + tmpdir, + [ + 'react-packager-cache', + version, + cacheVersion, + roots, + ].join('-') + ); +} diff --git a/react-packager/src/JSTransformer/README.md b/react-packager/src/JSTransformer/README.md new file mode 100644 index 00000000..e69de29b diff --git a/react-packager/src/JSTransformer/__mocks__/worker.js b/react-packager/src/JSTransformer/__mocks__/worker.js new file mode 100644 index 00000000..04a24e8d --- /dev/null +++ b/react-packager/src/JSTransformer/__mocks__/worker.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function (data, callback) { + callback(null, {}); +}; diff --git a/react-packager/src/JSTransformer/__tests__/Cache-test.js b/react-packager/src/JSTransformer/__tests__/Cache-test.js new file mode 100644 index 00000000..c77c6384 --- /dev/null +++ b/react-packager/src/JSTransformer/__tests__/Cache-test.js @@ -0,0 +1,202 @@ +'use strict'; + +jest + .dontMock('underscore') + .dontMock('path') + .dontMock('q') + .dontMock('absolute-path') + .dontMock('../../fb-path-utils') + .dontMock('../Cache'); + +var q = require('q'); + +describe('JSTransformer Cache', function() { + var Cache; + + beforeEach(function() { + require('os').tmpDir.mockImpl(function() { + return 'tmpDir'; + }); + + Cache = require('../Cache'); + }); + + describe('getting/settig', function() { + it('calls loader callback for uncached file', function() { + var cache = new Cache({projectRoots: ['/rootDir']}); + var loaderCb = jest.genMockFn().mockImpl(function() { + return q(); + }); + cache.get('/rootDir/someFile', loaderCb); + expect(loaderCb).toBeCalledWith('/rootDir/someFile'); + }); + + pit('gets the value from the loader callback', function() { + require('fs').stat.mockImpl(function(file, callback) { + callback(null, { + mtime: { + getTime: function() {} + } + }); + }); + var cache = new Cache({projectRoots: ['/rootDir']}); + var loaderCb = jest.genMockFn().mockImpl(function() { + return q('lol'); + }); + return cache.get('/rootDir/someFile', loaderCb).then(function(value) { + expect(value).toBe('lol'); + }); + }); + + pit('caches the value after the first call', function() { + require('fs').stat.mockImpl(function(file, callback) { + callback(null, { + mtime: { + getTime: function() {} + } + }); + }); + var cache = new Cache({projectRoots: ['/rootDir']}); + var loaderCb = jest.genMockFn().mockImpl(function() { + return q('lol'); + }); + return cache.get('/rootDir/someFile', loaderCb).then(function() { + var shouldNotBeCalled = jest.genMockFn(); + return cache.get('/rootDir/someFile', shouldNotBeCalled) + .then(function(value) { + expect(shouldNotBeCalled).not.toBeCalled(); + expect(value).toBe('lol'); + }); + }); + }); + }); + + describe('loading cache from disk', function() { + var fileStats; + + beforeEach(function() { + fileStats = { + '/rootDir/someFile': { + mtime: { + getTime: function() { + return 22; + } + } + }, + '/rootDir/foo': { + mtime: { + getTime: function() { + return 11; + } + } + } + }; + + var fs = require('fs'); + + fs.existsSync.mockImpl(function() { + return true; + }); + + fs.statSync.mockImpl(function(filePath) { + return fileStats[filePath]; + }); + + fs.readFileSync.mockImpl(function() { + return JSON.stringify({ + '/rootDir/someFile': { + mtime: 22, + data: 'oh hai' + }, + '/rootDir/foo': { + mtime: 11, + data: 'lol wat' + } + }); + }); + }); + + pit('should load cache from disk', function() { + var cache = new Cache({projectRoots: ['/rootDir']}); + var loaderCb = jest.genMockFn(); + return cache.get('/rootDir/someFile', loaderCb).then(function(value) { + expect(loaderCb).not.toBeCalled(); + expect(value).toBe('oh hai'); + + return cache.get('/rootDir/foo', loaderCb).then(function(value) { + expect(loaderCb).not.toBeCalled(); + expect(value).toBe('lol wat'); + }); + }); + }); + + pit('should not load outdated cache', function() { + require('fs').stat.mockImpl(function(file, callback) { + callback(null, { + mtime: { + getTime: function() {} + } + }); + }); + + fileStats['/rootDir/foo'].mtime.getTime = function() { + return 123; + }; + + var cache = new Cache({projectRoots: ['/rootDir']}); + var loaderCb = jest.genMockFn().mockImpl(function() { + return q('new value'); + }); + + return cache.get('/rootDir/someFile', loaderCb).then(function(value) { + expect(loaderCb).not.toBeCalled(); + expect(value).toBe('oh hai'); + + return cache.get('/rootDir/foo', loaderCb).then(function(value) { + expect(loaderCb).toBeCalled(); + expect(value).toBe('new value'); + }); + }); + }); + }); + + describe('writing cache to disk', function() { + it('should write cache to disk', function() { + var index = 0; + var mtimes = [10, 20, 30]; + var debounceIndex = 0; + require('underscore').debounce = function(callback) { + return function () { + if (++debounceIndex === 3) { + callback(); + } + }; + }; + + var fs = require('fs'); + fs.stat.mockImpl(function(file, callback) { + callback(null, { + mtime: { + getTime: function() { + return mtimes[index++]; + } + } + }); + }); + + var cache = new Cache({projectRoots: ['/rootDir']}); + cache.get('/rootDir/bar', function() { + return q('bar value'); + }); + cache.get('/rootDir/foo', function() { + return q('foo value'); + }); + cache.get('/rootDir/baz', function() { + return q('baz value'); + }); + + jest.runAllTimers(); + expect(fs.writeFile).toBeCalled(); + }); + }); +}); diff --git a/react-packager/src/JSTransformer/__tests__/Transformer-test.js b/react-packager/src/JSTransformer/__tests__/Transformer-test.js new file mode 100644 index 00000000..6c9c6644 --- /dev/null +++ b/react-packager/src/JSTransformer/__tests__/Transformer-test.js @@ -0,0 +1,71 @@ +'use strict'; + +jest + .dontMock('worker-farm') + .dontMock('q') + .dontMock('os') + .dontMock('../index'); + +var OPTIONS = { + transformModulePath: '/foo/bar' +}; + +describe('Transformer', function() { + var Transformer; + var workers; + + beforeEach(function() { + workers = jest.genMockFn(); + jest.setMock('worker-farm', jest.genMockFn().mockImpl(function() { + return workers; + })); + require('../Cache').prototype.get.mockImpl(function(filePath, callback) { + return callback(); + }); + require('fs').readFile.mockImpl(function(file, callback) { + callback(null, 'content'); + }); + Transformer = require('../'); + }); + + pit('should loadFileAndTransform', function() { + workers.mockImpl(function(data, callback) { + callback(null, { code: 'transformed' }); + }); + require('fs').readFile.mockImpl(function(file, callback) { + callback(null, 'content'); + }); + + return new Transformer(OPTIONS).loadFileAndTransform([], 'file', {}) + .then(function(data) { + expect(data).toEqual({ + code: 'transformed', + sourcePath: 'file', + sourceCode: 'content' + }); + }); + }); + + pit('should add file info to parse errors', function() { + require('fs').readFile.mockImpl(function(file, callback) { + callback(null, 'var x;\nvar answer = 1 = x;'); + }); + + workers.mockImpl(function(data, callback) { + var esprimaError = new Error('Error: Line 2: Invalid left-hand side in assignment'); + esprimaError.description = 'Invalid left-hand side in assignment'; + esprimaError.lineNumber = 2; + esprimaError.column = 15; + callback(null, {error: esprimaError}); + }); + + return new Transformer(OPTIONS).loadFileAndTransform([], 'foo-file.js', {}) + .catch(function(error) { + expect(error.type).toEqual('TransformError'); + expect(error.snippet).toEqual([ + 'var answer = 1 = x;', + ' ^', + ].join('\n')); + }); + }); +}); diff --git a/react-packager/src/JSTransformer/index.js b/react-packager/src/JSTransformer/index.js new file mode 100644 index 00000000..7b01d961 --- /dev/null +++ b/react-packager/src/JSTransformer/index.js @@ -0,0 +1,112 @@ + +'use strict'; + +var os = require('os'); +var fs = require('fs'); +var q = require('q'); +var Cache = require('./Cache'); +var _ = require('underscore'); +var workerFarm = require('worker-farm'); + +var readFile = q.nfbind(fs.readFile); + +module.exports = Transformer; +Transformer.TransformError = TransformError; + +function Transformer(projectConfig) { + this._cache = projectConfig.nonPersistent + ? new DummyCache() : new Cache(projectConfig); + + if (projectConfig.transformModulePath == null) { + this._failedToStart = q.Promise.reject(new Error('No transfrom module')); + } else { + this._workers = workerFarm( + {autoStart: true}, + projectConfig.transformModulePath + ); + } +} + +Transformer.prototype.kill = function() { + this._workers && workerFarm.end(this._workers); + return this._cache.end(); +}; + +Transformer.prototype.invalidateFile = function(filePath) { + this._cache.invalidate(filePath); + //TODO: We can read the file and put it into the cache right here + // This would simplify some caching logic as we can be sure that the cache is up to date +} + +Transformer.prototype.loadFileAndTransform = function( + transformSets, + filePath, + options +) { + if (this._failedToStart) { + return this._failedToStart; + } + + var workers = this._workers; + return this._cache.get(filePath, function() { + return readFile(filePath) + .then(function(buffer) { + var sourceCode = buffer.toString(); + var opts = _.extend({}, options, {filename: filePath}); + return q.nfbind(workers)({ + transformSets: transformSets, + sourceCode: sourceCode, + options: opts, + }).then( + function(res) { + if (res.error) { + throw formatEsprimaError(res.error, filePath, sourceCode); + } + + return { + code: res.code, + sourcePath: filePath, + sourceCode: sourceCode + }; + } + ); + }); + }); +}; + +function TransformError() {} +TransformError.__proto__ = SyntaxError.prototype; + +function formatEsprimaError(err, filename, source) { + if (!(err.lineNumber && err.column)) { + return err; + } + + var stack = err.stack.split('\n'); + stack.shift(); + + var msg = 'TransformError: ' + err.description + ' ' + filename + ':' + + err.lineNumber + ':' + err.column; + var sourceLine = source.split('\n')[err.lineNumber - 1]; + var snippet = sourceLine + '\n' + new Array(err.column - 1).join(' ') + '^'; + + stack.unshift(msg); + + var error = new TransformError(); + error.message = msg; + error.type = 'TransformError'; + error.stack = stack.join('\n'); + error.snippet = snippet; + error.filename = filename; + error.lineNumber = err.lineNumber; + error.column = err.column; + error.description = err.description; + return error; +} + +function DummyCache() {} +DummyCache.prototype.get = function(filePath, loaderCb) { + return loaderCb(); +}; +DummyCache.prototype.end = +DummyCache.prototype.invalidate = function(){}; diff --git a/react-packager/src/JSTransformer/worker.js b/react-packager/src/JSTransformer/worker.js new file mode 100644 index 00000000..26f789e4 --- /dev/null +++ b/react-packager/src/JSTransformer/worker.js @@ -0,0 +1,26 @@ +'use strict'; + +var transformer = require('./transformer'); + +module.exports = function (data, callback) { + var result; + try { + result = transformer.transform( + data.transformSets, + data.sourceCode, + data.options + ); + } catch (e) { + return callback(null, { + error: { + lineNumber: e.lineNumber, + column: e.column, + message: e.message, + stack: e.stack, + description: e.description + } + }); + } + + callback(null, result); +}; diff --git a/react-packager/src/Packager/Package.js b/react-packager/src/Packager/Package.js new file mode 100644 index 00000000..787684bc --- /dev/null +++ b/react-packager/src/Packager/Package.js @@ -0,0 +1,132 @@ +'use strict'; + +var _ = require('underscore'); +var SourceMapGenerator = require('source-map').SourceMapGenerator; +var base64VLQ = require('./base64-vlq'); + +module.exports = Package; + +function Package(sourceMapUrl) { + this._modules = []; + this._sourceMapUrl = sourceMapUrl; +} + +Package.prototype.setMainModuleId = function(moduleId) { + this._mainModuleId = moduleId; +}; + +Package.prototype.addModule = function( + transformedCode, + sourceCode, + sourcePath +) { + this._modules.push({ + transformedCode: transformedCode, + sourceCode: sourceCode, + sourcePath: sourcePath + }); +}; + +Package.prototype.finalize = function(options) { + if (options.runMainModule) { + var runCode = ';require("' + this._mainModuleId + '");'; + this.addModule( + runCode, + runCode, + 'RunMainModule.js' + ); + } + + Object.freeze(this._modules); + Object.seal(this._modules); +}; + +Package.prototype.getSource = function() { + return this._source || ( + this._source = _.pluck(this._modules, 'transformedCode').join('\n') + '\n' + + 'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) + + ';\n' + '\/\/@ sourceMappingURL=' + this._sourceMapUrl + ); +}; + +Package.prototype.getSourceMap = function(options) { + options = 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._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 mappings = ''; + for (var i = 0; i < modules.length; i++) { + var module = modules[i]; + var transformedCode = module.transformedCode; + var lastCharNewLine = false; + module.lines = 0; + for (var t = 0; t < transformedCode.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 - modules[i - 1].lines); + mappings += 'A'; + } else if (lastCharNewLine) { + module.lines++; + mappings += line; + } + lastCharNewLine = transformedCode[t] === '\n'; + if (lastCharNewLine) { + mappings += ';'; + } + } + if (i != modules.length - 1) { + mappings += ';'; + } + } + return mappings; +}; + +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'); +}; diff --git a/react-packager/src/Packager/__mocks__/source-map.js b/react-packager/src/Packager/__mocks__/source-map.js new file mode 100644 index 00000000..08c127f6 --- /dev/null +++ b/react-packager/src/Packager/__mocks__/source-map.js @@ -0,0 +1,5 @@ +var SourceMapGenerator = jest.genMockFn(); +SourceMapGenerator.prototype.addMapping = jest.genMockFn(); +SourceMapGenerator.prototype.setSourceContent = jest.genMockFn(); +SourceMapGenerator.prototype.toJSON = jest.genMockFn(); +exports.SourceMapGenerator = SourceMapGenerator; diff --git a/react-packager/src/Packager/__tests__/Package-test.js b/react-packager/src/Packager/__tests__/Package-test.js new file mode 100644 index 00000000..d18bb4d6 --- /dev/null +++ b/react-packager/src/Packager/__tests__/Package-test.js @@ -0,0 +1,95 @@ +'use strict'; + +jest + .dontMock('underscore') + .dontMock('../base64-vlq') + .dontMock('source-map') + .dontMock('../Package'); + +var SourceMapGenerator = require('source-map').SourceMapGenerator; + +describe('Package', function() { + var Package; + var ppackage; + + beforeEach(function() { + Package = require('../Package'); + ppackage = new Package('test_url'); + ppackage.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('transformed foo;', 'source foo', 'foo path'); + ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.finalize({}); + expect(ppackage.getSource()).toBe([ + 'transformed foo;', + 'transformed bar;', + 'RAW_SOURCE_MAP = "test-source-map";', + '\/\/@ sourceMappingURL=test_url', + ].join('\n')); + }); + + it('should create a package and add run module code', function() { + ppackage.addModule('transformed foo;', 'source foo', 'foo path'); + ppackage.addModule('transformed bar;', 'source bar', 'bar path'); + ppackage.setMainModuleId('foo'); + ppackage.finalize({runMainModule: true}); + expect(ppackage.getSource()).toBe([ + 'transformed foo;', + 'transformed bar;', + ';require("foo");', + 'RAW_SOURCE_MAP = "test-source-map";', + '\/\/@ sourceMappingURL=test_url', + ].join('\n')); + }); + }); + + describe('sourcemap package', function() { + it('should create sourcemap', function() { + var ppackage = new Package('test_url'); + ppackage.addModule('transformed foo;\n', 'source foo', 'foo path'); + ppackage.addModule('transformed bar;\n', 'source bar', 'bar path'); + ppackage.setMainModuleId('foo'); + ppackage.finalize({runMainModule: true}); + var s = ppackage.getSourceMap(); + expect(s).toEqual(genSourceMap(ppackage._modules)); + }); + }); +}); + + function genSourceMap(modules) { + var sourceMapGen = new SourceMapGenerator({file: 'bundle.js', version: 3}); + var packageLineNo = 0; + for (var i = 0; i < modules.length; i++) { + var module = modules[i]; + var transformedCode = module.transformedCode; + var sourcePath = module.sourcePath; + var sourceCode = module.sourceCode; + var transformedLineCount = 0; + var lastCharNewLine = false; + for (var t = 0; t < transformedCode.length; t++) { + if (t === 0 || lastCharNewLine) { + sourceMapGen.addMapping({ + generated: {line: packageLineNo + 1, column: 0}, + original: {line: transformedLineCount + 1, column: 0}, + source: sourcePath + }); + } + lastCharNewLine = transformedCode[t] === '\n'; + if (lastCharNewLine) { + transformedLineCount++; + packageLineNo++; + } + } + packageLineNo++; + sourceMapGen.setSourceContent( + sourcePath, + sourceCode + ); + } + return sourceMapGen.toJSON(); +}; diff --git a/react-packager/src/Packager/__tests__/Packager-test.js b/react-packager/src/Packager/__tests__/Packager-test.js new file mode 100644 index 00000000..21af12ca --- /dev/null +++ b/react-packager/src/Packager/__tests__/Packager-test.js @@ -0,0 +1,83 @@ +'use strict'; + +jest + .setMock('worker-farm', function() { return function() {};}) + .dontMock('path') + .dontMock('q') + .dontMock('os') + .dontMock('underscore') + .dontMock('../'); + +var q = require('q'); + +describe('Packager', function() { + var getDependencies; + var wrapModule; + var Packager; + + beforeEach(function() { + getDependencies = jest.genMockFn(); + wrapModule = jest.genMockFn(); + require('../../DependencyResolver').mockImpl(function() { + return { + getDependencies: getDependencies, + wrapModule: wrapModule, + }; + }); + + Packager = require('../'); + }); + + pit('create a package', function() { + require('fs').statSync.mockImpl(function() { + return { + isDirectory: function() {return true;} + }; + }); + + var packager = new Packager({projectRoots: []}); + var modules = [ + {id: 'foo', path: '/root/foo.js', dependencies: []}, + {id: 'bar', path: '/root/bar.js', dependencies: []}, + ]; + + getDependencies.mockImpl(function() { + return q({ + mainModuleId: 'foo', + dependencies: modules + }); + }); + + require('../../JSTransformer').prototype.loadFileAndTransform + .mockImpl(function(tsets, path) { + return q({ + code: 'transformed ' + path, + sourceCode: 'source ' + path, + sourcePath: path + }); + }); + + wrapModule.mockImpl(function(module, code) { + return 'lol ' + code + ' lol'; + }); + + return packager.package('/root/foo.js', true, 'source_map_url') + .then(function(p) { + expect(p.addModule.mock.calls[0]).toEqual([ + 'lol transformed /root/foo.js lol', + 'source /root/foo.js', + '/root/foo.js' + ]); + expect(p.addModule.mock.calls[1]).toEqual([ + 'lol transformed /root/bar.js lol', + 'source /root/bar.js', + '/root/bar.js' + ]); + + expect(p.finalize.mock.calls[0]).toEqual([ + {runMainModule: true} + ]); + }); + }); + +}); diff --git a/react-packager/src/Packager/base64-vlq.js b/react-packager/src/Packager/base64-vlq.js new file mode 100644 index 00000000..91d490b7 --- /dev/null +++ b/react-packager/src/Packager/base64-vlq.js @@ -0,0 +1,168 @@ +/* -*- Mode: js; js-indent-level: 2; -*- */ +/* + * Copyright 2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + * + * Based on the Base 64 VLQ implementation in Closure Compiler: + * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java + * + * Copyright 2011 The Closure Compiler Authors. All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var charToIntMap = {}; +var intToCharMap = {}; + +'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + .split('') + .forEach(function (ch, index) { + charToIntMap[ch] = index; + intToCharMap[index] = ch; + }); + +var base64 = {}; +/** + * Encode an integer in the range of 0 to 63 to a single base 64 digit. + */ +base64.encode = function base64_encode(aNumber) { + if (aNumber in intToCharMap) { + return intToCharMap[aNumber]; + } + throw new TypeError("Must be between 0 and 63: " + aNumber); +}; + +/** + * Decode a single base 64 digit to an integer. + */ +base64.decode = function base64_decode(aChar) { + if (aChar in charToIntMap) { + return charToIntMap[aChar]; + } + throw new TypeError("Not a valid base 64 digit: " + aChar); +}; + + + +// A single base 64 digit can contain 6 bits of data. For the base 64 variable +// length quantities we use in the source map spec, the first bit is the sign, +// the next four bits are the actual value, and the 6th bit is the +// continuation bit. The continuation bit tells us whether there are more +// digits in this value following this digit. +// +// Continuation +// | Sign +// | | +// V V +// 101011 + +var VLQ_BASE_SHIFT = 5; + +// binary: 100000 +var VLQ_BASE = 1 << VLQ_BASE_SHIFT; + +// binary: 011111 +var VLQ_BASE_MASK = VLQ_BASE - 1; + +// binary: 100000 +var VLQ_CONTINUATION_BIT = VLQ_BASE; + +/** + * Converts from a two-complement value to a value where the sign bit is + * placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + */ +function toVLQSigned(aValue) { + return aValue < 0 + ? ((-aValue) << 1) + 1 + : (aValue << 1) + 0; +} + +/** + * Converts to a two-complement value from a value where the sign bit is + * placed in the least significant bit. For example, as decimals: + * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 + * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 + */ +function fromVLQSigned(aValue) { + var isNegative = (aValue & 1) === 1; + var shifted = aValue >> 1; + return isNegative + ? -shifted + : shifted; +} + +/** + * Returns the base 64 VLQ encoded value. + */ +exports.encode = function base64VLQ_encode(aValue) { + var encoded = ""; + var digit; + + var vlq = toVLQSigned(aValue); + + do { + digit = vlq & VLQ_BASE_MASK; + vlq >>>= VLQ_BASE_SHIFT; + if (vlq > 0) { + // There are still more digits in this value, so we must make sure the + // continuation bit is marked. + digit |= VLQ_CONTINUATION_BIT; + } + encoded += base64.encode(digit); + } while (vlq > 0); + + return encoded; +}; + +/** + * Decodes the next base 64 VLQ value from the given string and returns the + * value and the rest of the string via the out parameter. + */ +exports.decode = function base64VLQ_decode(aStr, aOutParam) { + var i = 0; + var strLen = aStr.length; + var result = 0; + var shift = 0; + var continuation, digit; + + do { + if (i >= strLen) { + throw new Error("Expected more digits in base 64 VLQ value."); + } + digit = base64.decode(aStr.charAt(i++)); + continuation = !!(digit & VLQ_CONTINUATION_BIT); + digit &= VLQ_BASE_MASK; + result = result + (digit << shift); + shift += VLQ_BASE_SHIFT; + } while (continuation); + + aOutParam.value = fromVLQSigned(result); + aOutParam.rest = aStr.slice(i); +}; + diff --git a/react-packager/src/Packager/index.js b/react-packager/src/Packager/index.js new file mode 100644 index 00000000..3ec4e378 --- /dev/null +++ b/react-packager/src/Packager/index.js @@ -0,0 +1,127 @@ +'use strict'; + +var assert = require('assert'); +var fs = require('fs'); +var path = require('path'); +var q = require('q'); +var Promise = require('q').Promise; +var Transformer = require('../JSTransformer'); +var DependencyResolver = require('../DependencyResolver'); +var _ = require('underscore'); +var Package = require('./Package'); +var Activity = require('../Activity'); + +var DEFAULT_CONFIG = { + /** + * RegExp used to ignore paths when scanning the filesystem to calculate the + * dependency graph. + */ + blacklistRE: null, + + /** + * The kind of module system/transport wrapper to use for the modules bundled + * in the package. + */ + moduleFormat: 'haste', + + /** + * An ordered list of module names that should be considered as dependencies + * of every module in the system. The list is ordered because every item in + * the list will have an implicit dependency on all items before it. + * + * (This ordering is necessary to build, for example, polyfills that build on + * each other) + */ + polyfillModuleNames: [], + + nonPersistent: false, +}; + +function Packager(projectConfig) { + projectConfig.projectRoots.forEach(verifyRootExists); + + this._config = Object.create(DEFAULT_CONFIG); + for (var key in projectConfig) { + this._config[key] = projectConfig[key]; + } + + this._resolver = new DependencyResolver(this._config); + + this._transformer = new Transformer(projectConfig); +} + +Packager.prototype.kill = function() { + return q.all([ + this._transformer.kill(), + this._resolver.end(), + ]); +}; + +Packager.prototype.package = function(main, runModule, sourceMapUrl) { + var transformModule = this._transformModule.bind(this); + var ppackage = new Package(sourceMapUrl); + + var findEventId = Activity.startEvent('find dependencies'); + var transformEventId; + + return this.getDependencies(main) + .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(transformed) { + ppackage.addModule( + transformed.code, + transformed.sourceCode, + transformed.sourcePath + ); + }); + + ppackage.finalize({ runMainModule: runModule }); + return ppackage; + }); +}; + +Packager.prototype.invalidateFile = function(filePath) { + this._transformer.invalidateFile(filePath); +} + +Packager.prototype.getDependencies = function(main) { + return this._resolver.getDependencies(main); +}; + +Packager.prototype._transformModule = function(module) { + var resolver = this._resolver; + return this._transformer.loadFileAndTransform( + ['es6'], + path.resolve(module.path), + this._config.transformer || {} + ).then(function(transformed) { + return _.extend( + {}, + transformed, + {code: resolver.wrapModule(module, transformed.code)} + ); + }); +}; + + +function verifyRootExists(root) { + // Verify that the root exists. + assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); +} + +Packager.prototype.getGraphDebugInfo = function() { + return this._resolver.getDebugInfo(); +}; + + +module.exports = Packager; diff --git a/react-packager/src/Server/__tests__/Server-test.js b/react-packager/src/Server/__tests__/Server-test.js new file mode 100644 index 00000000..511ec8a3 --- /dev/null +++ b/react-packager/src/Server/__tests__/Server-test.js @@ -0,0 +1,158 @@ +jest.setMock('worker-farm', function(){ return function(){}; }) + .dontMock('q') + .dontMock('os') + .dontMock('errno/custom') + .dontMock('path') + .dontMock('url') + .dontMock('../'); + + +var server = require('../'); +var q = require('q'); + +describe('processRequest', function(){ + var server; + var Activity; + var Packager; + var FileWatcher; + + var options = { + projectRoots: ['root'], + blacklistRE: null, + cacheVersion: null, + polyfillModuleNames: null + }; + + var makeRequest = function(requestHandler, requrl){ + var deferred = q.defer(); + requestHandler({ + url: requrl + },{ + end: function(res){ + deferred.resolve(res); + } + },{ + next: function(){} + } + ); + return deferred.promise; + }; + + var invalidatorFunc = jest.genMockFunction(); + var watcherFunc = jest.genMockFunction(); + var requestHandler; + + beforeEach(function(){ + Activity = require('../../Activity'); + Packager = require('../../Packager'); + FileWatcher = require('../../FileWatcher') + + Packager.prototype.package = function(main, runModule, sourceMapUrl) { + return q({ + getSource: function(){ + return "this is the source" + }, + getSourceMap: function(){ + return "this is the source map" + } + }) + }; + + FileWatcher.prototype.on = watcherFunc; + + Packager.prototype.invalidateFile = invalidatorFunc; + + var Server = require('../'); + server = new Server(options); + requestHandler = server.processRequest.bind(server); + }); + + pit('returns JS bundle source on request of *.bundle',function(){ + result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); + return result.then(function(response){ + expect(response).toEqual("this is the source"); + }); + }); + + pit('returns sourcemap on request of *.map', function(){ + result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle.map'); + return result.then(function(response){ + expect(response).toEqual('"this is the source map"'); + }); + }); + + pit('watches all files in projectRoot', function(){ + result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); + return result.then(function(response){ + expect(watcherFunc.mock.calls[0][0]).toEqual('all'); + expect(watcherFunc.mock.calls[0][1]).not.toBe(null); + }) + }); + + + describe('file changes', function() { + var triggerFileChange; + beforeEach(function() { + FileWatcher.prototype.on = function(eventType, callback) { + if (eventType !== 'all') { + throw new Error('Can only handle "all" event in watcher.'); + } + triggerFileChange = callback; + return this; + }; + }); + + pit('invalides files in package when file is updated', function() { + result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); + return result.then(function(response){ + var onFileChange = watcherFunc.mock.calls[0][1]; + onFileChange('all','path/file.js', 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 + .mockReturnValueOnce( + q({ + getSource: function(){ + return "this is the first source" + }, + getSourceMap: function(){}, + }) + ) + .mockReturnValue( + q({ + getSource: function(){ + return "this is the rebuilt source" + }, + getSourceMap: function(){}, + }) + ); + + Packager.prototype.package = packageFunc; + + var Server = require('../../Server'); + var server = new Server(options); + + requestHandler = server.processRequest.bind(server); + + + return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle') + .then(function(response){ + expect(response).toEqual("this is the first source"); + expect(packageFunc.mock.calls.length).toBe(1); + triggerFileChange('all','path/file.js', options.projectRoots[0]); + jest.runAllTimers(); + }) + .then(function(){ + expect(packageFunc.mock.calls.length).toBe(2); + return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle') + .then(function(response){ + expect(response).toEqual("this is the rebuilt source"); + }); + }); + }); + }); +}); diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js new file mode 100644 index 00000000..26929ebb --- /dev/null +++ b/react-packager/src/Server/index.js @@ -0,0 +1,173 @@ +var url = require('url'); +var path = require('path'); +var FileWatcher = require('../FileWatcher') +var Packager = require('../Packager'); +var Activity = require('../Activity'); +var q = require('q'); + +module.exports = Server; + +function Server(options) { + this._projectRoots = options.projectRoots; + this._packages = Object.create(null); + this._packager = new Packager({ + projectRoots: options.projectRoots, + blacklistRE: options.blacklistRE, + polyfillModuleNames: options.polyfillModuleNames || [], + runtimeCode: options.runtimeCode, + cacheVersion: options.cacheVersion, + resetCache: options.resetCache, + dev: options.dev, + transformModulePath: options.transformModulePath, + nonPersistent: options.nonPersistent, + }); + + this._fileWatcher = options.nonPersistent + ? FileWatcher.createDummyWatcher() + : new FileWatcher(options.projectRoots); + + var onFileChange = this._onFileChange.bind(this); + this._fileWatcher.on('all', onFileChange); +} + +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. + setImmediate(this._rebuildPackages.bind(this, absPath)) +}; + +Server.prototype._rebuildPackages = function(filepath) { + var buildPackage = this._buildPackage.bind(this); + var packages = this._packages; + Object.keys(packages).forEach(function(key) { + var options = getOptionsFromPath(url.parse(key).pathname); + packages[key] = buildPackage(options).then(function(p) { + // Make a throwaway call to getSource to cache the source string. + p.getSource(); + return p; + }); + }); +}; + +Server.prototype.end = function() { + q.all([ + this._fileWatcher.end(), + this._packager.kill(), + ]); +}; + +Server.prototype._buildPackage = function(options) { + return this._packager.package( + options.main, + options.runModule, + options.sourceMapUrl + ); +}; + +Server.prototype.buildPackageFromUrl = function(reqUrl) { + var options = getOptionsFromPath(url.parse(reqUrl).pathname); + return this._buildPackage(options); +}; + +Server.prototype.getDependencies = function(main) { + return this._packager.getDependencies(main); +}; + +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 += ''; + ret += ''; + res.end(ret); + } else if (parts[1] === 'packages') { + ret += '

Cached Packages

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

' + url + '

'; + ret += p.getDebugInfo(); + }); + }, this)).then( + function() { res.end(ret); }, + function(e) { + res.wrteHead(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.processRequest = function(req, res, next) { + var requestType; + if (req.url.match(/\.bundle$/)) { + requestType = 'bundle'; + } else if (req.url.match(/\.map$/)) { + requestType = 'map'; + } else if (req.url.match(/^\/debug/)) { + this._processDebugRequest(req.url, res); + return; + } else { + return next(); + } + + var startReqEventId = Activity.startEvent('request:' + req.url); + var options = getOptionsFromPath(url.parse(req.url).pathname); + var building = this._packages[req.url] || this._buildPackage(options) + this._packages[req.url] = building; + building.then( + function(p) { + if (requestType === 'bundle') { + res.end(p.getSource()); + Activity.endEvent(startReqEventId); + } else if (requestType === 'map') { + res.end(JSON.stringify(p.getSourceMap())); + Activity.endEvent(startReqEventId); + } + }, + function(error) { + handleError(res, error); + } + ).done(); +}; + +function getOptionsFromPath(pathname) { + var parts = pathname.split('.'); + // Remove the leading slash. + var main = parts[0].slice(1) + '.js'; + return { + runModule: parts.slice(1).some(function(part) { + return part === 'runModule'; + }), + main: main, + sourceMapUrl: parts.slice(0, -1).join('.') + '.map' + }; +} + +function handleError(res, error) { + res.writeHead(500, { + 'Content-Type': 'application/json; charset=UTF-8', + }); + + if (error.type === 'TransformError') { + res.end(JSON.stringify(error)); + } else { + console.error(error.stack || error); + res.end(JSON.stringify({ + type: 'InternalError', + message: 'react-packager has encountered an internal error, ' + + 'please check your terminal error output for more details', + })); + } +} diff --git a/react-packager/src/fb-path-utils/index.js b/react-packager/src/fb-path-utils/index.js new file mode 100644 index 00000000..b4a1cb96 --- /dev/null +++ b/react-packager/src/fb-path-utils/index.js @@ -0,0 +1,14 @@ +var absolutePath = require('absolute-path'); +var path = require('path'); +var pathIsInside = require('path-is-inside'); + +function isAbsolutePath(pathStr) { + return absolutePath(pathStr); +} + +function isChildPath(parentPath, childPath) { + return pathIsInside(parentPath, childPath); +} + +exports.isAbsolutePath = isAbsolutePath; +exports.isChildPath = isChildPath; diff --git a/transformer.js b/transformer.js new file mode 100644 index 00000000..df5e7143 --- /dev/null +++ b/transformer.js @@ -0,0 +1,57 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * Note: This is a fork of the fb-specific transform.js + */ +'use strict'; + +var jstransform = require('jstransform').transform; + +var reactVisitors = + require('react-tools/vendor/fbtransform/visitors').getAllVisitors(); +var staticTypeSyntax = + require('jstransform/visitors/type-syntax').visitorList; +// Note that reactVisitors now handles ES6 classes, rest parameters, arrow +// functions, template strings, and object short notation. +var visitorList = reactVisitors; + + +function transform(transformSets, srcTxt, options) { + options = options || {}; + + // These tranforms mostly just erase type annotations and static typing + // related statements, but they were conflicting with other tranforms. + // Running them first solves that problem + var staticTypeSyntaxResult = jstransform( + staticTypeSyntax, + srcTxt + ); + + return jstransform(visitorList, staticTypeSyntaxResult.code); +} + +module.exports = function(data, callback) { + var result; + try { + result = transform( + data.transformSets, + data.sourceCode, + data.options + ); + } catch (e) { + return callback(null, { + error: { + lineNumber: e.lineNumber, + column: e.column, + message: e.message, + stack: e.stack, + description: e.description + } + }); + } + + callback(null, result); +}; + +// export for use in jest +module.exports.transform = transform;