diff --git a/packages/metro-resolver/src/PackageExportsResolve.js b/packages/metro-resolver/src/PackageExportsResolve.js index cf6883980f..84a371defb 100644 --- a/packages/metro-resolver/src/PackageExportsResolve.js +++ b/packages/metro-resolver/src/PackageExportsResolve.js @@ -9,7 +9,12 @@ * @oncall react_native */ -import type {ExportMap, FileResolution, ResolutionContext} from './types'; +import type { + ExportMap, + ExportsField, + FileResolution, + ResolutionContext, +} from './types'; import path from 'path'; import InvalidModuleSpecifierError from './errors/InvalidModuleSpecifierError'; @@ -46,7 +51,7 @@ export function resolvePackageTargetFromExports( * to a package-relative subpath for comparison. */ modulePath: string, - exportsField: ExportMap | string, + exportsField: ExportsField, platform: string | null, ): FileResolution { const raiseConfigError = (reason: string) => { @@ -142,13 +147,23 @@ function getExportsSubpath(packagePath: string, modulePath: string): string { * See https://nodejs.org/docs/latest-v19.x/api/packages.html#exports-sugar. */ function normalizeExportsField( - exportsField: ExportMap | string, + exportsField: ExportsField, raiseConfigError: (reason: string) => void, ): ExportMap { if (typeof exportsField === 'string') { return {'.': exportsField}; } + if (Array.isArray(exportsField)) { + return exportsField.reduce( + (result, subpath) => ({ + ...result, + [subpath]: subpath, + }), + {}, + ); + } + const firstLevelKeys = Object.keys(exportsField); const subpathKeys = firstLevelKeys.filter(subpathOrCondition => subpathOrCondition.startsWith('.'), diff --git a/packages/metro-resolver/src/__tests__/package-exports-test.js b/packages/metro-resolver/src/__tests__/package-exports-test.js index fa18a9ca91..276b93ff0a 100644 --- a/packages/metro-resolver/src/__tests__/package-exports-test.js +++ b/packages/metro-resolver/src/__tests__/package-exports-test.js @@ -333,6 +333,29 @@ describe('with package exports resolution enabled', () => { }); }); + test('should expand array of strings as subpath mapping (root shorthand)', () => { + const logWarning = jest.fn(); + const context = { + ...baseContext, + ...createPackageAccessors({ + '/root/node_modules/test-pkg/package.json': JSON.stringify({ + exports: ['./index.js', './foo.js'], + }), + }), + unstable_logWarning: logWarning, + }; + + expect(Resolver.resolve(context, 'test-pkg/index.js', null)).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/index.js', + }); + expect(Resolver.resolve(context, 'test-pkg/foo.js', null)).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/test-pkg/foo.js', + }); + expect(logWarning).not.toHaveBeenCalled(); + }); + describe('should resolve "exports" target directly', () => { test('without expanding `sourceExts`', () => { expect(Resolver.resolve(baseContext, 'test-pkg/foo.js', null)).toEqual({ diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index b5ad29ab0a..0881f3505b 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -54,10 +54,12 @@ export type ExportMap = $ReadOnly<{ [subpathOrCondition: string]: ExportMap | string | null, }>; +export type ExportsField = string | $ReadOnlyArray | ExportMap; + export type PackageJson = $ReadOnly<{ name?: string, main?: string, - exports?: string | ExportMap, + exports?: ExportsField, ... }>;