diff --git a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js index f7c90ad0e5..616eb76cb6 100644 --- a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js @@ -3040,6 +3040,7 @@ function dep(name: string): TransformResultDependency { 'package.json', ), content: new Set(), + haste: null, }, }); }); @@ -3085,6 +3086,7 @@ function dep(name: string): TransformResultDependency { 'node_modules/pkg/main.js', ), content: new Set(), + haste: null, }, }); }); @@ -3107,6 +3109,7 @@ function dep(name: string): TransformResultDependency { ), // Retargeting the link changes what every path through it means content: canonical('link'), + haste: null, }, }); }); @@ -3132,6 +3135,7 @@ function dep(name: string): TransformResultDependency { 'package.json', ), content: new Set(), + haste: null, }); }); @@ -3171,6 +3175,7 @@ function dep(name: string): TransformResultDependency { 'package.json', ), content: new Set(), + haste: null, }, }); }); @@ -3183,6 +3188,109 @@ function dep(name: string): TransformResultDependency { expect(second).toBe(first); expect(second.unstable_observations?.existence.size).toBeGreaterThan(0); }); + describe('Haste names', () => { + const withHaste: InputConfigT = { + resolver: { + hasteImplModulePath: path.join( + __dirname, + '../__fixtures__/hasteImpl.js', + ), + unstable_incrementalResolution: true, + }, + }; + + test('are not observed when no file can be given one', async () => { + setMockFileSystem({ + 'index.js': '', + node_modules: {pkg: {'index.js': ''}}, + }); + resolver = await createResolver(incremental, 'ios'); + // Bare specifiers go to the Haste map, which stays empty + expect( + resolver.resolve(p('/root/index.js'), dep('pkg')) + .unstable_observations?.haste, + ).toBe(null); + }); + + test('records a name that is found', async () => { + setMockFileSystem({ + 'index.js': '', + 'hasteModule.js': '@providesModule hasteModule', + }); + resolver = await createResolver(withHaste, 'ios'); + const resolution = resolver.resolve( + p('/root/index.js'), + dep('hasteModule'), + ); + expect(resolution.filePath).toBe(p('/root/hasteModule.js')); + expect(resolution.unstable_observations?.haste).toEqual( + new Set(['hasteModule']), + ); + }); + + test('records a name that is not found, since it may appear later', async () => { + setMockFileSystem({ + 'index.js': '', + node_modules: {pkg: {'index.js': ''}}, + }); + resolver = await createResolver(withHaste, 'ios'); + const resolution = resolver.resolve(p('/root/index.js'), dep('pkg')); + expect(resolution.filePath).toBe( + p('/root/node_modules/pkg/index.js'), + ); + expect(resolution.unstable_observations?.haste).toEqual( + new Set(['pkg']), + ); + }); + + test('records only the package name of a deep import', async () => { + setMockFileSystem({ + 'index.js': '', + node_modules: {pkg: {lib: {'thing.js': ''}}}, + }); + resolver = await createResolver(withHaste, 'ios'); + expect( + resolver.resolve(p('/root/index.js'), dep('pkg/lib/thing')) + .unstable_observations?.haste, + ).toEqual(new Set(['pkg'])); + }); + + test('records nothing for a relative import, which does not consult Haste', async () => { + setMockFileSystem({'index.js': '', 'a.js': ''}); + resolver = await createResolver(withHaste, 'ios'); + expect( + resolver.resolve(p('/root/index.js'), dep('./a')) + .unstable_observations?.haste, + ).toBe(null); + }); + + test('records a global package name', async () => { + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + }); + resolver = await createResolver( + { + resolver: { + enableGlobalPackages: true, + unstable_incrementalResolution: true, + }, + }, + 'ios', + ); + const resolution = resolver.resolve( + p('/root/index.js'), + dep('aPackage'), + ); + expect(resolution.filePath).toBe(p('/root/aPackage/index.js')); + expect(resolution.unstable_observations?.haste).toEqual( + new Set(['aPackage']), + ); + }); + }); }); describe('schemeResolvers', () => { diff --git a/packages/metro/src/DeltaBundler/types.js b/packages/metro/src/DeltaBundler/types.js index 3441a30e94..76cbc39062 100644 --- a/packages/metro/src/DeltaBundler/types.js +++ b/packages/metro/src/DeltaBundler/types.js @@ -163,10 +163,13 @@ export type AllowOptionalDependencies = boolean | AllowOptionalDependenciesWithOptions; // Canonical paths whose addition or removal (`existence`) or modification -// (`content`) can change a resolution's result. +// (`content`) can change a resolution's result, and every Haste name it looked +// up, found or not (`haste`). `haste` is null if there were none, which is +// always so when no file can have a Haste name. export type ResolutionObservations = Readonly<{ existence: Set, content: Set, + haste: ?Set, }>; export type BundlerResolution = Readonly<{ diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index 007c3711e7..515600ae53 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -48,6 +48,8 @@ type Options = Readonly<{ ) => ReturnType, getHasteModulePath: (name: string, platform: ?string) => ?string, getHastePackagePath: (name: string, platform: ?string) => ?string, + // Whether any file can have a Haste name. If not, lookups are not observed. + isHasteEnabled: boolean, mainFields: ReadonlyArray, getPackage: (packageJsonPath: string) => ?PackageJson, getPackageForModule: ( @@ -76,8 +78,8 @@ type Options = Readonly<{ }>; // Same shape for every record, to keep lookups monomorphic. -function createObservations(): ResolutionObservations { - return {existence: new Set(), content: new Set()}; +function createObservations(): {...ResolutionObservations} { + return {existence: new Set(), content: new Set(), haste: null}; } export class ModuleResolver { @@ -210,7 +212,7 @@ export class ModuleResolver { allowHaste: boolean, platform: string | null, resolverOptions: ResolverInputOptions, - observations: ?ResolutionObservations, + observations: ?{...ResolutionObservations}, ): Resolution { const { assetExts, @@ -232,6 +234,18 @@ export class ModuleResolver { unstable_enablePackageExports, unstable_incrementalResolution, } = this._options; + // Most resolutions never consult Haste, so the set is created on demand. + const observeHasteName = + observations != null && this._options.isHasteEnabled + ? (name: string) => { + let names = observations.haste; + if (names == null) { + names = new Set(); + observations.haste = names; + } + names.add(name); + } + : null; return Resolver.resolve( createDefaultContext( @@ -265,10 +279,15 @@ export class ModuleResolver { ? resolveAsset : (dirPath, assetName, extension) => resolveAsset(dirPath, assetName, extension, observations), - resolveHasteModule: (name: string) => - this._options.getHasteModulePath(name, platform), - resolveHastePackage: (name: string) => - this._options.getHastePackagePath(name, platform), + // Recorded by name, found or not: lookups fall back across platforms + resolveHasteModule: (name: string) => { + observeHasteName?.(name); + return this._options.getHasteModulePath(name, platform); + }, + resolveHastePackage: (name: string) => { + observeHasteName?.(name); + return this._options.getHastePackagePath(name, platform); + }, resolveRequest, schemeResolvers, sourceExts, @@ -319,6 +338,7 @@ export class ModuleResolver { return emptyModule; } // The empty module's resolution is cached, so merge in what it observed. + // It is resolved without Haste, so has no names to merge. const emptyObservations = emptyModule.unstable_observations; if (emptyObservations != null) { for (const canonicalPath of emptyObservations.existence) { diff --git a/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js b/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js index d2f9f4342a..2327f3a947 100644 --- a/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js +++ b/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js @@ -64,6 +64,10 @@ export default function createModuleResolver({ emptyModulePath: config.resolver.emptyModulePath, extraNodeModules: config.resolver.extraNodeModules, fileSystemLookup, + // Mirrors which files `HastePlugin` gives a name + isHasteEnabled: + config.resolver.enableGlobalPackages || + config.resolver.hasteImplModulePath != null, getHasteModulePath: (name, platform) => hasteMap.getModule(name, platform, true), getHastePackagePath: (name, platform) =>