diff --git a/packages/metro-resolver/src/PackageExportsResolve.js b/packages/metro-resolver/src/PackageExportsResolve.js index 8e454f1f84..04b544d638 100644 --- a/packages/metro-resolver/src/PackageExportsResolve.js +++ b/packages/metro-resolver/src/PackageExportsResolve.js @@ -9,10 +9,11 @@ * @oncall react_native */ -import type {ExportMap, ResolutionContext, SourceFileResolution} from './types'; +import type {ExportMap, FileResolution, ResolutionContext} from './types'; import path from 'path'; import InvalidPackageConfigurationError from './errors/InvalidPackageConfigurationError'; +import resolveAsset from './resolveAsset'; import toPosixPath from './utils/toPosixPath'; /** @@ -40,7 +41,7 @@ export function resolvePackageTargetFromExports( modulePath: string, exportsField: ExportMap | string, platform: string | null, -): SourceFileResolution | null { +): FileResolution | null { const raiseConfigError = (reason: string) => { throw new InvalidPackageConfigurationError({ reason, @@ -64,6 +65,10 @@ export function resolvePackageTargetFromExports( if (match != null) { const filePath = path.join(packagePath, match); + if (context.isAssetFile(filePath)) { + return resolveAsset(context, filePath); + } + if (context.doesFileExist(filePath)) { return {type: 'sourceFile', filePath}; } diff --git a/packages/metro-resolver/src/__tests__/package-exports-test.js b/packages/metro-resolver/src/__tests__/package-exports-test.js index 88a1a0555d..1542a91b4c 100644 --- a/packages/metro-resolver/src/__tests__/package-exports-test.js +++ b/packages/metro-resolver/src/__tests__/package-exports-test.js @@ -9,6 +9,7 @@ * @oncall react_native */ +import path from 'path'; import Resolver from '../index'; import {createPackageAccessors, createResolutionContext} from './utils'; @@ -588,4 +589,63 @@ describe('with package exports resolution enabled', () => { }); }); }); + + describe('asset resolutions', () => { + const assetResolutions = ['1', '1.5', '2', '3', '4']; + const isAssetFile = (filePath: string) => filePath.endsWith('.png'); + + const baseContext = { + ...createResolutionContext({ + '/root/src/main.js': '', + '/root/node_modules/test-pkg/package.json': JSON.stringify({ + main: './index.js', + exports: { + './icons/metro.png': './assets/icons/metro.png', + }, + }), + '/root/node_modules/test-pkg/assets/icons/metro.png': '', + '/root/node_modules/test-pkg/assets/icons/metro@2x.png': '', + '/root/node_modules/test-pkg/assets/icons/metro@3x.png': '', + }), + isAssetFile, + originModulePath: '/root/src/main.js', + unstable_enablePackageExports: true, + }; + + test('should resolve assets using "exports" field and calling `resolveAsset`', () => { + const resolveAsset = jest.fn( + (dirPath: string, basename: string, extension: string) => { + const basePath = dirPath + path.sep + basename; + const assets = [ + basePath + extension, + ...assetResolutions.map( + resolution => basePath + '@' + resolution + 'x' + extension, + ), + ].filter(candidate => baseContext.doesFileExist(candidate)); + + return assets.length ? assets : null; + }, + ); + const context = { + ...baseContext, + resolveAsset, + }; + + expect( + Resolver.resolve(context, 'test-pkg/icons/metro.png', null), + ).toEqual({ + type: 'assetFiles', + filePaths: [ + '/root/node_modules/test-pkg/assets/icons/metro.png', + '/root/node_modules/test-pkg/assets/icons/metro@2x.png', + '/root/node_modules/test-pkg/assets/icons/metro@3x.png', + ], + }); + expect(resolveAsset).toHaveBeenLastCalledWith( + '/root/node_modules/test-pkg/assets/icons', + 'metro', + '.png', + ); + }); + }); }); diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index be01fbab8a..d2d15bafea 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -28,6 +28,7 @@ import InvalidPackageError from './errors/InvalidPackageError'; import formatFileCandidates from './errors/formatFileCandidates'; import {getPackageEntryPoint} from './PackageResolve'; import {resolvePackageTargetFromExports} from './PackageExportsResolve'; +import resolveAsset from './resolveAsset'; import invariant from 'invariant'; function resolve( @@ -355,27 +356,19 @@ function resolveFile( fileName: string, platform: string | null, ): Result { - const {isAssetFile, resolveAsset} = context; - if (isAssetFile(fileName)) { - const extension = path.extname(fileName); - const basename = path.basename(fileName, extension); - if (!/@\d+(?:\.\d+)?x$/.test(basename)) { - try { - const assets = resolveAsset(dirPath, basename, extension); - if (assets != null) { - return mapResult(resolvedAs(assets), filePaths => ({ - type: 'assetFiles', - filePaths, - })); - } - } catch (err) { - if (err.code === 'ENOENT') { - return failedFor({type: 'asset', name: fileName}); - } - } + if (context.isAssetFile(fileName)) { + const assetResolutions = resolveAsset( + context, + path.join(dirPath, fileName), + ); + + if (assetResolutions == null) { + return failedFor({type: 'asset', name: fileName}); } - return failedFor({type: 'asset', name: fileName}); + + return resolvedAs(assetResolutions); } + const candidateExts: Array = []; const filePathPrefix = path.join(dirPath, fileName); const sfContext = {...context, candidateExts, filePathPrefix}; @@ -513,14 +506,4 @@ function failedFor( return {type: 'failed', candidates}; } -function mapResult( - result: Result, - mapper: TResolution => TNewResolution, -): Result { - if (result.type === 'failed') { - return result; - } - return {type: 'resolved', resolution: mapper(result.resolution)}; -} - module.exports = resolve; diff --git a/packages/metro-resolver/src/resolveAsset.js b/packages/metro-resolver/src/resolveAsset.js new file mode 100644 index 0000000000..65308ed8c3 --- /dev/null +++ b/packages/metro-resolver/src/resolveAsset.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + * @oncall react_native + */ + +import type {AssetResolution, ResolutionContext} from './types'; + +import path from 'path'; + +/** + * Resolve a file path as an asset. Returns the set of files found after + * expanding asset resolutions (e.g. `icon@2x.png`). Users may override this + * behaviour via `context.resolveAsset`. + */ +export default function resolveAsset( + context: ResolutionContext, + filePath: string, +): AssetResolution | null { + const dirPath = path.dirname(filePath); + const extension = path.extname(filePath); + const basename = path.basename(filePath, extension); + + try { + if (!/@\d+(?:\.\d+)?x$/.test(basename)) { + const assets = context.resolveAsset(dirPath, basename, extension); + if (assets != null) { + return { + type: 'assetFiles', + filePaths: assets, + }; + } + } + } catch (e) {} + + return null; +} diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index 9753ad003b..7b13a756d0 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -22,9 +22,11 @@ export type SourceFileResolution = $ReadOnly<{ filePath: string, }>; export type AssetFileResolution = $ReadOnlyArray; -export type FileResolution = - | SourceFileResolution - | {+type: 'assetFiles', +filePaths: AssetFileResolution}; +export type AssetResolution = $ReadOnly<{ + type: 'assetFiles', + filePaths: AssetFileResolution, +}>; +export type FileResolution = AssetResolution | SourceFileResolution; export type FileAndDirCandidates = { +dir: FileCandidates,