diff --git a/getFlowTypeCheckMiddleware.js b/getFlowTypeCheckMiddleware.js new file mode 100644 index 00000000..e0f98bde --- /dev/null +++ b/getFlowTypeCheckMiddleware.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var exec = require('child_process').exec; + +function getFlowTypeCheckMiddleware(options) { + return function(req, res, next) { + if (options.skipflow) { + return next(); + } + if (options.flowroot || options.projectRoots.length === 1) { + var flowroot = options.flowroot || options.projectRoots[0]; + } else { + console.warn('flow: No suitable root'); + return next(); + } + exec('command -v flow >/dev/null 2>&1', function(error, stdout) { + if (error) { + console.warn('flow: Skipping because not installed. Install with ' + + '`brew install flow`.'); + return next(); + } else { + return doFlowTypecheck(res, flowroot, next); + } + }); + }; +} + +function doFlowTypecheck(res, flowroot, next) { + var flowCmd = 'cd "' + flowroot + '" && flow --json --timeout 20'; + var start = Date.now(); + console.log('flow: Running static typechecks.'); + exec(flowCmd, function(flowError, stdout) { + if (!flowError) { + console.log('flow: Typechecks passed (' + (Date.now() - start) + 'ms).'); + return next(); + } else { + try { + var flowResponse = JSON.parse(stdout); + var errors = []; + var errorNum = 1; + flowResponse.errors.forEach(function(err) { + // flow errors are paired across callsites, so we indent and prefix to + // group them + var indent = ''; + err.message.forEach(function(msg) { + errors.push({ + description: indent + 'E' + errorNum + ': ' + msg.descr, + filename: msg.path, + lineNumber: msg.line, + column: msg.start, + }); + indent = ' '; + }); + errorNum++; + }); + var message = 'Flow found type errors. If you think these are wrong, ' + + 'make sure flow is up to date, or disable with --skipflow.'; + } catch (e) { + var message = + 'Flow failed to provide parseable output:\n\n`' + stdout + '`'; + console.error(message, '\nException: `', e, '`\n\n'); + } + var error = { + status: 500, + message: message, + type: 'FlowError', + errors: errors, + }; + console.error('flow: Error running command `' + flowCmd + '`:\n', error); + res.writeHead(error.status, { + 'Content-Type': 'application/json; charset=UTF-8', + }); + res.end(JSON.stringify(error)); + } + }); +} + +module.exports = getFlowTypeCheckMiddleware; diff --git a/packager.js b/packager.js index 55004b7c..b098f0a3 100644 --- a/packager.js +++ b/packager.js @@ -13,6 +13,8 @@ var path = require('path'); var exec = require('child_process').exec; var http = require('http'); +var getFlowTypeCheckMiddleware = require('./getFlowTypeCheckMiddleware'); + if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { console.log( '\n' + @@ -40,6 +42,9 @@ var options = parseCommandLine([{ }, { command: 'assetRoots', description: 'specify the root directories of app assets' +}, { + command: 'skipflow', + description: 'Disable flow checks' }]); if (options.projectRoots) { @@ -203,6 +208,7 @@ function runServer( .use(openStackFrameInEditor) .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) + .use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options)); options.projectRoots.forEach(function(root) { @@ -213,5 +219,5 @@ function runServer( .use(connect.compress()) .use(connect.errorHandler()); - return http.createServer(app).listen(options.port, readyCallback); + return http.createServer(app).listen(options.port, '::', readyCallback); } diff --git a/react-packager/src/DependencyResolver/ModuleDescriptor.js b/react-packager/src/DependencyResolver/ModuleDescriptor.js index c56593cf..c46a57e6 100644 --- a/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ b/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -30,8 +30,13 @@ function ModuleDescriptor(fields) { this.isPolyfill = fields.isPolyfill || false; + this.isAsset_DEPRECATED = fields.isAsset_DEPRECATED || false; this.isAsset = fields.isAsset || false; + if (this.isAsset_DEPRECATED && this.isAsset) { + throw new Error('Cannot be an asset and a deprecated asset'); + } + this.altId = fields.altId; this._fields = fields; diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index b6a978c6..f42f6f8a 100644 --- a/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -92,7 +92,7 @@ describe('DependencyGraph', function() { { id: 'image!a', path: '/root/imgs/a.png', dependencies: [], - isAsset: true + isAsset_DEPRECATED: true }, ]); }); @@ -183,7 +183,7 @@ describe('DependencyGraph', function() { id: 'image!a', path: '/root/imgs/a.png', dependencies: [], - isAsset: true + isAsset_DEPRECATED: true }, ]); }); @@ -954,7 +954,7 @@ describe('DependencyGraph', function() { { id: 'image!foo', path: '/root/foo.png', dependencies: [], - isAsset: true, + isAsset_DEPRECATED: true, }, ]); }); diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 3348907f..fbc7de71 100644 --- a/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -596,7 +596,7 @@ DependecyGraph.prototype._processAsset_DEPRECATED = function(file) { this._assetMap_DEPRECATED[name] = new ModuleDescriptor({ id: 'image!' + name, path: path.resolve(file), - isAsset: true, + isAsset_DEPRECATED: true, dependencies: [], }); } diff --git a/react-packager/src/JSTransformer/index.js b/react-packager/src/JSTransformer/index.js index abfae248..962eb7fe 100644 --- a/react-packager/src/JSTransformer/index.js +++ b/react-packager/src/JSTransformer/index.js @@ -125,7 +125,7 @@ function formatError(err, filename, source) { function formatGenericError(err, filename) { var msg = 'TransformError: ' + filename + ': ' + err.message; var error = new TransformError(); - var stack = err.stack.split('\n').slice(0, -1); + var stack = (err.stack || '').split('\n').slice(0, -1); stack.push(msg); error.stack = stack.join('\n'); error.message = msg; diff --git a/react-packager/src/Packager/__tests__/Packager-test.js b/react-packager/src/Packager/__tests__/Packager-test.js index 8f61df97..f4675c47 100644 --- a/react-packager/src/Packager/__tests__/Packager-test.js +++ b/react-packager/src/Packager/__tests__/Packager-test.js @@ -43,14 +43,21 @@ describe('Packager', function() { }; }); - var packager = new Packager({projectRoots: []}); + var packager = new Packager({projectRoots: ['/root']}); var modules = [ {id: 'foo', path: '/root/foo.js', dependencies: []}, {id: 'bar', path: '/root/bar.js', dependencies: []}, - { id: 'image!img', + { + id: 'image!img', path: '/root/img/img.png', - isAsset: true, + isAsset_DEPRECATED: true, dependencies: [], + }, + { + id: 'new_image.png', + path: '/root/img/new_image.png', + isAsset: true, + dependencies: [] } ]; @@ -74,6 +81,10 @@ describe('Packager', function() { return 'lol ' + code + ' lol'; }); + require('image-size').mockImpl(function(path, cb) { + cb(null, { width: 50, height: 100 }); + }); + return packager.package('/root/foo.js', true, 'source_map_url') .then(function(p) { expect(p.addModule.mock.calls[0]).toEqual([ @@ -96,6 +107,24 @@ describe('Packager', function() { '/root/img/img.png' ]); + var imgModule = { + isStatic: true, + path: '/root/img/new_image.png', + uri: 'img/new_image.png', + width: 50, + height: 100, + }; + + expect(p.addModule.mock.calls[3]).toEqual([ + 'lol module.exports = ' + + JSON.stringify(imgModule) + + '; lol', + 'module.exports = ' + + JSON.stringify(imgModule) + + ';', + '/root/img/new_image.png' + ]); + expect(p.finalize.mock.calls[0]).toEqual([ {runMainModule: true} ]); diff --git a/react-packager/src/Packager/index.js b/react-packager/src/Packager/index.js index bf5a635d..cfdd842d 100644 --- a/react-packager/src/Packager/index.js +++ b/react-packager/src/Packager/index.js @@ -18,6 +18,7 @@ var _ = require('underscore'); var Package = require('./Package'); var Activity = require('../Activity'); var declareOpts = require('../lib/declareOpts'); +var imageSize = require('image-size'); var validateOpts = declareOpts({ projectRoots: { @@ -88,6 +89,8 @@ function Packager(options) { transformModulePath: opts.transformModulePath, nonPersistent: opts.nonPersistent, }); + + this._projectRoots = opts.projectRoots; } Packager.prototype.kill = function() { @@ -138,8 +141,13 @@ Packager.prototype.getDependencies = function(main, isDev) { Packager.prototype._transformModule = function(module) { var transform; - if (module.isAsset) { - transform = Promise.resolve(generateAssetModule(module)); + if (module.isAsset_DEPRECATED) { + transform = Promise.resolve(generateAssetModule_DEPRECATED(module)); + } else if (module.isAsset) { + transform = generateAssetModule( + module, + getPathRelativeToRoot(this._projectRoots, module.path) + ); } else { transform = this._transformer.loadFileAndTransform( path.resolve(module.path) @@ -166,7 +174,7 @@ Packager.prototype.getGraphDebugInfo = function() { return this._resolver.getDebugInfo(); }; -function generateAssetModule(module) { +function generateAssetModule_DEPRECATED(module) { var code = 'module.exports = ' + JSON.stringify({ uri: module.id.replace(/^[^!]+!/, ''), isStatic: true, @@ -179,4 +187,39 @@ function generateAssetModule(module) { }; } +var sizeOf = Promise.promisify(imageSize); + +function generateAssetModule(module, relPath) { + return sizeOf(module.path).then(function(dimensions) { + var img = { + isStatic: true, + path: module.path, //TODO(amasad): this should be path inside tar file. + uri: relPath, + width: dimensions.width, + height: dimensions.height, + }; + + var code = 'module.exports = ' + JSON.stringify(img) + ';'; + + return { + code: code, + sourceCode: code, + sourcePath: module.path, + }; + }); +} + +function getPathRelativeToRoot(roots, absPath) { + for (var i = 0; i < roots.length; i++) { + var relPath = path.relative(roots[i], absPath); + if (relPath[0] !== '.') { + return relPath; + } + } + + throw new Error( + 'Expected root module to be relative to one of the project roots' + ); +} + module.exports = Packager; diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index 65507581..617359bf 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -320,6 +320,12 @@ function handleError(res, error) { }); if (error.type === 'TransformError' || error.type === 'NotFoundError') { + error.errors = [{ + description: error.description, + filename: error.filename, + lineNumber: error.lineNumber, + }]; + console.error(error); res.end(JSON.stringify(error)); } else { console.error(error.stack || error);