diff --git a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js index dbc00f759657a7..a74d9e5d484e9d 100644 --- a/packager/react-packager/src/Bundler/__tests__/Bundler-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -208,6 +208,65 @@ describe('Bundler', function() { }); }); + it('loads and runs asset plugins', function() { + jest.mock('mockPlugin1', () => { + return asset => { + asset.extraReverseHash = asset.hash.split('').reverse().join(''); + return asset; + }; + }, {virtual: true}); + + jest.mock('asyncMockPlugin2', () => { + return asset => { + expect(asset.extraReverseHash).toBeDefined(); + return new Promise((resolve) => { + asset.extraPixelCount = asset.width * asset.height; + resolve(asset); + }); + }; + }, {virtual: true}); + + const mockAsset = { + scales: [1,2,3], + files: [ + '/root/img/img.png', + '/root/img/img@2x.png', + '/root/img/img@3x.png', + ], + hash: 'i am a hash', + name: 'img', + type: 'png', + }; + assetServer.getAssetData.mockImpl(() => mockAsset); + + return bundler.bundle({ + entryFile: '/root/foo.js', + runBeforeMainModule: [], + runModule: true, + sourceMapUrl: 'source_map_url', + assetPlugins: ['mockPlugin1', 'asyncMockPlugin2'], + }).then(bundle => { + expect(bundle.addAsset.mock.calls[1]).toEqual([{ + __packager_asset: true, + fileSystemLocation: '/root/img', + httpServerLocation: '/assets/img', + width: 25, + height: 50, + scales: [1, 2, 3], + files: [ + '/root/img/img.png', + '/root/img/img@2x.png', + '/root/img/img@3x.png', + ], + hash: 'i am a hash', + name: 'img', + type: 'png', + extraReverseHash: 'hsah a ma i', + extraPixelCount: 1250, + }]); + }); + }); + pit('gets the list of dependencies from the resolver', function() { const entryFile = '/root/foo.js'; return bundler.getDependencies({entryFile, recursive: true}).then(() => diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index dfa7680f0ed416..2e3116a07b2d12 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -258,6 +258,7 @@ class Bundler { resolutionResponse, isolateModuleIDs, generateSourceMaps, + assetPlugins, }) { const onResolutionResponse = response => { bundle.setMainModuleId(response.getModuleId(getMainModule(response))); @@ -303,6 +304,7 @@ class Bundler { finalizeBundle, isolateModuleIDs, generateSourceMaps, + assetPlugins, }); } @@ -313,6 +315,7 @@ class Bundler { sourceMapUrl, dev, platform, + assetPlugins, }) { const onModuleTransformed = ({module, transformed, response, bundle}) => { const deps = Object.create(null); @@ -341,6 +344,7 @@ class Bundler { finalizeBundle, minify: false, bundle: new PrepackBundle(sourceMapUrl), + assetPlugins, }); } @@ -355,6 +359,7 @@ class Bundler { resolutionResponse, isolateModuleIDs, generateSourceMaps, + assetPlugins, onResolutionResponse = noop, onModuleTransformed = noop, finalizeBundle = noop, @@ -416,6 +421,7 @@ class Bundler { module, bundle, entryFilePath, + assetPlugins, transformOptions: response.transformOptions, getModuleId: response.getModuleId, dependencyPairs: response.getResolvedDependencyPairs(module), @@ -557,6 +563,7 @@ class Bundler { transformOptions, getModuleId, dependencyPairs, + assetPlugins, }) { let moduleTransport; const moduleId = getModuleId(module); @@ -566,7 +573,7 @@ class Bundler { this._generateAssetModule_DEPRECATED(bundle, module, moduleId); } else if (module.isAsset()) { moduleTransport = this._generateAssetModule( - bundle, module, moduleId, transformOptions.platform); + bundle, module, moduleId, assetPlugins, transformOptions.platform); } if (moduleTransport) { @@ -629,7 +636,7 @@ class Bundler { }); } - _generateAssetObjAndCode(module, platform = null) { + _generateAssetObjAndCode(module, assetPlugins, platform = null) { const relPath = getPathRelativeToRoot(this._projectRoots, module.path); var assetUrlPath = path.join('/assets', path.dirname(relPath)); @@ -647,7 +654,7 @@ class Bundler { return Promise.all([ isImage ? sizeOf(module.path) : null, this._assetServer.getAssetData(relPath, platform), - ]).then(function(res) { + ]).then((res) => { const dimensions = res[0]; const assetData = res[1]; const asset = { @@ -663,6 +670,8 @@ class Bundler { type: assetData.type, }; + return this._applyAssetPlugins(assetPlugins, asset); + }).then((asset) => { const json = JSON.stringify(filterObject(asset, assetPropertyBlacklist)); const assetRegistryPath = 'react-native/Libraries/Image/AssetRegistry'; const code = @@ -678,11 +687,30 @@ class Bundler { }); } + _applyAssetPlugins(assetPlugins, asset) { + if (!assetPlugins.length) { + return asset; + } + + let [currentAssetPlugin, ...remainingAssetPlugins] = assetPlugins; + let assetPluginFunction = require(currentAssetPlugin); + let result = assetPluginFunction(asset); + + // If the plugin was an async function, wait for it to fulfill before + // applying the remaining plugins + if (typeof result.then === 'function') { + return result.then(resultAsset => + this._applyAssetPlugins(remainingAssetPlugins, resultAsset) + ); + } else { + return this._applyAssetPlugins(remainingAssetPlugins, result); + } + } - _generateAssetModule(bundle, module, moduleId, platform = null) { + _generateAssetModule(bundle, module, moduleId, assetPlugins = [], platform = null) { return Promise.all([ module.getName(), - this._generateAssetObjAndCode(module, platform), + this._generateAssetObjAndCode(module, assetPlugins, platform), ]).then(([name, {asset, code, meta}]) => { bundle.addAsset(asset); return new ModuleTransport({ diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index c2546ffdc31577..b781b5dd524acb 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -160,6 +160,7 @@ describe('processRequest', () => { unbundle: false, entryModuleOnly: false, isolateModuleIDs: false, + assetPlugins: [], }); }); }); @@ -183,6 +184,31 @@ describe('processRequest', () => { unbundle: false, entryModuleOnly: false, isolateModuleIDs: false, + assetPlugins: [], + }); + }); + }); + + pit('passes in the assetPlugin param', function() { + return makeRequest( + requestHandler, + 'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2' + ).then(function(response) { + expect(response.body).toEqual('this is the source'); + expect(Bundler.prototype.bundle).toBeCalledWith({ + entryFile: 'index.js', + inlineSourceMap: false, + minify: false, + hot: false, + runModule: true, + sourceMapUrl: 'index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2', + dev: true, + platform: undefined, + runBeforeMainModule: ['InitializeJavaScriptAppEngine'], + unbundle: false, + entryModuleOnly: false, + isolateModuleIDs: false, + assetPlugins: ['assetPlugin1', 'assetPlugin2'], }); }); }); @@ -412,6 +438,7 @@ describe('processRequest', () => { unbundle: false, entryModuleOnly: false, isolateModuleIDs: false, + assetPlugins: [], }) ); }); @@ -434,6 +461,7 @@ describe('processRequest', () => { unbundle: false, entryModuleOnly: false, isolateModuleIDs: false, + assetPlugins: [], }) ); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 29efa7960456cc..6159452fe38a30 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -153,7 +153,11 @@ const bundleOpts = declareOpts({ generateSourceMaps: { type: 'boolean', required: false, - } + }, + assetPlugins: { + type: 'array', + default: [], + }, }); const dependencyOpts = declareOpts({ @@ -797,9 +801,6 @@ class Server { _getOptionsFromUrl(reqUrl) { // `true` to parse the query param as an object. const urlObj = url.parse(reqUrl, true); - // node v0.11.14 bug see https://github.com/facebook/react-native/issues/218 - urlObj.query = urlObj.query || {}; - const pathname = decodeURIComponent(urlObj.pathname); // Backwards compatibility. Options used to be as added as '.' to the @@ -819,6 +820,11 @@ class Server { const platform = urlObj.query.platform || getPlatformExtension(pathname); + const assetPlugin = urlObj.query.assetPlugin; + const assetPlugins = Array.isArray(assetPlugin) ? + assetPlugin : + (typeof assetPlugin === 'string') ? [assetPlugin] : []; + return { sourceMapUrl: url.format(sourceMapUrlObj), entryFile: entryFile, @@ -838,6 +844,7 @@ class Server { false, ), generateSourceMaps: this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap'), + assetPlugins, }; }