diff --git a/packages/cli-config/src/schema.ts b/packages/cli-config/src/schema.ts index e2bbfbc1c..eab98a944 100644 --- a/packages/cli-config/src/schema.ts +++ b/packages/cli-config/src/schema.ts @@ -96,6 +96,7 @@ export const dependencyConfig = t t.string(), t.object({ npmPackageName: t.string().optional(), + saveAssetsPlugin: t.string().optional(), dependencyConfig: t.func(), projectConfig: t.func(), linkConfig: t.func(), @@ -180,6 +181,7 @@ export const projectConfig = t t.string(), t.object({ npmPackageName: t.string().optional(), + saveAssetsPlugin: t.string().optional(), dependencyConfig: t.func(), projectConfig: t.func(), linkConfig: t.func(), diff --git a/packages/cli-plugin-metro/src/commands/bundle/__tests__/filterPlatformAssetScales-test.ts b/packages/cli-plugin-metro/src/commands/bundle/__tests__/filterPlatformAssetScales-test.ts index 94364e6dc..26ed43931 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/__tests__/filterPlatformAssetScales-test.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/__tests__/filterPlatformAssetScales-test.ts @@ -14,23 +14,23 @@ jest.dontMock('../filterPlatformAssetScales').dontMock('../assetPathUtils'); describe('filterPlatformAssetScales', () => { it('removes everything but 2x and 3x for iOS', () => { - expect(filterPlatformAssetScales('ios', [1, 1.5, 2, 3, 4])).toEqual([ + expect(filterPlatformAssetScales([1, 2, 3], [1, 1.5, 2, 3, 4])).toEqual([ 1, 2, 3, ]); - expect(filterPlatformAssetScales('ios', [3, 4])).toEqual([3]); + expect(filterPlatformAssetScales([1, 2, 3], [3, 4])).toEqual([3]); }); it('keeps closest largest one if nothing matches', () => { - expect(filterPlatformAssetScales('ios', [0.5, 4, 100])).toEqual([4]); - expect(filterPlatformAssetScales('ios', [0.5, 100])).toEqual([100]); - expect(filterPlatformAssetScales('ios', [0.5])).toEqual([0.5]); - expect(filterPlatformAssetScales('ios', [])).toEqual([]); + expect(filterPlatformAssetScales([1, 2, 3], [0.5, 4, 100])).toEqual([4]); + expect(filterPlatformAssetScales([1, 2, 3], [0.5, 100])).toEqual([100]); + expect(filterPlatformAssetScales([1, 2, 3], [0.5])).toEqual([0.5]); + expect(filterPlatformAssetScales([1, 2, 3], [])).toEqual([]); }); it('keeps all scales for unknown platform', () => { - expect(filterPlatformAssetScales('freebsd', [1, 1.5, 2, 3.7])).toEqual([ + expect(filterPlatformAssetScales(undefined, [1, 1.5, 2, 3.7])).toEqual([ 1, 1.5, 2, diff --git a/packages/cli-plugin-metro/src/commands/bundle/__tests__/getAssetDestPathIOS-test.ts b/packages/cli-plugin-metro/src/commands/bundle/__tests__/getAssetDestPathIOS-test.ts index 502db8906..b59529ffb 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/__tests__/getAssetDestPathIOS-test.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/__tests__/getAssetDestPathIOS-test.ts @@ -8,9 +8,9 @@ * @emails oncall+javascript_foundation */ -import getAssetDestPathIOS from '../getAssetDestPathIOS'; +import getAssetDestPath from '../getAssetDestPath'; -jest.dontMock('../getAssetDestPathIOS'); +jest.dontMock('../getAssetDestPath'); const path = require('path'); @@ -22,7 +22,7 @@ describe('getAssetDestPathIOS', () => { httpServerLocation: '/assets/test', }; - expect(getAssetDestPathIOS(asset, 1)).toBe( + expect(getAssetDestPath(asset, 1)).toBe( path.normalize('assets/test/icon.png'), ); }); @@ -34,10 +34,10 @@ describe('getAssetDestPathIOS', () => { httpServerLocation: '/assets/test', }; - expect(getAssetDestPathIOS(asset, 2)).toBe( + expect(getAssetDestPath(asset, 2)).toBe( path.normalize('assets/test/icon@2x.png'), ); - expect(getAssetDestPathIOS(asset, 3)).toBe( + expect(getAssetDestPath(asset, 3)).toBe( path.normalize('assets/test/icon@3x.png'), ); }); @@ -49,7 +49,7 @@ describe('getAssetDestPathIOS', () => { httpServerLocation: '/assets/../../test', }; - expect(getAssetDestPathIOS(asset, 1)).toBe( + expect(getAssetDestPath(asset, 1)).toBe( path.normalize('assets/__test/icon.png'), ); }); diff --git a/packages/cli-plugin-metro/src/commands/bundle/buildBundle.ts b/packages/cli-plugin-metro/src/commands/bundle/buildBundle.ts index 101193f72..4e89c4596 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/buildBundle.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/buildBundle.ts @@ -17,6 +17,10 @@ import type {Config} from '@react-native-community/cli-types'; import saveAssets from './saveAssets'; import {default as loadMetroConfig} from '../../tools/loadMetroConfig'; import {logger} from '@react-native-community/cli-tools'; +import type {AssetData} from 'metro'; +import saveAssetsAndroid from './saveAssetsAndroid'; +import saveAssetsDefault from './saveAssetsDefault'; +import saveAssetsIOS from './saveAssetsIOS'; interface RequestOptions { entryFile: string; @@ -38,7 +42,18 @@ async function buildBundle( config: args.config, }); - return buildBundleWithConfig(args, config, output); + let saveAssetsPlugin = + ctx.platforms[args.platform] && + ctx.platforms[args.platform].saveAssetsPlugin + ? require(require.resolve( + ctx.platforms[args.platform].saveAssetsPlugin!, + { + paths: [ctx.root], + }, + )) + : undefined; + + return buildBundleWithConfig(args, config, output, saveAssetsPlugin); } /** @@ -50,6 +65,17 @@ export async function buildBundleWithConfig( args: CommandLineArgs, config: ConfigT, output: typeof outputBundle = outputBundle, + saveAssetsPlugin: ( + assets: ReadonlyArray, + platform: string, + assetsDest: string | undefined, + assetCatalogDest: string | undefined, + addAssetToCopy: ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => void, + ) => void, ) { if (config.resolver.platforms.indexOf(args.platform) === -1) { logger.error( @@ -100,12 +126,22 @@ export async function buildBundleWithConfig( bundleType: 'todo', }); + if (!saveAssetsPlugin) { + saveAssetsPlugin = + args.platform === 'ios' + ? saveAssetsIOS + : args.platform === 'android' + ? saveAssetsAndroid + : saveAssetsDefault; + } + // When we're done saving bundle output and the assets, we're done. return await saveAssets( outputAssets, args.platform, args.assetsDest, args.assetCatalogDest, + saveAssetsPlugin, ); } finally { server.end(); diff --git a/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts b/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts index a8c48b0a1..4052d57dd 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts @@ -6,15 +6,10 @@ * */ -const ALLOWED_SCALES: {[key: string]: number[]} = { - ios: [1, 2, 3], -}; - function filterPlatformAssetScales( - platform: string, + whitelist: ReadonlyArray | undefined, scales: ReadonlyArray, ): ReadonlyArray { - const whitelist: number[] = ALLOWED_SCALES[platform]; if (!whitelist) { return scales; } diff --git a/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts b/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPath.ts similarity index 85% rename from packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts rename to packages/cli-plugin-metro/src/commands/bundle/getAssetDestPath.ts index 6544a8ca3..9db4ca185 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPath.ts @@ -9,7 +9,7 @@ import path from 'path'; import {PackagerAsset} from './assetPathUtils'; -function getAssetDestPathIOS(asset: PackagerAsset, scale: number): string { +function getAssetDestPath(asset: PackagerAsset, scale: number): string { const suffix = scale === 1 ? '' : `@${scale}x`; const fileName = `${asset.name + suffix}.${asset.type}`; return path.join( @@ -21,4 +21,4 @@ function getAssetDestPathIOS(asset: PackagerAsset, scale: number): string { ); } -export default getAssetDestPathIOS; +export default getAssetDestPath; diff --git a/packages/cli-plugin-metro/src/commands/bundle/saveAssets.ts b/packages/cli-plugin-metro/src/commands/bundle/saveAssets.ts index 28ed6330c..3e71ef9d9 100644 --- a/packages/cli-plugin-metro/src/commands/bundle/saveAssets.ts +++ b/packages/cli-plugin-metro/src/commands/bundle/saveAssets.ts @@ -10,15 +10,7 @@ import {logger} from '@react-native-community/cli-tools'; import fs from 'fs'; import type {AssetData} from 'metro'; import path from 'path'; -import { - cleanAssetCatalog, - getImageSet, - isCatalogAsset, - writeImageSet, -} from './assetCatalogIOS'; import filterPlatformAssetScales from './filterPlatformAssetScales'; -import getAssetDestPathAndroid from './getAssetDestPathAndroid'; -import getAssetDestPathIOS from './getAssetDestPathIOS'; interface CopiedFiles { [src: string]: string; @@ -29,6 +21,17 @@ function saveAssets( platform: string, assetsDest: string | undefined, assetCatalogDest: string | undefined, + saveAssetsPlugin: ( + assets: ReadonlyArray, + platform: string, + assetsDest: string | undefined, + assetCatalogDest: string | undefined, + addAssetToCopy: ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => void, + ) => void, ) { if (!assetsDest) { logger.warn('Assets destination folder is not set, skipping...'); @@ -37,15 +40,16 @@ function saveAssets( const filesToCopy: CopiedFiles = Object.create(null); // Map src -> dest - const getAssetDestPath = - platform === 'android' ? getAssetDestPathAndroid : getAssetDestPathIOS; - - const addAssetToCopy = (asset: AssetData) => { + const addAssetToCopy = ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => { const validScales = new Set( - filterPlatformAssetScales(platform, asset.scales), + filterPlatformAssetScales(allowedScales, asset.scales), ); - asset.scales.forEach((scale, idx) => { + asset.scales.forEach((scale: number, idx: number) => { if (!validScales.has(scale)) { return; } @@ -55,36 +59,13 @@ function saveAssets( }); }; - if (platform === 'ios' && assetCatalogDest != null) { - // Use iOS Asset Catalog for images. This will allow Apple app thinning to - // remove unused scales from the optimized bundle. - const catalogDir = path.join(assetCatalogDest, 'RNAssets.xcassets'); - if (!fs.existsSync(catalogDir)) { - logger.error( - `Could not find asset catalog 'RNAssets.xcassets' in ${assetCatalogDest}. Make sure to create it if it does not exist.`, - ); - return; - } - - logger.info('Adding images to asset catalog', catalogDir); - cleanAssetCatalog(catalogDir); - for (const asset of assets) { - if (isCatalogAsset(asset)) { - const imageSet = getImageSet( - catalogDir, - asset, - filterPlatformAssetScales(platform, asset.scales), - ); - writeImageSet(imageSet); - } else { - addAssetToCopy(asset); - } - } - logger.info('Done adding images to asset catalog'); - } else { - assets.forEach(addAssetToCopy); - } - + saveAssetsPlugin( + assets, + platform, + assetsDest, + assetCatalogDest, + addAssetToCopy, + ); return copyAll(filesToCopy); } diff --git a/packages/cli-plugin-metro/src/commands/bundle/saveAssetsAndroid.ts b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsAndroid.ts new file mode 100644 index 000000000..88db8001e --- /dev/null +++ b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsAndroid.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type {AssetData} from 'metro'; +import getAssetDestPathAndroid from './getAssetDestPathAndroid'; + +function saveAssetsAndroid( + assets: ReadonlyArray, + _platform: string, + _assetsDest: string | undefined, + _assetCatalogDest: string | undefined, + addAssetToCopy: ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => void, +) { + assets.forEach((asset) => + addAssetToCopy(asset, undefined, getAssetDestPathAndroid), + ); +} + +export default saveAssetsAndroid; diff --git a/packages/cli-plugin-metro/src/commands/bundle/saveAssetsDefault.ts b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsDefault.ts new file mode 100644 index 000000000..629d3e09c --- /dev/null +++ b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsDefault.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type {AssetData} from 'metro'; +import getAssetDestPath from './getAssetDestPath'; + +function saveAssetsDefault( + assets: ReadonlyArray, + _platform: string, + _assetsDest: string | undefined, + _assetCatalogDest: string | undefined, + addAssetToCopy: ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => void, +) { + assets.forEach((asset) => addAssetToCopy(asset, undefined, getAssetDestPath)); +} + +export default saveAssetsDefault; diff --git a/packages/cli-plugin-metro/src/commands/bundle/saveAssetsIOS.ts b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsIOS.ts new file mode 100644 index 000000000..36ae206d4 --- /dev/null +++ b/packages/cli-plugin-metro/src/commands/bundle/saveAssetsIOS.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {logger} from '@react-native-community/cli-tools'; +import fs from 'fs'; +import type {AssetData} from 'metro'; +import path from 'path'; +import { + cleanAssetCatalog, + getImageSet, + isCatalogAsset, + writeImageSet, +} from './assetCatalogIOS'; +import filterPlatformAssetScales from './filterPlatformAssetScales'; +import getAssetDestPath from './getAssetDestPath'; + +const ALLOWED_SCALES = [1, 2, 3]; + +function saveAssetsIOS( + assets: ReadonlyArray, + _platform: string, + _assetsDest: string | undefined, + assetCatalogDest: string | undefined, + addAssetToCopy: ( + asset: AssetData, + allowedScales: number[] | undefined, + getAssetDestPath: (asset: AssetData, scale: number) => string, + ) => void, +) { + if (assetCatalogDest != null) { + // Use iOS Asset Catalog for images. This will allow Apple app thinning to + // remove unused scales from the optimized bundle. + const catalogDir = path.join(assetCatalogDest, 'RNAssets.xcassets'); + if (!fs.existsSync(catalogDir)) { + logger.error( + `Could not find asset catalog 'RNAssets.xcassets' in ${assetCatalogDest}. Make sure to create it if it does not exist.`, + ); + return; + } + + logger.info('Adding images to asset catalog', catalogDir); + cleanAssetCatalog(catalogDir); + for (const asset of assets) { + if (isCatalogAsset(asset)) { + const imageSet = getImageSet( + catalogDir, + asset, + filterPlatformAssetScales(ALLOWED_SCALES, asset.scales), + ); + writeImageSet(imageSet); + } else { + addAssetToCopy(asset, ALLOWED_SCALES, getAssetDestPath); + } + } + logger.info('Done adding images to asset catalog'); + } else { + assets.forEach((asset) => + addAssetToCopy(asset, ALLOWED_SCALES, getAssetDestPath), + ); + } +} + +export default saveAssetsIOS; diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index b20e4378a..0e7da0dc1 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -66,6 +66,7 @@ interface PlatformConfig< DependencyParams > { npmPackageName?: string; + saveAssetsPlugin?: string; projectConfig: ( projectRoot: string, projectParams: ProjectParams | void, diff --git a/yarn.lock b/yarn.lock index 92ba9718e..1cae82cf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5102,9 +5102,9 @@ dayjs@^1.8.15: integrity sha512-1kbWK0hziklUHkGgiKr7xm59KwAg/K3Tp7H/8X+f58DnNCwY3pKYjOCJpIlVs125FRBukGVZdKZojC073D0IeQ== deasync@^0.1.14: - version "0.1.19" - resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.19.tgz#e7ea89fcc9ad483367e8a48fe78f508ca86286e8" - integrity sha512-oh3MRktfnPlLysCPpBpKZZzb4cUC/p0aA3SyRGp15lN30juJBTo/CiD0d4fR+f1kBtUQoJj1NE9RPNWQ7BQ9Mg== + version "0.1.28" + resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.28.tgz#9b447b79b3f822432f0ab6a8614c0062808b5ad2" + integrity sha512-QqLF6inIDwiATrfROIyQtwOQxjZuek13WRYZ7donU5wJPLoP67MnYxA6QtqdvdBy2mMqv5m3UefBVdJjvevOYg== dependencies: bindings "^1.5.0" node-addon-api "^1.7.1"