Skip to content

Commit 29c77bf

Browse files
huntiefacebook-github-bot
authored andcommitted
Fix getPackageForModule implementation to avoid edge cases in resolver
Summary: Fixes an awkward bug where, while attempting package resolution against candidate `node_modules` paths, paths which don't exist are short-circuited to the parent package if present. Because Package Exports resolution has the side-effect of logging a warning for an invalid package path (`PackagePathNotExportedError`), repeat `resolvePackage` calls under this scenario (to apparent subpaths including `/node_modules/`) would log incorrect warnings to the terminal. More specifically, this is because `context.getPackageForModule` uses a different resolution strategy to the top-level `resolve` function (originating from the `redirectModulePath` design). This produces a mismatch where we may eagerly locate a parent package. Independently, we should address this disparity in future. Does not affect [`"browser"` spec](https://github.com/defunctzombie/package-browser-field-spec) / `mainFields` resolution, since the `redirectModulePath` approach bypasses the above `node_modules` lookup strategy in the simple case. Changelog: **[Experimental]** Fix bug where Package Exports warnings may have been logged for nested `node_modules` path candidates Reviewed By: motiz88 Differential Revision: D44149246 fbshipit-source-id: 43df6885e712a93f9d07e8fb8e2e36132a766fc8
1 parent c893f31 commit 29c77bf

6 files changed

Lines changed: 35 additions & 80 deletions

File tree

packages/metro-resolver/src/PackageExportsResolve.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
} from './types';
1919

2020
import path from 'path';
21-
import InvalidModuleSpecifierError from './errors/InvalidModuleSpecifierError';
2221
import InvalidPackageConfigurationError from './errors/InvalidPackageConfigurationError';
2322
import PackagePathNotExportedError from './errors/PackagePathNotExportedError';
2423
import resolveAsset from './resolveAsset';
@@ -91,16 +90,6 @@ export function resolvePackageTargetFromExports(
9190
);
9291
}
9392

94-
if (patternMatch != null && findInvalidPathSegment(patternMatch) != null) {
95-
throw new InvalidModuleSpecifierError({
96-
importSpecifier: modulePath,
97-
reason:
98-
`The target for "${subpath}" defined in "exports" is "${target}", ` +
99-
'however this expands to an invalid subpath because the pattern ' +
100-
`match "${patternMatch}" is invalid.`,
101-
});
102-
}
103-
10493
const filePath = path.join(
10594
packagePath,
10695
patternMatch != null ? target.replace('*', patternMatch) : target,

packages/metro-resolver/src/__tests__/package-exports-test.js

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ describe('with package exports resolution enabled', () => {
254254
'/root/node_modules/test-pkg/lib/foo.ios.js': '',
255255
'/root/node_modules/test-pkg/private/bar.js': '',
256256
'/root/node_modules/test-pkg/node_modules/baz/index.js': '',
257+
'/root/node_modules/test-pkg/node_modules/baz/package.json': '',
257258
'/root/node_modules/test-pkg/metadata.json': '',
258259
'/root/node_modules/test-pkg/metadata.min.json': '',
259260
}),
@@ -347,6 +348,31 @@ describe('with package exports resolution enabled', () => {
347348
});
348349
});
349350

