Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/metro/src/DeltaBundler/__tests__/resolver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2933,6 +2933,7 @@ function dep(name: string): TransformResultDependency {
'.metro-package-scope',
),
content: new Set(),
haste: null,
},
});
});
Expand Down Expand Up @@ -2979,6 +2980,7 @@ function dep(name: string): TransformResultDependency {
'node_modules/pkg/main.js',
),
content: new Set(),
haste: null,
},
});
});
Expand All @@ -3002,6 +3004,7 @@ function dep(name: string): TransformResultDependency {
),
// Retargeting the link changes what every path through it means
content: canonical('link'),
haste: null,
},
});
});
Expand All @@ -3027,6 +3030,7 @@ function dep(name: string): TransformResultDependency {
'package.json',
),
content: new Set(),
haste: null,
});
});

Expand Down Expand Up @@ -3067,6 +3071,7 @@ function dep(name: string): TransformResultDependency {
'package.json',
),
content: new Set(),
haste: null,
},
});
});
Expand All @@ -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', () => {
Expand Down
13 changes: 10 additions & 3 deletions packages/metro/src/DeltaBundler/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
content: Set<string>,
haste: ?Set<string>,
}>;

export type BundlerResolution = Readonly<{
Expand Down
38 changes: 30 additions & 8 deletions packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type Options = Readonly<{
) => ReturnType<FileSystemLookup>,
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<string>,
getPackage: (packageJsonPath: string) => ?PackageJson,
getPackageForModule: (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string>();
observations.haste = names;
}
names.add(name);
}
: null;

try {
const result = Resolver.resolve(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Loading