mirror of
https://github.com/status-im/metro.git
synced 2025-01-24 01:49:07 +00:00
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:
parent
1152a69432
commit
da2fdba240
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}),
|
}),
|
||||||
|
@ -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,
|
||||||
|
@ -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], ''));
|
||||||
|
|
||||||
|
@ -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 = [
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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"`;
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user