diff --git a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js index 7c3086a13b..b80819a005 100644 --- a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js @@ -2933,6 +2933,7 @@ function dep(name: string): TransformResultDependency { '.metro-package-scope', ), content: new Set(), + haste: null, }, }); }); @@ -2979,6 +2980,7 @@ function dep(name: string): TransformResultDependency { 'node_modules/pkg/main.js', ), content: new Set(), + haste: null, }, }); }); @@ -3002,6 +3004,7 @@ function dep(name: string): TransformResultDependency { ), // Retargeting the link changes what every path through it means content: canonical('link'), + haste: null, }, }); }); @@ -3027,6 +3030,7 @@ function dep(name: string): TransformResultDependency { 'package.json', ), content: new Set(), + haste: null, }); }); @@ -3067,6 +3071,7 @@ function dep(name: string): TransformResultDependency { 'package.json', ), content: new Set(), + haste: null, }, }); }); @@ -3079,6 +3084,110 @@ 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'); + // A bare specifier is looked up in the Haste map, which is empty and + // will stay that way + 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 d114a6dc21..a55b875ef3 100644 --- a/packages/metro/src/DeltaBundler/types.js +++ b/packages/metro/src/DeltaBundler/types.js @@ -151,12 +151,19 @@ export type AllowOptionalDependenciesWithOptions = { export type AllowOptionalDependencies = boolean | AllowOptionalDependenciesWithOptions; -// What a resolution observed of the file system: canonical paths whose -// addition or removal can change its result, and those whose modification -// can too. Satisfies `metro-file-map`'s `Observations`, which populates it. +// What a resolution observed. `existence` and `content` are canonical paths +// whose addition or removal can change its result, and those whose +// modification can too. They satisfy `metro-file-map`'s `Observations`, which +// populates them. +// +// `haste` is every Haste module or package name the resolution looked up, +// whether or not it was found, since one appearing later changes the result. +// It is null if there were none, which is always the case when no file can be +// given a Haste name - nothing is paid to observe what cannot change. 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 305cf3dd86..4688b30000 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -48,6 +48,9 @@ type Options = Readonly<{ ) => ReturnType, getHasteModulePath: (name: string, platform: ?string) => ?string, getHastePackagePath: (name: string, platform: ?string) => ?string, + // Whether any file can be given a Haste name. If not, Haste lookups always + // miss, and are not worth observing. + isHasteEnabled: boolean, mainFields: ReadonlyArray, getPackage: (packageJsonPath: string) => ?PackageJson, getPackageForModule: ( @@ -77,8 +80,8 @@ type Options = Readonly<{ // Every record is created here, with the same properties in the same order, // so that reading them stays monomorphic on the lookup hot path. -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 { @@ -149,9 +152,21 @@ export class ModuleResolver { // The capabilities given to the resolver are bound to it, so that the // resolution context keeps its shape and a custom resolver records what // it looks up without having to know about it. - const observations: ?ResolutionObservations = unstable_incrementalResolution + const observations = unstable_incrementalResolution ? createObservations() : null; + // 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; try { const result = Resolver.resolve( @@ -187,10 +202,17 @@ 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), + // A name is recorded whether or not it is found, and for any + // platform, since `HastePlugin` falls back from the platform to + // `native` and then to the generic module of that name. + 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, @@ -303,7 +325,7 @@ export class ModuleResolver { } // The empty module is resolved once and cached, so a resolution that // lands on it depends on what that resolution observed as well as on - // what led here. + // what led here. It is resolved without Haste, so observes no names. 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 85efa30ab4..82cd63e3bd 100644 --- a/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js +++ b/packages/metro/src/node-haste/DependencyGraph/createModuleResolver.js @@ -65,6 +65,11 @@ export default function createModuleResolver({ emptyModulePath: config.resolver.emptyModulePath, extraNodeModules: config.resolver.extraNodeModules, fileSystemLookup, + // As `HastePlugin` decides which files to give a name: packages only if + // global packages are enabled, and other files only with a `hasteImpl`. + isHasteEnabled: + config.resolver.enableGlobalPackages || + config.resolver.hasteImplModulePath != null, getHasteModulePath: (name, platform) => hasteMap.getModule(name, platform, true), getHastePackagePath: (name, platform) =>