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 fs = require('fs');
const mockImageWidth = 300;
const mockImageHeight = 200;
require('image-size').mockReturnValue({
width: 300,
height: 200,
width: mockImageWidth,
height: mockImageHeight,
});
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.objectContaining({
__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.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:', () => {
let mockFS;
@ -237,17 +301,21 @@ describe('getAssetData', () => {
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')}),
);
});
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';
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);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
// 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) {
options = options || {
assetDataPlugins: [],
platform: '',
projectRoot: '',
inlineRequires: false,