351+
test('should resolve subpath when package is located in nested node_modules path', () => {
352+
const logWarning = jest.fn();
353+
const context = {
354+
...baseContext,
355+
...createPackageAccessors({
356+
'/root/node_modules/test-pkg/package.json': {
357+
exports: './index-exports.js',
358+
},
359+
'/root/node_modules/test-pkg/node_modules/baz/package.json': {
360+
exports: './index.js',
361+
},
362+
}),
363+
originModulePath: '/root/node_modules/test-pkg/private/bar.js',
364+
unstable_logWarning: logWarning,
365+
};
366+
367+
expect(Resolver.resolve(context, 'baz', null)).toEqual({
368+
type: 'sourceFile',
369+
filePath: '/root/node_modules/test-pkg/node_modules/baz/index.js',
370+
});
371+
// If a warning was logged, we have incorrectly tried to resolve "exports"
372+
// against the parent package.json.
373+
expect(logWarning).not.toHaveBeenCalled();
374+
});
375+
350376
test('should expand array of strings as subpath mapping (root shorthand)', () => {
351377
const logWarning = jest.fn();
352378
const context = {
@@ -509,34 +535,6 @@ describe('with package exports resolution enabled', () => {
509535
`);
510536
});
511537

512-
test('[nonstrict] should fall back and log warning for an invalid pattern match substitution', () => {
513-
const logWarning = jest.fn();
514-
const context = {
515-
...baseContext,
516-
unstable_logWarning: logWarning,
517-
};
518-
519-
// TODO(T145206395): Improve this error trace
520-
expect(() =>
521-
Resolver.resolve(
522-
context,
523-
'test-pkg/features/node_modules/foo/index.js',
524-
null,
525-
),
526-
).toThrowErrorMatchingInlineSnapshot(`
527-
"Module does not exist in the Haste module map or in these directories:
528-
/root/src/node_modules
529-
/root/node_modules
530-
/node_modules
531-
"
532-
`);
533-
expect(logWarning).toHaveBeenCalledTimes(1);
534-
expect(logWarning.mock.calls[0][0]).toMatchInlineSnapshot(`
535-
"Invalid import specifier /root/node_modules/test-pkg/features/node_modules/foo/index.js.
536-
Reason: The target for \\"./features/node_modules/foo/index.js\\" defined in \\"exports\\" is \\"./src/features/*.js\\", however this expands to an invalid subpath because the pattern match \\"node_modules/foo/index\\" is invalid. Falling back to file-based resolution."
537-
`);
538-
});
539-
540538
describe('package encapsulation', () => {
541539
test('[nonstrict] should fall back to "browser" spec resolution and log inaccessible import warning', () => {
542540
const logWarning = jest.fn();

packages/metro-resolver/src/__tests__/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export function createPackageAccessors(
102102
let dir = path.join(parsedPath.dir, parsedPath.name);
103103

104104
do {
105+
if (path.basename(dir) === 'node_modules') {
106+
return null;
107+
}
105108
const candidate = path.join(dir, 'package.json');
106109
const packageJson = getPackage(candidate);
107110

packages/metro-resolver/src/errors/InvalidModuleSpecifierError.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

packages/metro-resolver/src/resolve.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type {
2222
import path from 'path';
2323
import FailedToResolveNameError from './errors/FailedToResolveNameError';
2424
import FailedToResolvePathError from './errors/FailedToResolvePathError';
25-
import InvalidModuleSpecifierError from './errors/InvalidModuleSpecifierError';
2625
import InvalidPackageConfigurationError from './errors/InvalidPackageConfigurationError';
2726
import InvalidPackageError from './errors/InvalidPackageError';
2827
import PackagePathNotExportedError from './errors/PackagePathNotExportedError';
@@ -280,10 +279,7 @@ function resolvePackage(
280279
' Falling back to file-based resolution. Consider updating the ' +
281280
'call site or asking the package maintainer(s) to expose this API.',
282281
);
283-
} else if (
284-
e instanceof InvalidModuleSpecifierError ||
285-
e instanceof InvalidPackageConfigurationError
286-
) {
282+
} else if (e instanceof InvalidPackageConfigurationError) {
287283
context.unstable_logWarning(
288284
e.message + ' Falling back to file-based resolution.',
289285
);

packages/metro/src/node-haste/DependencyGraph.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ class DependencyGraph extends EventEmitter {
154154
const root = parsedPath.root;
155155
let dir = parsedPath.dir;
156156
do {
157+
// If we've hit a node_modules directory, the closest package was not
158+
// found (`filePath` was likely nonexistent).
159+
if (path.basename(dir) === 'node_modules') {
160+
return null;
161+
}
157162
const candidate = path.join(dir, 'package.json');
158163
if (this._fileSystem.exists(candidate)) {
159164
return candidate;

0 commit comments

Comments
 (0)