Add back support for the assetPlugin option

Summary:
**Summary**
Metro used to have support for "asset plugins", which allowed developers to specify arbitrary JS modules that could export a function for adding more fields to asset data objects. Some of this functionality was removed in the delta bundler work -- this PR adds it back.

**Test plan**
Made existing unit tests pass and added unit tests to test asset plugin behavior. Also tested E2E in a React Native project by adding `assetPlugin=/path/to/pluginModule` to a JS bundle URL and ensuring that the plugin ran.
Closes https://github.com/facebook/metro/pull/118

Differential Revision: D6711094

Pulled By: rafeca

fbshipit-source-id: f42c54cfd11bac5103194f85083084eef25fa3cd
This commit is contained in:
James Ide 2018-01-12 05:47:04 -08:00 committed by Facebook Github Bot
parent 1152a69432
commit da2fdba240
12 changed files with 140 additions and 25 deletions

View File

@ -19,9 +19,12 @@ const {getAssetData, getAsset} = require('../');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const mockImageWidth = 300;
const mockImageHeight = 200;
require('image-size').mockReturnValue({ require('image-size').mockReturnValue({
width: 300, width: mockImageWidth,
height: 200, height: mockImageHeight,
}); });
describe('getAsset', () => { describe('getAsset', () => {
@ -160,7 +163,7 @@ describe('getAssetData', () => {
}, },
}); });
return getAssetData('/root/imgs/b.png', 'imgs/b.png').then(data => { return getAssetData('/root/imgs/b.png', 'imgs/b.png', []).then(data => {
expect(data).toEqual( expect(data).toEqual(
expect.objectContaining({ expect.objectContaining({
__packager_asset: true, __packager_asset: true,
@ -192,7 +195,7 @@ describe('getAssetData', () => {
}, },
}); });
const data = await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg'); const data = await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg', []);
expect(data).toEqual( expect(data).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -212,6 +215,67 @@ describe('getAssetData', () => {
); );
}); });
it('loads and runs asset plugins', async () => {
jest.mock(
'mockPlugin1',
() => {
return asset => {
asset.extraReverseHash = asset.hash
.split('')
.reverse()
.join('');
return asset;
};
},
{virtual: true},
);
jest.mock(
'asyncMockPlugin2',
() => {
return async asset => {
expect(asset.extraReverseHash).toBeDefined();
asset.extraPixelCount = asset.width * asset.height;
return asset;
};
},
{virtual: true},
);
fs.__setMockFilesystem({
root: {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@3x.png': 'b3 image',
},
},
});
const data = await getAssetData('/root/imgs/b.png', 'imgs/b.png', [
'mockPlugin1',
'asyncMockPlugin2',
]);
expect(data).toEqual(
expect.objectContaining({
__packager_asset: true,
type: 'png',
name: 'b',
scales: [1, 2, 3],
fileSystemLocation: '/root/imgs',
httpServerLocation: '/assets/imgs',
files: [
'/root/imgs/b@1x.png',
'/root/imgs/b@2x.png',
'/root/imgs/b@3x.png',
],
extraPixelCount: mockImageWidth * mockImageHeight,
}),
);
expect(typeof data.extraReverseHash).toBe('string');
});
describe('hash:', () => { describe('hash:', () => {
let mockFS; let mockFS;
@ -237,17 +301,21 @@ describe('getAssetData', () => {
hash.update(mockFS.root.imgs[name]); hash.update(mockFS.root.imgs[name]);
} }
expect(await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg')).toEqual( expect(await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg', [])).toEqual(
expect.objectContaining({hash: hash.digest('hex')}), expect.objectContaining({hash: hash.digest('hex')}),
); );
}); });
it('changes the hash when the passed-in file watcher emits an `all` event', async () => { it('changes the hash when the passed-in file watcher emits an `all` event', async () => {
const initialData = await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg'); const initialData = await getAssetData(
'/root/imgs/b.jpg',
'imgs/b.jpg',
[],
);
mockFS.root.imgs['b@4x.jpg'] = 'updated data'; mockFS.root.imgs['b@4x.jpg'] = 'updated data';
const data = await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg'); const data = await getAssetData('/root/imgs/b.jpg', 'imgs/b.jpg', []);
expect(data.hash).not.toEqual(initialData.hash); expect(data.hash).not.toEqual(initialData.hash);
}); });
}); });

View File

