diff --git a/packages/metro/src/Assets.js b/packages/metro/src/Assets.js index b2759ad59b..b81e3f67d9 100644 --- a/packages/metro/src/Assets.js +++ b/packages/metro/src/Assets.js @@ -299,29 +299,36 @@ export async function getAsset( } // NOTE: If fileExistsInFileMap is not provided, we fall back to pathBelongsToRoots for backward compatibility, as getAsset is part of the public API. - if (fileExistsInFileMap != null) { - if (!fileExistsInFileMap(absolutePath)) { - throw new Error( - `'${relativePath}' could not be found, because it is not within the projectRoot or watchFolders, or it is blocked via the resolver.blockList config`, - ); - } - } else { - if (!pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders])) { - throw new Error( - `'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`, - ); - } + if ( + fileExistsInFileMap == null && + !pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders]) + ) { + throw new Error( + `'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`, + ); } const record = await getAbsoluteAssetRecord(absolutePath, platform ?? null); for (let i = 0; i < record.scales.length; i++) { if (record.scales[i] >= assetData.resolution) { + if ( + fileExistsInFileMap != null && + !fileExistsInFileMap(record.files[i]) + ) { + continue; + } return fs.promises.readFile(record.files[i]); } } - return fs.promises.readFile(record.files[record.files.length - 1]); + const lastFile = record.files[record.files.length - 1]; + if (fileExistsInFileMap != null && !fileExistsInFileMap(lastFile)) { + throw new Error( + `'${relativePath}' could not be found, because it is not within the projectRoot or watchFolders, or it is blocked via the resolver.blockList config`, + ); + } + return fs.promises.readFile(lastFile); } function pathBelongsToRoots( diff --git a/packages/metro/src/__tests__/Assets-test.js b/packages/metro/src/__tests__/Assets-test.js index 804438ba60..f010240f24 100644 --- a/packages/metro/src/__tests__/Assets-test.js +++ b/packages/metro/src/__tests__/Assets-test.js @@ -166,6 +166,81 @@ describe('getAsset', () => { getAssetStr('imgs/b.png', '/root', [], null, ['png'], () => false), ).rejects.toBeInstanceOf(Error); }); + + test('should serve scale variant when only scale variants exist and fileExistsInFileMap is provided', async () => { + writeImages({ + 'b@2x.png': 'b2 image', + 'b@3x.png': 'b3 image', + }); + + expect( + await getAssetStr( + 'imgs/b@2x.png', + '/root', + [], + null, + ['png'], + () => true, + ), + ).toBe('b2 image'); + }); + + test('should throw when fileExistsInFileMap rejects the resolved scale variant', async () => { + writeImages({ + 'b@2x.png': 'b2 image', + 'b@3x.png': 'b3 image', + }); + + await expect( + getAssetStr('imgs/b@2x.png', '/root', [], null, ['png'], () => false), + ).rejects.toBeInstanceOf(Error); + }); + + test('should check fileExistsInFileMap against the resolved file, not the base path', async () => { + writeImages({ + 'b@2x.png': 'b2 image', + 'b@3x.png': 'b3 image', + }); + + const checkedPaths = []; + const result = await getAssetStr( + 'imgs/b@2x.png', + '/root', + [], + null, + ['png'], + filePath => { + checkedPaths.push(filePath); + return true; + }, + ); + + expect(result).toBe('b2 image'); + expect(checkedPaths).toEqual(['/root/imgs/b@2x.png']); + }); + + test('should check fileExistsInFileMap for the fallback (highest scale) file', async () => { + writeImages({ + 'b@1x.png': 'b1 image', + 'b@2x.png': 'b2 image', + }); + + const checkedPaths = []; + const result = await getAssetStr( + 'imgs/b@3x.png', + '/root', + [], + null, + ['png'], + filePath => { + checkedPaths.push(filePath); + return true; + }, + ); + + expect(result).toBe('b2 image'); + expect(checkedPaths).toEqual(['/root/imgs/b@2x.png']); + }); }); describe('getAssetData', () => {