diff --git a/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/react-packager/src/AssetServer/__tests__/AssetServer-test.js new file mode 100644 index 00000000..eede72c0 --- /dev/null +++ b/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -0,0 +1,85 @@ +'use strict'; + +jest + .autoMockOff() + .mock('../../lib/declareOpts') + .mock('fs'); + +var fs = require('fs'); +var AssetServer = require('../'); +var Promise = require('bluebird'); + +describe('AssetServer', function() { + pit('should work for the simple case', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + 'b@2x.png': 'b2 image', + } + } + }); + + return Promise.all([ + server.get('imgs/b.png'), + server.get('imgs/b@1x.png'), + ]).then(function(resp) { + resp.forEach(function(data) { + expect(data).toBe('b image'); + }); + }); + }); + + pit.only('should pick the bigger one', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + 'b@4x.png': 'b4 image', + 'b@4.5x.png': 'b4.5 image', + } + } + }); + + return server.get('imgs/b@3x.png').then(function(data) { + expect(data).toBe('b4 image'); + }); + }); + + pit('should support multiple project roots', function() { + var server = new AssetServer({ + projectRoots: ['/root'], + assetExts: ['png'], + }); + + fs.__setMockFilesystem({ + 'root': { + imgs: { + 'b.png': 'b image', + }, + 'root2': { + 'newImages': { + 'imgs': { + 'b@1x.png': 'b1 image', + }, + }, + }, + } + }); + + return server.get('newImages/imgs/b.png').then(function(data) { + expect(data).toBe('b1 image'); + }); + }); +}); diff --git a/react-packager/src/AssetServer/index.js b/react-packager/src/AssetServer/index.js new file mode 100644 index 00000000..bdabafff --- /dev/null +++ b/react-packager/src/AssetServer/index.js @@ -0,0 +1,132 @@ +/** + * 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 declareOpts = require('../lib/declareOpts'); +var extractAssetResolution = require('../lib/extractAssetResolution'); +var path = require('path'); +var Promise = require('bluebird'); +var fs = require('fs'); + +var lstat = Promise.promisify(fs.lstat); +var readDir = Promise.promisify(fs.readdir); +var readFile = Promise.promisify(fs.readFile); + +module.exports = AssetServer; + +var validateOpts = declareOpts({ + projectRoots: { + type: 'array', + required: true, + }, + assetExts: { + type: 'array', + default: ['png'], + }, +}); + +function AssetServer(options) { + var opts = validateOpts(options); + this._roots = opts.projectRoots; + this._assetExts = opts.assetExts; +} + +/** + * Given a request for an image by path. That could contain a resolution + * postfix, we need to find that image (or the closest one to it's resolution) + * in one of the project roots: + * + * 1. We first parse the directory of the asset + * 2. We check to find a matching directory in one of the project roots + * 3. We then build a map of all assets and their resolutions in this directory + * 4. Then pick the closest resolution (rounding up) to the requested one + */ + +AssetServer.prototype.get = function(assetPath) { + var filename = path.basename(assetPath); + + return findRoot( + this._roots, + path.dirname(assetPath) + ).then(function(dir) { + return [ + dir, + readDir(dir), + ]; + }).spread(function(dir, files) { + // Easy case. File exactly what the client requested. + var index = files.indexOf(filename); + if (index > -1) { + return readFile(path.join(dir, filename)); + } + + var assetData = extractAssetResolution(filename); + var map = buildAssetMap(dir, files); + var record = map[assetData.assetName]; + + if (!record) { + throw new Error('Asset not found'); + } + + for (var i = 0; i < record.resolutions.length; i++) { + if (record.resolutions[i] >= assetData.resolution) { + return readFile(record.files[i]); + } + } + + return readFile(record.files[record.files.length - 1]); + }); +}; + +function findRoot(roots, dir) { + return Promise.some( + roots.map(function(root) { + var absPath = path.join(root, dir); + return lstat(absPath).then(function(stat) { + if (!stat.isDirectory()) { + throw new Error('Looking for dirs'); + } + stat._path = absPath; + return stat; + }); + }), + 1 + ).spread( + function(stat) { + return stat._path; + } + ); +} + +function buildAssetMap(dir, files) { + var assets = files.map(extractAssetResolution); + var map = Object.create(null); + assets.forEach(function(asset, i) { + var file = files[i]; + var record = map[asset.assetName]; + if (!record) { + record = map[asset.assetName] = { + resolutions: [], + files: [], + }; + } + + var insertIndex; + var length = record.resolutions.length; + for (insertIndex = 0; insertIndex < length; insertIndex++) { + if (asset.resolution < record.resolutions[insertIndex]) { + break; + } + } + record.resolutions.splice(insertIndex, 0, asset.resolution); + record.files.splice(insertIndex, 0, path.join(dir, file)); + }); + + return map; +} 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 40705911..98ae7eb7 100644 --- a/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -14,6 +14,7 @@ jest .dontMock('absolute-path') .dontMock('../docblock') .dontMock('../../replacePatterns') + .dontMock('../../../../lib/extractAssetResolution') .setMock('../../../ModuleDescriptor', function(data) {return data;}); describe('DependencyGraph', function() { diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index f92a3195..9257d788 100644 --- a/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -18,6 +18,7 @@ var isAbsolutePath = require('absolute-path'); var debug = require('debug')('DependecyGraph'); var util = require('util'); var declareOpts = require('../../../lib/declareOpts'); +var extractAssetResolution = require('../../../lib/extractAssetResolution'); var readFile = Promise.promisify(fs.readFile); var readDir = Promise.promisify(fs.readdir); @@ -421,7 +422,7 @@ DependecyGraph.prototype._processModule = function(modulePath) { var module; if (this._assetExts.indexOf(extname(modulePath)) > -1) { - var assetData = extractResolutionPostfix(this._lookupName(modulePath)); + var assetData = extractAssetResolution(this._lookupName(modulePath)); moduleData.id = assetData.assetName; moduleData.resolution = assetData.resolution; moduleData.isAsset = true; @@ -772,28 +773,6 @@ function extname(name) { return path.extname(name).replace(/^\./, ''); } -function extractResolutionPostfix(filename) { - var ext = extname(filename); - var re = new RegExp('@([\\d\\.]+)x\\.' + ext + '$'); - - var match = filename.match(re); - var resolution; - - if (!(match && match[1])) { - resolution = 1; - } else { - resolution = parseFloat(match[1], 10); - if (isNaN(resolution)) { - resolution = 1; - } - } - - return { - resolution: resolution, - assetName: match ? filename.replace(re, '.' + ext) : filename, - }; -} - function NotFoundError() { Error.call(this); Error.captureStackTrace(this, this.constructor); diff --git a/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js index 78298a2f..8df5bbcc 100644 --- a/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js +++ b/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js @@ -23,7 +23,7 @@ get: function() { console.error( 'Your code is broken! Do not iterate over arrays with ' + - 'for...in. See https://fburl.com/31944000 for more information.' + 'for...in.' ); } } @@ -49,9 +49,6 @@ * - Use a regular for loop with index. * - Use one of the array methods: a.forEach, a.map, etc. * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - * - * More info: - * https://our.intern.facebook.com/intern/dex/qa/669736606441771/ */ if (this == null) { throw new TypeError( @@ -92,9 +89,6 @@ * - Use a regular for loop with index. * - Use one of the array methods: a.forEach, a.map, etc. * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check. - * - * More info: - * https://our.intern.facebook.com/intern/dex/qa/669736606441771/ */ if (this == null) { throw new TypeError('Array.prototype.find called on null or undefined'); diff --git a/react-packager/src/Packager/__tests__/Packager-test.js b/react-packager/src/Packager/__tests__/Packager-test.js index cc5c4471..0c9d4a84 100644 --- a/react-packager/src/Packager/__tests__/Packager-test.js +++ b/react-packager/src/Packager/__tests__/Packager-test.js @@ -111,7 +111,7 @@ describe('Packager', function() { var imgModule = { isStatic: true, path: '/root/img/new_image.png', - uri: 'img/new_image.png', + uri: 'assets/img/new_image.png', width: 25, height: 50, }; diff --git a/react-packager/src/Packager/index.js b/react-packager/src/Packager/index.js index 9e94be21..74e2ff4c 100644 --- a/react-packager/src/Packager/index.js +++ b/react-packager/src/Packager/index.js @@ -194,7 +194,7 @@ function generateAssetModule(module, relPath) { var img = { isStatic: true, path: module.path, //TODO(amasad): this should be path inside tar file. - uri: relPath, + uri: path.join('assets', relPath), width: dimensions.width / module.resolution, height: dimensions.height / module.resolution, }; diff --git a/react-packager/src/Server/__tests__/Server-test.js b/react-packager/src/Server/__tests__/Server-test.js index a7b65021..58a1aca7 100644 --- a/react-packager/src/Server/__tests__/Server-test.js +++ b/react-packager/src/Server/__tests__/Server-test.js @@ -229,4 +229,32 @@ describe('processRequest', function() { expect(res.end).not.toBeCalled(); }); }); + + describe.only('/assets endpoint', function() { + var AssetServer; + beforeEach(function() { + AssetServer = require('../../AssetServer'); + }); + + it('should serve simple case', function() { + var req = { + url: '/assets/imgs/a.png', + }; + var res = { + end: jest.genMockFn(), + }; + + AssetServer.prototype.get.mockImpl(function() { + return Promise.resolve('i am image'); + }); + + server.processRequest(req, res); + jest.runAllTimers(); + expect(res.end).toBeCalledWith('i am image'); + }); + + it('should return 404', function() { + + }); + }); }); diff --git a/react-packager/src/Server/index.js b/react-packager/src/Server/index.js index 617359bf..3c7be043 100644 --- a/react-packager/src/Server/index.js +++ b/react-packager/src/Server/index.js @@ -14,8 +14,11 @@ var declareOpts = require('../lib/declareOpts'); var FileWatcher = require('../FileWatcher'); var Packager = require('../Packager'); var Activity = require('../Activity'); +var AssetServer = require('../AssetServer'); var Promise = require('bluebird'); var _ = require('underscore'); +var exec = require('child_process').exec; +var fs = require('fs'); module.exports = Server; @@ -99,6 +102,11 @@ function Server(options) { packagerOpts.fileWatcher = this._fileWatcher; this._packager = new Packager(packagerOpts); + this._assetServer = new AssetServer({ + projectRoots: opts.projectRoots, + assetExts: opts.assetExts, + }); + var onFileChange = this._onFileChange.bind(this); this._fileWatcher.on('all', onFileChange); @@ -230,6 +238,58 @@ Server.prototype._processOnChangeRequest = function(req, res) { }); }; +Server.prototype._processAssetsRequest = function(req, res) { + var urlObj = url.parse(req.url, true); + var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); + this._assetServer.get(assetPath[1]) + .then( + function(data) { + res.end(data); + }, + function(error) { + console.error(error.stack); + res.writeHead('404'); + res.end('Asset not found'); + } + ).done(); +}; + +Server.prototype._processProfile = function(req, res) { + console.log('Dumping profile information...'); + var dumpName = '/tmp/dump_' + Date.now() + '.json'; + var prefix = process.env.TRACE_VIEWER_PATH || ''; + var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; + fs.writeFileSync(dumpName, req.rawBody); + exec(cmd, function (error) { + if (error) { + if (error.code === 127) { + console.error( + '\n** Failed executing `' + cmd + '` **\n\n' + + 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + + 'You can get it at:\n\n' + + ' https://github.com/google/trace-viewer\n\n' + + 'If it\'s not in your path, you can set a custom path with:\n\n' + + ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + + 'NOTE: Your profile data was kept at:\n\n' + + ' ' + dumpName + ); + } else { + console.error('Unknown error', error); + } + res.end(); + return; + } else { + exec('rm ' + dumpName); + exec('open ' + dumpName.replace(/json$/, 'html'), function (error) { + if (error) { + console.error(error); + } + res.end(); + }); + } + }); +}; + Server.prototype.processRequest = function(req, res, next) { var urlObj = url.parse(req.url, true); var pathname = urlObj.pathname; @@ -245,6 +305,12 @@ Server.prototype.processRequest = function(req, res, next) { } else if (pathname.match(/^\/onchange\/?$/)) { this._processOnChangeRequest(req, res); return; + } else if (pathname.match(/^\/assets\//)) { + this._processAssetsRequest(req, res); + return; + } else if (pathname.match(/^\/profile\/?$/)) { + this._processProfile(req, res); + return; } else { next(); return; diff --git a/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js b/react-packager/src/__mocks__/fs.js similarity index 96% rename from react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js rename to react-packager/src/__mocks__/fs.js index 3ebee183..0ea13d15 100644 --- a/react-packager/src/DependencyResolver/haste/DependencyGraph/__mocks__/fs.js +++ b/react-packager/src/__mocks__/fs.js @@ -42,6 +42,11 @@ fs.readdir.mockImpl(function(filepath, callback) { }); fs.readFile.mockImpl(function(filepath, encoding, callback) { + if (arguments.length === 2) { + callback = encoding; + encoding = null; + } + try { var node = getToNode(filepath); // dir check diff --git a/react-packager/src/lib/__tests__/extractAssetResolution-test.js b/react-packager/src/lib/__tests__/extractAssetResolution-test.js new file mode 100644 index 00000000..ad5ac3fb --- /dev/null +++ b/react-packager/src/lib/__tests__/extractAssetResolution-test.js @@ -0,0 +1,42 @@ +'use strict'; + +jest.autoMockOff(); +var extractAssetResolution = require('../extractAssetResolution'); + +describe('extractAssetResolution', function() { + it('should extract resolution simple case', function() { + var data = extractAssetResolution('test@2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 2, + }); + }); + + it('should default resolution to 1', function() { + var data = extractAssetResolution('test.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1, + }); + }); + + it('should support float', function() { + var data = extractAssetResolution('test@1.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 1.1, + }); + + data = extractAssetResolution('test@.1x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.1, + }); + + data = extractAssetResolution('test@0.2x.png'); + expect(data).toEqual({ + assetName: 'test.png', + resolution: 0.2, + }); + }); +}); diff --git a/react-packager/src/lib/extractAssetResolution.js b/react-packager/src/lib/extractAssetResolution.js new file mode 100644 index 00000000..8fb91afc --- /dev/null +++ b/react-packager/src/lib/extractAssetResolution.js @@ -0,0 +1,28 @@ +'use strict'; + +var path = require('path'); + +function extractAssetResolution(filename) { + var ext = path.extname(filename); + + var re = new RegExp('@([\\d\\.]+)x\\' + ext + '$'); + + var match = filename.match(re); + var resolution; + + if (!(match && match[1])) { + resolution = 1; + } else { + resolution = parseFloat(match[1], 10); + if (isNaN(resolution)) { + resolution = 1; + } + } + + return { + resolution: resolution, + assetName: match ? filename.replace(re, ext) : filename, + }; +} + +module.exports = extractAssetResolution;