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
177 changes: 177 additions & 0 deletions packages/metro/src/DeltaBundler/__tests__/resolver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2853,6 +2853,183 @@ function dep(name: string): TransformResultDependency {
});
});

describe('unstable_incrementalResolution', () => {
const incremental: InputConfigT = {
resolver: {unstable_incrementalResolution: true},
};
// Observed paths are canonical: relative to the root, with system
// separators.
const canonical = (...posixPaths: Array<string>) =>
new Set(posixPaths.map(posixPath => joinPath(...posixPath.split('/'))));

test('resolutions carry no observations by default', async () => {
setMockFileSystem({'index.js': '', 'a.js': ''});
resolver = await createResolver();
expect(
resolver.resolve(p('/root/index.js'), dep('./a')),
).not.toHaveProperty('unstable_observations');
});

test('records every candidate probed for a relative import', async () => {
setMockFileSystem({'index.js': '', 'a.js': ''});
resolver = await createResolver(incremental, 'ios');
expect(resolver.resolve(p('/root/index.js'), dep('./a'))).toEqual({
type: 'sourceFile',
filePath: p('/root/a.js'),
unstable_observations: {
// A more specific candidate appearing changes the result
existence: canonical(
'a',
'a.ios.js',
'a.native.js',
'a.js',
'package.json',
),
content: new Set(),
},
});
});

test('records each level probed for a package', async () => {
setMockFileSystem({
deep: {dir: {'index.js': ''}},
node_modules: {
pkg: {
'package.json': JSON.stringify({name: 'pkg', main: 'main.js'}),
'main.js': '',
},
},
});
resolver = await createResolver(incremental, 'ios');
expect(
resolver.resolve(p('/root/deep/dir/index.js'), dep('pkg')),
).toEqual({
type: 'sourceFile',
filePath: p('/root/node_modules/pkg/main.js'),
unstable_observations: {
existence: canonical(
// The closest package to the origin
'deep/dir/index.js',
'deep/dir/package.json',
'deep/package.json',
'package.json',
// Each node_modules on the way up. A missing one is one entry,
// however many packages are looked up beneath it.
'deep/dir/node_modules',
'deep/node_modules',
'node_modules',
// A file is preferred to a package of the same name
'node_modules/pkg.ios.js',
'node_modules/pkg.native.js',
'node_modules/pkg.js',
'node_modules/pkg.ios.json',
'node_modules/pkg.native.json',
'node_modules/pkg.json',
'node_modules/pkg',
'node_modules/pkg/package.json',
'node_modules/pkg/main.js',
),
content: new Set(),
},
});
});

test('records a traversed symlink as content, and lookups by real path', async () => {
setMockFileSystem({'index.js': '', real: {'x.js': ''}});
fs.symlinkSync(p('/root/real'), p('/root/link'));
resolver = await createResolver(incremental, 'ios');
expect(resolver.resolve(p('/root/index.js'), dep('./link/x'))).toEqual({
type: 'sourceFile',
filePath: p('/root/real/x.js'),
unstable_observations: {
existence: canonical(
'real/x',
'real/x.ios.js',
'real/x.native.js',
'real/x.js',
'real/package.json',
'package.json',
),
// Retargeting the link changes what every path through it means
content: canonical('link'),
},
});
});

test('records every asset variant probed', async () => {
setMockFileSystem({
'index.js': '',
'asset.png': '',
'asset@2x.png': '',
});
resolver = await createResolver(incremental, 'ios');
expect(
resolver.resolve(p('/root/index.js'), dep('./asset.png'))
.unstable_observations,
).toEqual({
existence: canonical(
'asset.png',
'asset@1x.png',
'asset@1.5x.png',
'asset@2x.png',
'asset@3x.png',
'asset@4x.png',
'package.json',
),
content: new Set(),
});
});

test('a resolution landing on the empty module includes what resolving the empty module observed', async () => {
setMockFileSystem({
'empty.js': '',
withBrowser: {
'package.json': JSON.stringify({
name: 'with-browser',
browser: {'./gone.js': false},
}),
'index.js': '',
'gone.js': '',
},
});
resolver = await createResolver(
{
resolver: {
emptyModulePath: p('/root/empty.js'),
unstable_incrementalResolution: true,
},
},
'ios',
);
expect(
resolver.resolve(p('/root/withBrowser/index.js'), dep('./gone')),
).toEqual({
type: 'sourceFile',
filePath: p('/root/empty.js'),
unstable_observations: {
existence: canonical(
// What led to the empty module
'withBrowser/gone',
'withBrowser/package.json',
// What resolving the empty module observed
'empty.js',
'package.json',
),
content: new Set(),
},
});
});

test('origins sharing a cached resolution share its observations', async () => {
setMockFileSystem({'index.js': '', 'other.js': '', 'a.js': ''});
resolver = await createResolver(incremental, 'ios');
const first = resolver.resolve(p('/root/index.js'), dep('./a'));
const second = resolver.resolve(p('/root/other.js'), dep('./a'));
expect(second).toBe(first);
expect(second.unstable_observations?.existence.size).toBeGreaterThan(0);
});
});

describe('schemeResolvers', () => {
test('config schemeResolvers are applied to scheme-prefixed specifiers', async () => {
setMockFileSystem({'index.js': '', 'a.js': ''});
Expand Down
12 changes: 12 additions & 0 deletions packages/metro/src/DeltaBundler/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,21 @@ 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.
export type ResolutionObservations = Readonly<{
existence: Set<string>,
content: Set<string>,
}>;

export type BundlerResolution = Readonly<{
type: 'sourceFile',
filePath: string,
// What this resolution observed of the file system, if
// `resolver.unstable_incrementalResolution` is enabled. It is shared by
// every caller given this resolution, so must not be mutated.
unstable_observations?: ResolutionObservations,
}>;

export type Options<T = MixedOutput> = Readonly<{
Expand Down
7 changes: 5 additions & 2 deletions packages/metro/src/node-haste/DependencyGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import type {
BundlerResolution,
ResolutionObservations,
TransformResultDependency,
} from '../DeltaBundler/types';
import type {ResolverInputOptions} from '../shared/types';
Expand Down Expand Up @@ -130,8 +131,8 @@ export default class DependencyGraph extends EventEmitter {
);
this._resolutionCache = new Map();
this.#packageCache = new PackageCache({
getClosestPackage: absoluteModulePath =>
this._getClosestPackage(absoluteModulePath),
getClosestPackage: (absoluteModulePath, observations) =>
this._getClosestPackage(absoluteModulePath, observations),
});
this._createModuleResolver();
});
Expand Down Expand Up @@ -175,6 +176,7 @@ export default class DependencyGraph extends EventEmitter {

_getClosestPackage(
absoluteModulePath: string,
observations?: ?ResolutionObservations,
): ?{packageJsonPath: string, packageRelativePath: string} {
const result = this._fileSystem.hierarchicalLookup(
absoluteModulePath,
Expand All @@ -183,6 +185,7 @@ export default class DependencyGraph extends EventEmitter {
breakOnSegment: 'node_modules',
subpathType: 'f',
},
observations,
);
return result
? {
Expand Down
Loading
Loading