From 6d65a328e6f5d7c7a5c27cad144b169f6eaad379 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Mon, 20 Mar 2023 06:53:01 -0700 Subject: [PATCH] Fix selection of assets when assetExts include a multipart extension Summary: Resolves https://github.com/facebook/metro/issues/937. Previously, the matching of `resolver.assetExts` in the resolve and transform steps was based on `path.extname()`, which would only consider the final segment in a path split by a `'.'`. This meant the inability to configure and match multipart extensions in `assetExts`, such as `'.asset.json'`. The new implementation iterates over all possible file extensions. Validated against: - Internal test app. - Sample repo with relevant Metro config. Changelog: **[Fix]** `resolver.assetExts` will now match asset files for extension values that include a dot (`.`) Reviewed By: motiz88 Differential Revision: D43737453 fbshipit-source-id: 150c3aaf3bd58c24cd1670cc1a46e8531c24acf7 --- .../src/__tests__/assets-test.js | 86 +++++++++++++++++++ .../metro-resolver/src/utils/isAssetFile.js | 14 ++- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/metro-resolver/src/__tests__/assets-test.js diff --git a/packages/metro-resolver/src/__tests__/assets-test.js b/packages/metro-resolver/src/__tests__/assets-test.js new file mode 100644 index 0000000000..91a0515129 --- /dev/null +++ b/packages/metro-resolver/src/__tests__/assets-test.js @@ -0,0 +1,86 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +'use strict'; + +import path from 'path'; +import Resolver from '../index'; +import {createResolutionContext} from './utils'; + +describe('asset resolutions', () => { + const baseContext = { + ...createResolutionContext({ + '/root/project/index.js': '', + '/root/project/src/data.json': '', + '/root/project/assets/example.asset.json': '', + '/root/project/assets/icon.png': '', + '/root/project/assets/icon@2x.png': '', + }), + originModulePath: '/root/project/index.js', + }; + const assetResolutions = ['1', '2']; + const resolveAsset = ( + dirPath: string, + assetName: string, + extension: string, + ) => { + const basePath = dirPath + path.sep + assetName; + let assets = [ + basePath + extension, + ...assetResolutions.map( + resolution => basePath + '@' + resolution + 'x' + extension, + ), + ]; + + assets = assets.filter(candidate => baseContext.doesFileExist(candidate)); + + return assets.length ? assets : null; + }; + + test('should resolve a path as an asset when matched against `assetExts`', () => { + const context = { + ...baseContext, + assetExts: new Set(['png']), + resolveAsset, + }; + + expect(Resolver.resolve(context, './assets/icon.png', null)).toEqual({ + type: 'assetFiles', + filePaths: [ + '/root/project/assets/icon.png', + '/root/project/assets/icon@2x.png', + ], + }); + }); + + test('should resolve a path as an asset when matched against `assetExts` (overlap with `sourceExts`)', () => { + const context = { + ...baseContext, + assetExts: new Set(['asset.json']), + resolveAsset, + sourceExts: ['js', 'json'], + }; + + // Source file matching `sourceExts` + expect(Resolver.resolve(context, './src/data.json', null)).toEqual({ + type: 'sourceFile', + filePath: '/root/project/src/data.json', + }); + + // Asset file matching more specific asset ext + expect( + Resolver.resolve(context, './assets/example.asset.json', null), + ).toEqual({ + type: 'assetFiles', + filePaths: ['/root/project/assets/example.asset.json'], + }); + }); +}); diff --git a/packages/metro-resolver/src/utils/isAssetFile.js b/packages/metro-resolver/src/utils/isAssetFile.js index 725d724c1e..1380c63eb6 100644 --- a/packages/metro-resolver/src/utils/isAssetFile.js +++ b/packages/metro-resolver/src/utils/isAssetFile.js @@ -19,5 +19,17 @@ export default function isAssetFile( filePath: string, assetExts: $ReadOnlySet, ): boolean { - return assetExts.has(path.extname(filePath).slice(1)); + const baseName = path.basename(filePath); + + for (let i = baseName.length - 1; i >= 0; i--) { + if (baseName[i] === '.') { + const ext = baseName.slice(i + 1); + + if (assetExts.has(ext)) { + return true; + } + } + } + + return false; }