@ -50,6 +50,10 @@ export type AssetData = AssetDataWithoutFiles & {
+files: Array<string>, +files: Array<string>,
}; };
export type AssetDataPlugin = (
assetData: AssetData,
) => AssetData | Promise<AssetData>;
const hashFiles = denodeify(function hashFilesCb(files, hash, callback) { const hashFiles = denodeify(function hashFilesCb(files, hash, callback) {
if (!files.length) { if (!files.length) {
callback(null); callback(null);
@ -234,6 +238,7 @@ async function getAbsoluteAssetInfo(
async function getAssetData( async function getAssetData(
assetPath: string, assetPath: string,
localPath: string, localPath: string,
assetDataPlugins: $ReadOnlyArray<string>,
platform: ?string = null, platform: ?string = null,
): Promise<AssetData> { ): Promise<AssetData> {
let assetUrlPath = path.join('/assets', path.dirname(localPath)); let assetUrlPath = path.join('/assets', path.dirname(localPath));
@ -244,22 +249,38 @@ async function getAssetData(
} }
const isImage = isAssetTypeAnImage(path.extname(assetPath).slice(1)); const isImage = isAssetTypeAnImage(path.extname(assetPath).slice(1));
const assetData = await getAbsoluteAssetInfo(assetPath, platform); const assetInfo = await getAbsoluteAssetInfo(assetPath, platform);
const dimensions = isImage ? imageSize(assetData.files[0]) : null; const dimensions = isImage ? imageSize(assetInfo.files[0]) : null;
const scale = assetData.scales[0]; const scale = assetInfo.scales[0];
return { const assetData = {
__packager_asset: true, __packager_asset: true,
fileSystemLocation: path.dirname(assetPath), fileSystemLocation: path.dirname(assetPath),
httpServerLocation: assetUrlPath, httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined, width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined, height: dimensions ? dimensions.height / scale : undefined,
scales: assetData.scales, scales: assetInfo.scales,
files: assetData.files, files: assetInfo.files,
hash: assetData.hash, hash: assetInfo.hash,
name: assetData.name, name: assetInfo.name,
type: assetData.type, type: assetInfo.type,
}; };
return await applyAssetDataPlugins(assetDataPlugins, assetData);
}
async function applyAssetDataPlugins(
assetDataPlugins: $ReadOnlyArray<string>,
assetData: AssetData,
): Promise<AssetData> {
if (!assetDataPlugins.length) {
return assetData;
}
const [currentAssetPlugin, ...remainingAssetPlugins] = assetDataPlugins;
// $FlowFixMe: impossible to type a dynamic require.
const assetPluginFunction: AssetDataPlugin = require(currentAssetPlugin);
const resultAssetData = await assetPluginFunction(assetData);
return await applyAssetDataPlugins(remainingAssetPlugins, resultAssetData);
} }
/** /**

View File

@ -159,6 +159,7 @@ class DeltaCalculator extends EventEmitter {
} = this._bundler.getGlobalTransformOptions(); } = this._bundler.getGlobalTransformOptions();
const transformOptionsForBlacklist = { const transformOptionsForBlacklist = {
assetDataPlugins: this._options.assetPlugins,
enableBabelRCLookup, enableBabelRCLookup,
dev: this._options.dev, dev: this._options.dev,
hot: this._options.hot, hot: this._options.hot,

View File

@ -238,7 +238,12 @@ async function getAssets(
module.path, module.path,
); );
return getAssetData(module.path, localPath, options.platform); return getAssetData(
module.path,
localPath,
options.assetPlugins,
options.platform,
);
} }
return null; return null;
}), }),

View File

@ -297,6 +297,7 @@ describe('DeltaCalculator', () => {
describe('getTransformerOptions()', () => { describe('getTransformerOptions()', () => {
it('should calculate the transform options correctly', async () => { it('should calculate the transform options correctly', async () => {
expect(await deltaCalculator.getTransformerOptions()).toEqual({ expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true, dev: true,
enableBabelRCLookup: false, enableBabelRCLookup: false,
hot: true, hot: true,
@ -315,6 +316,7 @@ describe('DeltaCalculator', () => {
); );
expect(await deltaCalculator.getTransformerOptions()).toEqual({ expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true, dev: true,
enableBabelRCLookup: false, enableBabelRCLookup: false,
hot: true, hot: true,
@ -333,6 +335,7 @@ describe('DeltaCalculator', () => {
); );
expect(await deltaCalculator.getTransformerOptions()).toEqual({ expect(await deltaCalculator.getTransformerOptions()).toEqual({
assetDataPlugins: [],
dev: true, dev: true,
enableBabelRCLookup: false, enableBabelRCLookup: false,
hot: true, hot: true,

View File

@ -78,11 +78,13 @@ describe('Serializers', () => {
}, },
}; };
getAssetData.mockImplementation((path, localPath, platform) => ({ getAssetData.mockImplementation(
(path, localPath, assetDataPlugins, platform) => ({
path, path,
platform, platform,
assetData: true, assetData: true,
})); }),
);
toLocalPath.mockImplementation((roots, path) => path.replace(roots[0], '')); toLocalPath.mockImplementation((roots, path) => path.replace(roots[0], ''));

View File

@ -61,6 +61,7 @@ export type Transformer<ExtraOptions: {} = {}> = {
}; };
export type TransformOptionsStrict = {| export type TransformOptionsStrict = {|
+assetDataPlugins: $ReadOnlyArray<string>,
+enableBabelRCLookup: boolean, +enableBabelRCLookup: boolean,
+dev: boolean, +dev: boolean,
+hot: boolean, +hot: boolean,
@ -71,6 +72,7 @@ export type TransformOptionsStrict = {|
|}; |};
export type TransformOptions = { export type TransformOptions = {
+assetDataPlugins: $ReadOnlyArray<string>,
+enableBabelRCLookup?: boolean, +enableBabelRCLookup?: boolean,
+dev?: boolean, +dev?: boolean,
+hot?: boolean, +hot?: boolean,
@ -200,7 +202,11 @@ function transformCode(
}; };
const transformResult = isAsset(filename, assetExts) const transformResult = isAsset(filename, assetExts)
? assetTransformer.transform(transformerArgs, assetRegistryPath) ? assetTransformer.transform(
transformerArgs,
assetRegistryPath,
options.assetDataPlugins,
)
: transformer.transform(transformerArgs); : transformer.transform(transformerArgs);
const postTransformArgs = [ const postTransformArgs = [

View File

@ -88,6 +88,7 @@ describe('transforming JS modules:', () => {
}); });
const defaults = { const defaults = {
assetDataPlugins: [],
dev: false, dev: false,
hot: false, hot: false,
inlineRequires: false, inlineRequires: false,

View File

@ -49,6 +49,7 @@ export type TransformOptions<ExtraOptions> = {|
const NODE_MODULES = path.sep + 'node_modules' + path.sep; const NODE_MODULES = path.sep + 'node_modules' + path.sep;
const defaultTransformOptions = { const defaultTransformOptions = {
assetDataPlugins: [],
dev: false, dev: false,
hot: false, hot: false,
inlineRequires: false, inlineRequires: false,

View File

@ -27,6 +27,7 @@ type Params = {
async function transform( async function transform(
{filename, localPath, options, src}: Params, {filename, localPath, options, src}: Params,
assetRegistryPath: string, assetRegistryPath: string,
assetDataPlugins: $ReadOnlyArray<string>,
): Promise<{ast: Ast}> { ): Promise<{ast: Ast}> {
options = options || { options = options || {
platform: '', platform: '',
@ -35,7 +36,12 @@ async function transform(
minify: false, minify: false,
}; };
const data = await getAssetData(filename, localPath, options.platform); const data = await getAssetData(
filename,
localPath,
assetDataPlugins,
options.platform,
);
return { return {
ast: generateAssetCodeFileAst(assetRegistryPath, data), ast: generateAssetCodeFileAst(assetRegistryPath, data),

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getTransformCacheKeyFn Should return always the same key for the same params 1`] = `"4f055d9baf74ca63c449a03d12d3ea5e06dce486012af2c6d0afa156e0a3350ab9187479"`; exports[`getTransformCacheKeyFn Should return always the same key for the same params 1`] = `"78c7c4254ad9c8290da4f45eec858cee4466daae32be6252a9bc5b8df65171914db10f84"`;

View File

@ -122,6 +122,7 @@ type Params = {
function transform({filename, options, src, plugins}: Params) { function transform({filename, options, src, plugins}: Params) {
options = options || { options = options || {
assetDataPlugins: [],
platform: '', platform: '',
projectRoot: '', projectRoot: '',
inlineRequires: false, inlineRequires: false,