Skip to content

Commit 6e6f36f

Browse files
huntiefacebook-github-bot
authored andcommitted
Implement resolveAsset handling
Summary: Add support for resolving assets (determined by `resolver.isAssetFile`) from Package Exports. - As with source files, assets in `"exports"` do not automatically append `sourceExts` or platform-specific exts. **However**, we allow expansion of *source resolutions* (i.e. `2x.png` etc) from an `"exports"` target. This is Metro/React Native-specific functionality which otherwise has no equivalent in the Package Exports spec. - Falls back to legacy file resolution logic when the asset is missing or `unstable_enablePackageExports` is `false`. Changelog: **[Experimental]** Add asset handling for package exports via `context.resolveAsset` Reviewed By: motiz88 Differential Revision: D43193253 fbshipit-source-id: 41ced8d6a91fc2d32051348635852c5df1f5d213
1 parent aa20356 commit 6e6f36f

File tree

5 files changed

+126
-34
lines changed

5 files changed

+126
-34
lines changed

packages/metro-resolver/src/PackageExportsResolve.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
* @oncall react_native
1010
*/
1111

12-
import type {ExportMap, ResolutionContext, SourceFileResolution} from './types';
12+
import type {ExportMap, FileResolution, ResolutionContext} from './types';
1313

1414
import path from 'path';
1515
import InvalidPackageConfigurationError from './errors/InvalidPackageConfigurationError';
16+
import resolveAsset from './resolveAsset';
1617
import toPosixPath from './utils/toPosixPath';
1718

1819
/**
@@ -40,7 +41,7 @@ export function resolvePackageTargetFromExports(
4041
modulePath: string,
4142
exportsField: ExportMap | string,
4243
platform: string | null,
43-
): SourceFileResolution | null {
44+
): FileResolution | null {
4445
const raiseConfigError = (reason: string) => {
4546
throw new InvalidPackageConfigurationError({
4647
reason,
@@ -64,6 +65,10 @@ export function resolvePackageTargetFromExports(
6465
if (match != null) {
6566
const filePath = path.join(packagePath, match);
6667

68+
if (context.isAssetFile(filePath)) {
69+
return resolveAsset(context, filePath);
70+
}
71+
6772
if (context.doesFileExist(filePath)) {
6873
return {type: 'sourceFile', filePath};
6974
}

packages/metro-resolver/src/__tests__/package-exports-test.js

+60
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @oncall react_native
1010
*/
1111

12+
import path from 'path';
1213
import Resolver from '../index';
1314
import {createPackageAccessors, createResolutionContext} from './utils';
1415

@@ -588,4 +589,63 @@ describe('with package exports resolution enabled', () => {
588589
});
589590
});
590591
});
592+
593+
describe('asset resolutions', () => {
594+
const assetResolutions = ['1', '1.5', '2', '3', '4'];
595+
const isAssetFile = (filePath: string) => filePath.endsWith('.png');
596+
597+
const baseContext = {
598+
...createResolutionContext({
599+
'/root/src/main.js': '',
600+
'/root/node_modules/test-pkg/package.json': JSON.stringify({
601+
main: './index.js',
602+
exports: {
603+
'./icons/metro.png': './assets/icons/metro.png',
604+
},
605+
}),
606+
'/root/node_modules/test-pkg/assets/icons/metro.png': '',
607+
'/root/node_modules/test-pkg/assets/icons/metro@2x.png': '',
608+
'/root/node_modules/test-pkg/assets/icons/metro@3x.png': '',
609+
}),
610+
isAssetFile,
611+
originModulePath: '/root/src/main.js',
612+
unstable_enablePackageExports: true,
613+
};
614+
615+
test('should resolve assets using "exports" field and calling `resolveAsset`', () => {
616+
const resolveAsset = jest.fn(
617+
(dirPath: string, basename: string, extension: string) => {
618+
const basePath = dirPath + path.sep + basename;
619+
const assets = [
620+
basePath + extension,
621+
...assetResolutions.map(
622+
resolution => basePath + '@' + resolution + 'x' + extension,
623+
),
624+
].filter(candidate => baseContext.doesFileExist(candidate));
625+
626+
return assets.length ? assets : null;
627+
},
628+
);
629+
const context = {
630+
...baseContext,
631+
resolveAsset,
632+
};
633+
634+
expect(
635+
Resolver.resolve(context, 'test-pkg/icons/metro.png', null),
636+
).toEqual({
637+
type: 'assetFiles',
638+
filePaths: [
639+
'/root/node_modules/test-pkg/assets/icons/metro.png',
640+
'/root/node_modules/test-pkg/assets/icons/metro@2x.png',
641+
'/root/node_modules/test-pkg/assets/icons/metro@3x.png',
642+
],
643+
});
644+
expect(resolveAsset).toHaveBeenLastCalledWith(
645+
'/root/node_modules/test-pkg/assets/icons',
646+
'metro',
647+
'.png',
648+
);
649+
});
650+
});
591651
});

