Add "assetPlugin" option to allow arbitrary asset processing
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 https://github.com/facebook/react-native/pull/9993 Differential Revision: D3895384 Pulled By: davidaurelio fbshipit-source-id: 0afe24012fc54b6d18d9b2df5f5675d27ea58320
This commit is contained in:
parent
ebecd4872b
commit
5ac77062be
|
@ -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() {
|
pit('gets the list of dependencies from the resolver', function() {
|
||||||
const entryFile = '/root/foo.js';
|
const entryFile = '/root/foo.js';
|
||||||
return bundler.getDependencies({entryFile, recursive: true}).then(() =>
|
return bundler.getDependencies({entryFile, recursive: true}).then(() =>
|
||||||
|
|
|
@ -258,6 +258,7 @@ class Bundler {
|
||||||
resolutionResponse,
|
resolutionResponse,
|
||||||
isolateModuleIDs,
|
isolateModuleIDs,
|
||||||
generateSourceMaps,
|
generateSourceMaps,
|
||||||
|
assetPlugins,
|
||||||
}) {
|
}) {
|
||||||
const onResolutionResponse = response => {
|
const onResolutionResponse = response => {
|
||||||
bundle.setMainModuleId(response.getModuleId(getMainModule(response)));
|
bundle.setMainModuleId(response.getModuleId(getMainModule(response)));
|
||||||
|
@ -303,6 +304,7 @@ class Bundler {
|
||||||
finalizeBundle,
|
finalizeBundle,
|
||||||
isolateModuleIDs,
|
isolateModuleIDs,
|
||||||
generateSourceMaps,
|
generateSourceMaps,
|
||||||
|
assetPlugins,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +315,7 @@ class Bundler {
|
||||||
sourceMapUrl,
|
sourceMapUrl,
|
||||||
dev,
|
dev,
|
||||||
platform,
|
platform,
|
||||||
|
assetPlugins,
|
||||||
}) {
|
}) {
|
||||||
const onModuleTransformed = ({module, transformed, response, bundle}) => {
|
const onModuleTransformed = ({module, transformed, response, bundle}) => {
|
||||||
const deps = Object.create(null);
|
const deps = Object.create(null);
|
||||||
|
@ -341,6 +344,7 @@ class Bundler {
|
||||||
finalizeBundle,
|
finalizeBundle,
|
||||||
minify: false,
|
minify: false,
|
||||||
bundle: new PrepackBundle(sourceMapUrl),
|
bundle: new PrepackBundle(sourceMapUrl),
|
||||||
|
assetPlugins,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,6 +359,7 @@ class Bundler {
|
||||||
resolutionResponse,
|
resolutionResponse,
|
||||||
isolateModuleIDs,
|
isolateModuleIDs,
|
||||||
generateSourceMaps,
|
generateSourceMaps,
|
||||||
|
assetPlugins,
|
||||||
onResolutionResponse = noop,
|
onResolutionResponse = noop,
|
||||||
onModuleTransformed = noop,
|
onModuleTransformed = noop,
|
||||||
finalizeBundle = noop,
|
finalizeBundle = noop,
|
||||||
|
@ -416,6 +421,7 @@ class Bundler {
|
||||||
module,
|
module,
|
||||||
bundle,
|
bundle,
|
||||||
entryFilePath,
|
entryFilePath,
|
||||||
|
assetPlugins,
|
||||||
transformOptions: response.transformOptions,
|
transformOptions: response.transformOptions,
|
||||||
getModuleId: response.getModuleId,
|
getModuleId: response.getModuleId,
|
||||||
dependencyPairs: response.getResolvedDependencyPairs(module),
|
dependencyPairs: response.getResolvedDependencyPairs(module),
|
||||||
|
@ -557,6 +563,7 @@ class Bundler {
|
||||||
transformOptions,
|
transformOptions,
|
||||||
getModuleId,
|
getModuleId,
|
||||||
dependencyPairs,
|
dependencyPairs,
|
||||||
|
assetPlugins,
|
||||||
}) {
|
}) {
|
||||||
let moduleTransport;
|
let moduleTransport;
|
||||||
const moduleId = getModuleId(module);
|
const moduleId = getModuleId(module);
|
||||||
|
@ -566,7 +573,7 @@ class Bundler {
|
||||||
this._generateAssetModule_DEPRECATED(bundle, module, moduleId);
|
this._generateAssetModule_DEPRECATED(bundle, module, moduleId);
|
||||||
} else if (module.isAsset()) {
|
} else if (module.isAsset()) {
|
||||||
moduleTransport = this._generateAssetModule(
|
moduleTransport = this._generateAssetModule(
|
||||||
bundle, module, moduleId, transformOptions.platform);
|
bundle, module, moduleId, assetPlugins, transformOptions.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moduleTransport) {
|
if (moduleTransport) {
|
||||||
|
@ -629,7 +636,7 @@ class Bundler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateAssetObjAndCode(module, platform = null) {
|
_generateAssetObjAndCode(module, assetPlugins, platform = null) {
|
||||||
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
|
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
|
||||||
var assetUrlPath = path.join('/assets', path.dirname(relPath));
|
var assetUrlPath = path.join('/assets', path.dirname(relPath));
|
||||||
|
|
||||||
|
@ -647,7 +654,7 @@ class Bundler {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
isImage ? sizeOf(module.path) : null,
|
isImage ? sizeOf(module.path) : null,
|
||||||
this._assetServer.getAssetData(relPath, platform),
|
this._assetServer.getAssetData(relPath, platform),
|
||||||
]).then(function(res) {
|
]).then((res) => {
|
||||||
const dimensions = res[0];
|
const dimensions = res[0];
|
||||||
const assetData = res[1];
|
const assetData = res[1];
|
||||||
const asset = {
|
const asset = {
|
||||||
|
@ -663,6 +670,8 @@ class Bundler {
|
||||||
type: assetData.type,
|
type: assetData.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return this._applyAssetPlugins(assetPlugins, asset);
|
||||||
|
}).then((asset) => {
|
||||||
const json = JSON.stringify(filterObject(asset, assetPropertyBlacklist));
|
const json = JSON.stringify(filterObject(asset, assetPropertyBlacklist));
|
||||||
const assetRegistryPath = 'react-native/Libraries/Image/AssetRegistry';
|
const assetRegistryPath = 'react-native/Libraries/Image/AssetRegistry';
|
||||||
const code =
|
const code =
|
||||||
|
@ -678,11 +687,30 @@ class Bundler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_applyAssetPlugins(assetPlugins, asset) {
|
||||||
|
if (!assetPlugins.length) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
_generateAssetModule(bundle, module, moduleId, platform = null) {
|
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, assetPlugins = [], platform = null) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
module.getName(),
|
module.getName(),
|
||||||
this._generateAssetObjAndCode(module, platform),
|
this._generateAssetObjAndCode(module, assetPlugins, platform),
|
||||||
]).then(([name, {asset, code, meta}]) => {
|
]).then(([name, {asset, code, meta}]) => {
|
||||||
bundle.addAsset(asset);
|
bundle.addAsset(asset);
|
||||||
return new ModuleTransport({
|
return new ModuleTransport({
|
||||||
|
|
|
@ -160,6 +160,7 @@ describe('processRequest', () => {
|
||||||
unbundle: false,
|
unbundle: false,
|
||||||
entryModuleOnly: false,
|
entryModuleOnly: false,
|
||||||
isolateModuleIDs: false,
|
isolateModuleIDs: false,
|
||||||
|
assetPlugins: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -183,6 +184,31 @@ describe('processRequest', () => {
|
||||||
unbundle: false,
|
unbundle: false,
|
||||||
entryModuleOnly: false,
|
entryModuleOnly: false,
|
||||||
isolateModuleIDs: 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,
|
unbundle: false,
|
||||||
entryModuleOnly: false,
|
entryModuleOnly: false,
|
||||||
isolateModuleIDs: false,
|
isolateModuleIDs: false,
|
||||||
|
assetPlugins: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -434,6 +461,7 @@ describe('processRequest', () => {
|
||||||
unbundle: false,
|
unbundle: false,
|
||||||
entryModuleOnly: false,
|
entryModuleOnly: false,
|
||||||
isolateModuleIDs: false,
|
isolateModuleIDs: false,
|
||||||
|
assetPlugins: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -153,7 +153,11 @@ const bundleOpts = declareOpts({
|
||||||
generateSourceMaps: {
|
generateSourceMaps: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
required: false,
|
required: false,
|
||||||
}
|
},
|
||||||
|
assetPlugins: {
|
||||||
|
type: 'array',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dependencyOpts = declareOpts({
|
const dependencyOpts = declareOpts({
|
||||||
|
@ -797,9 +801,6 @@ class Server {
|
||||||
_getOptionsFromUrl(reqUrl) {
|
_getOptionsFromUrl(reqUrl) {
|
||||||
// `true` to parse the query param as an object.
|
// `true` to parse the query param as an object.
|
||||||
const urlObj = url.parse(reqUrl, true);
|
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);
|
const pathname = decodeURIComponent(urlObj.pathname);
|
||||||
|
|
||||||
// Backwards compatibility. Options used to be as added as '.' to the
|
// Backwards compatibility. Options used to be as added as '.' to the
|
||||||
|
@ -819,6 +820,11 @@ class Server {
|
||||||
const platform = urlObj.query.platform ||
|
const platform = urlObj.query.platform ||
|
||||||
getPlatformExtension(pathname);
|
getPlatformExtension(pathname);
|
||||||
|
|
||||||
|
const assetPlugin = urlObj.query.assetPlugin;
|
||||||
|
const assetPlugins = Array.isArray(assetPlugin) ?
|
||||||
|
assetPlugin :
|
||||||
|
(typeof assetPlugin === 'string') ? [assetPlugin] : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sourceMapUrl: url.format(sourceMapUrlObj),
|
sourceMapUrl: url.format(sourceMapUrlObj),
|
||||||
entryFile: entryFile,
|
entryFile: entryFile,
|
||||||
|
@ -838,6 +844,7 @@ class Server {
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
generateSourceMaps: this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap'),
|
generateSourceMaps: this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap'),
|
||||||
|
assetPlugins,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue