Skip to content

Commit

Permalink
Add "assetPlugin" option to allow arbitrary asset processing
Browse files Browse the repository at this point in the history
Summary:
Adds a new URL option to the packager server called "assetPlugin". This can be a name of a Node module or multiple Node modules (`assetPlugin=module1&assetPlugin=module2`). Each plugin is loaded using `require()` and is expected to export a function. Each plugin function is invoked with an asset as the argument. The plugins may be async functions; the packager will properly wait for them to settle and will chain them.

A plugin may be used to add extra metadata to an asset. For example it may add an array of hashes for all of the files belonging to an asset, or it may add the duration of a sound clip asset.
Closes #9993

Differential Revision: D3895384

Pulled By: davidaurelio

fbshipit-source-id: 0afe24012fc54b6d18d9b2df5f5675d27ea58320
  • Loading branch information
ide authored and Facebook Github Bot 6 committed Sep 20, 2016
1 parent ebecd48 commit 5ac7706
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 9 deletions.
59 changes: 59 additions & 0 deletions packager/react-packager/src/Bundler/__tests__/Bundler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() =>
Expand Down
38 changes: 33 additions & 5 deletions packager/react-packager/src/Bundler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ class Bundler {
resolutionResponse,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
}) {
const onResolutionResponse = response => {
bundle.setMainModuleId(response.getModuleId(getMainModule(response)));
Expand Down Expand Up @@ -303,6 +304,7 @@ class Bundler {
finalizeBundle,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
});
}

Expand All @@ -313,6 +315,7 @@ class Bundler {
sourceMapUrl,
dev,
platform,
assetPlugins,
}) {
const onModuleTransformed = ({module, transformed, response, bundle}) => {
const deps = Object.create(null);
Expand Down Expand Up @@ -341,6 +344,7 @@ class Bundler {
finalizeBundle,
minify: false,
bundle: new PrepackBundle(sourceMapUrl),
assetPlugins,
});
}

Expand All @@ -355,6 +359,7 @@ class Bundler {
resolutionResponse,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onResolutionResponse = noop,
onModuleTransformed = noop,
finalizeBundle = noop,
Expand Down Expand Up @@ -416,6 +421,7 @@ class Bundler {
module,
bundle,
entryFilePath,
assetPlugins,
transformOptions: response.transformOptions,
getModuleId: response.getModuleId,
dependencyPairs: response.getResolvedDependencyPairs(module),
Expand Down Expand Up @@ -557,6 +563,7 @@ class Bundler {
transformOptions,
getModuleId,
dependencyPairs,
assetPlugins,
}) {
let moduleTransport;
const moduleId = getModuleId(module);
Expand All @@ -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) {
Expand Down Expand Up @@ -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));

Expand All @@ -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 = {
Expand All @@ -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 =
Expand All @@ -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({
Expand Down
28 changes: 28 additions & 0 deletions packager/react-packager/src/Server/__tests__/Server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ describe('processRequest', () => {
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
});
});
});
Expand All @@ -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'],
});
});
});
Expand Down Expand Up @@ -412,6 +438,7 @@ describe('processRequest', () => {
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
Expand All @@ -434,6 +461,7 @@ describe('processRequest', () => {
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
Expand Down
15 changes: 11 additions & 4 deletions packager/react-packager/src/Server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ const bundleOpts = declareOpts({
generateSourceMaps: {
type: 'boolean',
required: false,
}
},
assetPlugins: {
type: 'array',
default: [],
},
});

const dependencyOpts = declareOpts({
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -838,6 +844,7 @@ class Server {
false,
),
generateSourceMaps: this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap'),
assetPlugins,
};
}

Expand Down

0 comments on commit 5ac7706

Please sign in to comment.