packages/metro-resolver/src/resolve.js

+12-29
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import InvalidPackageError from './errors/InvalidPackageError';
2828
import formatFileCandidates from './errors/formatFileCandidates';
2929
import {getPackageEntryPoint} from './PackageResolve';
3030
import {resolvePackageTargetFromExports} from './PackageExportsResolve';
31+
import resolveAsset from './resolveAsset';
3132
import invariant from 'invariant';
3233

3334
function resolve(
@@ -355,27 +356,19 @@ function resolveFile(
355356
fileName: string,
356357
platform: string | null,
357358
): Result<Resolution, FileCandidates> {
358-
const {isAssetFile, resolveAsset} = context;
359-
if (isAssetFile(fileName)) {
360-
const extension = path.extname(fileName);
361-
const basename = path.basename(fileName, extension);
362-
if (!/@\d+(?:\.\d+)?x$/.test(basename)) {
363-
try {
364-
const assets = resolveAsset(dirPath, basename, extension);
365-
if (assets != null) {
366-
return mapResult(resolvedAs(assets), filePaths => ({
367-
type: 'assetFiles',
368-
filePaths,
369-
}));
370-
}
371-
} catch (err) {
372-
if (err.code === 'ENOENT') {
373-
return failedFor({type: 'asset', name: fileName});
374-
}
375-
}
359+
if (context.isAssetFile(fileName)) {
360+
const assetResolutions = resolveAsset(
361+
context,
362+
path.join(dirPath, fileName),
363+
);
364+
365+
if (assetResolutions == null) {
366+
return failedFor({type: 'asset', name: fileName});
376367
}
377-
return failedFor({type: 'asset', name: fileName});
368+
369+
return resolvedAs(assetResolutions);
378370
}
371+
379372
const candidateExts: Array<string> = [];
380373
const filePathPrefix = path.join(dirPath, fileName);
381374
const sfContext = {...context, candidateExts, filePathPrefix};
@@ -513,14 +506,4 @@ function failedFor<TResolution, TCandidates>(
513506
return {type: 'failed', candidates};
514507
}
515508

516-
function mapResult<TResolution, TNewResolution, TCandidates>(
517-
result: Result<TResolution, TCandidates>,
518-
mapper: TResolution => TNewResolution,
519-
): Result<TNewResolution, TCandidates> {
520-
if (result.type === 'failed') {
521-
return result;
522-
}
523-
return {type: 'resolved', resolution: mapper(result.resolution)};
524-
}
525-
526509
module.exports = resolve;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import type {AssetResolution, ResolutionContext} from './types';
13+
14+
import path from 'path';
15+
16+
/**
17+
* Resolve a file path as an asset. Returns the set of files found after
18+
* expanding asset resolutions (e.g. `icon@2x.png`). Users may override this
19+
* behaviour via `context.resolveAsset`.
20+
*/
21+
export default function resolveAsset(
22+
context: ResolutionContext,
23+
filePath: string,
24+
): AssetResolution | null {
25+
const dirPath = path.dirname(filePath);
26+
const extension = path.extname(filePath);
27+
const basename = path.basename(filePath, extension);
28+
29+
try {
30+
if (!/@\d+(?:\.\d+)?x$/.test(basename)) {
31+
const assets = context.resolveAsset(dirPath, basename, extension);
32+
if (assets != null) {
33+
return {
34+
type: 'assetFiles',
35+
filePaths: assets,
36+
};
37+
}
38+
}
39+
} catch (e) {}
40+
41+
return null;
42+
}

packages/metro-resolver/src/types.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ export type SourceFileResolution = $ReadOnly<{
2222
filePath: string,
2323
}>;
2424
export type AssetFileResolution = $ReadOnlyArray<string>;
25-
export type FileResolution =
26-
| SourceFileResolution
27-
| {+type: 'assetFiles', +filePaths: AssetFileResolution};
25+
export type AssetResolution = $ReadOnly<{
26+
type: 'assetFiles',
27+
filePaths: AssetFileResolution,
28+
}>;
29+
export type FileResolution = AssetResolution | SourceFileResolution;
2830

2931
export type FileAndDirCandidates = {
3032
+dir: FileCandidates,

0 commit comments

Comments
 (0)