diff --git a/docs/Configuration.md b/docs/Configuration.md index 3ec32a5e1e..f6c593c05d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -243,6 +243,16 @@ Type: `string` What module to use as the canonical "empty" module when one is needed. Defaults to using the one included in `metro-runtime`. You only need to change this if Metro is installed outside of your project. +#### `enableGlobalPackages` + +Type: `boolean`. + +Whether to automatically resolve references first-party packages (e.g. workspaces) in your project. Any `package.json` file with a valid `name` property within `projectRoot` or `watchFolders` (but outside of `node_modules`) counts as a package for this purpose. Defaults to `true`. + +:::note +The default value of this option may change in a future version of Metro. If your project relies on it, it's recommended that you set it explicitly in your config file. +::: + #### `extraNodeModules` Type: `{[string]: string}` diff --git a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap index 5156df2dbb..81082e13a5 100644 --- a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap +++ b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap @@ -49,6 +49,7 @@ Object { "dependencyExtractor": undefined, "disableHierarchicalLookup": false, "emptyModulePath": "metro-runtime/src/modules/empty-module", + "enableGlobalPackages": true, "extraNodeModules": Object {}, "hasteImplModulePath": undefined, "nodeModulesPaths": Array [], @@ -226,6 +227,7 @@ Object { "dependencyExtractor": undefined, "disableHierarchicalLookup": false, "emptyModulePath": "metro-runtime/src/modules/empty-module", + "enableGlobalPackages": true, "extraNodeModules": Object {}, "hasteImplModulePath": undefined, "nodeModulesPaths": Array [], @@ -403,6 +405,7 @@ Object { "dependencyExtractor": undefined, "disableHierarchicalLookup": false, "emptyModulePath": "metro-runtime/src/modules/empty-module", + "enableGlobalPackages": true, "extraNodeModules": Object {}, "hasteImplModulePath": undefined, "nodeModulesPaths": Array [], @@ -580,6 +583,7 @@ Object { "dependencyExtractor": undefined, "disableHierarchicalLookup": false, "emptyModulePath": "metro-runtime/src/modules/empty-module", + "enableGlobalPackages": true, "extraNodeModules": Object {}, "hasteImplModulePath": undefined, "nodeModulesPaths": Array [], diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index 6dafad4ea4..36d9409c01 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -101,6 +101,7 @@ type ResolverConfigT = { disableHierarchicalLookup: boolean, dependencyExtractor: ?string, emptyModulePath: string, + enableGlobalPackages: boolean, unstable_enableSymlinks: boolean, extraNodeModules: {[name: string]: string, ...}, hasteImplModulePath: ?string, diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index 86c9be1f75..fd557645b6 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -44,6 +44,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({ emptyModulePath: require.resolve( 'metro-runtime/src/modules/empty-module.js', ), + enableGlobalPackages: true, extraNodeModules: {}, hasteImplModulePath: undefined, nodeModulesPaths: [], diff --git a/packages/metro-config/types/configTypes.d.ts b/packages/metro-config/types/configTypes.d.ts index 8d6e0d6721..2dc65b821d 100644 --- a/packages/metro-config/types/configTypes.d.ts +++ b/packages/metro-config/types/configTypes.d.ts @@ -97,6 +97,7 @@ export interface ResolverConfigT { disableHierarchicalLookup: boolean; extraNodeModules: {[name: string]: string}; emptyModulePath: string; + enableGlobalPackages: boolean; hasteImplModulePath?: string; nodeModulesPaths: ReadonlyArray; platforms: ReadonlyArray; diff --git a/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js b/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js index ac92a3b3ec..04259926d3 100644 --- a/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js +++ b/packages/metro-file-map/src/cache/__tests__/DiskCacheManager-test.js @@ -19,6 +19,7 @@ const buildParameters: BuildParameters = { computeDependencies: true, computeSha1: true, dependencyExtractor: null, + enableHastePackages: true, enableSymlinks: false, forceNodeFilesystemAPI: true, ignorePattern: /ignored/, diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index d7985afa57..d6b1573e67 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -21,6 +21,7 @@ export type {PerfLoggerFactory, PerfLogger}; export type BuildParameters = $ReadOnly<{ computeDependencies: boolean, computeSha1: boolean, + enableHastePackages: boolean, enableSymlinks: boolean, extensions: $ReadOnlyArray, forceNodeFilesystemAPI: boolean, diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 8854f927c3..d42e6257ca 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -75,6 +75,7 @@ export type { export type InputOptions = $ReadOnly<{ computeDependencies?: ?boolean, computeSha1?: ?boolean, + enableHastePackages?: boolean, enableSymlinks?: ?boolean, extensions: $ReadOnlyArray, forceNodeFilesystemAPI?: ?boolean, @@ -288,6 +289,7 @@ export default class FileMap extends EventEmitter { : options.computeDependencies, computeSha1: options.computeSha1 || false, dependencyExtractor: options.dependencyExtractor ?? null, + enableHastePackages: options.enableHastePackages ?? true, enableSymlinks: options.enableSymlinks || false, extensions: options.extensions, forceNodeFilesystemAPI: !!options.forceNodeFilesystemAPI, @@ -653,7 +655,7 @@ export default class FileMap extends EventEmitter { computeDependencies: this._options.computeDependencies, computeSha1, dependencyExtractor: this._options.dependencyExtractor, - enableHastePackages: true, + enableHastePackages: this._options.enableHastePackages, filePath, hasteImplModulePath: this._options.hasteImplModulePath, readLink: false, diff --git a/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js b/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js index efe0def796..dea70ba72b 100644 --- a/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js +++ b/packages/metro-file-map/src/lib/__tests__/rootRelativeCacheKeys-test.js @@ -18,6 +18,7 @@ const buildParameters: BuildParameters = { computeDependencies: false, computeSha1: false, dependencyExtractor: null, + enableHastePackages: true, enableSymlinks: false, extensions: ['a'], forceNodeFilesystemAPI: false, @@ -82,6 +83,7 @@ it('returns a distinct cache key for any change', () => { // Boolean case 'computeDependencies': case 'computeSha1': + case 'enableHastePackages': case 'enableSymlinks': case 'forceNodeFilesystemAPI': case 'retainAllFiles': diff --git a/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js b/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js index 72ed9d860f..1f0dd5132c 100644 --- a/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js +++ b/packages/metro-file-map/src/lib/rootRelativeCacheKeys.js @@ -54,6 +54,7 @@ export default function rootRelativeCacheKeys( case 'extensions': case 'computeDependencies': case 'computeSha1': + case 'enableHastePackages': case 'enableSymlinks': case 'forceNodeFilesystemAPI': case 'platforms': diff --git a/packages/metro-file-map/types/flow-types.d.ts b/packages/metro-file-map/types/flow-types.d.ts index 2e6114171c..6fec168deb 100644 --- a/packages/metro-file-map/types/flow-types.d.ts +++ b/packages/metro-file-map/types/flow-types.d.ts @@ -20,6 +20,7 @@ export type {PerfLoggerFactory, PerfLogger}; export type BuildParameters = Readonly<{ computeDependencies: boolean; computeSha1: boolean; + enableHastePackages: boolean; enableSymlinks: boolean; extensions: ReadonlyArray; forceNodeFilesystemAPI: boolean; diff --git a/packages/metro/src/DeltaBundler/__tests__/__snapshots__/resolver-test.js.snap b/packages/metro/src/DeltaBundler/__tests__/__snapshots__/resolver-test.js.snap index 8f67da35f0..0d61bd6eb7 100644 --- a/packages/metro/src/DeltaBundler/__tests__/__snapshots__/resolver-test.js.snap +++ b/packages/metro/src/DeltaBundler/__tests__/__snapshots__/resolver-test.js.snap @@ -48,7 +48,30 @@ None of these files exist: 3 | import bar from 'foo';" `; -exports[`linux global packages uses the name in the package.json as the package name 1`] = ` +exports[`linux global packages default config uses the name in the package.json as the package name 1`] = ` +"Unable to resolve module aPackage from /root/index.js: aPackage could not be found within the project. + 1 | import foo from 'bar'; +> 2 | import a from 'aPackage'; + | ^ + 3 | import bar from 'foo';" +`; + +exports[`linux global packages explicitly disabled does not resolve global packages 1`] = ` +"Unable to resolve module aPackage from /root/index.js: aPackage could not be found within the project. +> 1 |" +`; + +exports[`linux global packages explicitly disabled does not resolve global packages 2`] = ` +"Unable to resolve module aPackage/ from /root/index.js: aPackage/ could not be found within the project. +> 1 |" +`; + +exports[`linux global packages explicitly disabled does not resolve global packages 3`] = ` +"Unable to resolve module aPackage/other from /root/index.js: aPackage/other could not be found within the project. +> 1 |" +`; + +exports[`linux global packages explicitly enabled uses the name in the package.json as the package name 1`] = ` "Unable to resolve module aPackage from /root/index.js: aPackage could not be found within the project. 1 | import foo from 'bar'; > 2 | import a from 'aPackage'; @@ -225,7 +248,30 @@ None of these files exist: 3 | import bar from 'foo';" `; -exports[`win32 global packages uses the name in the package.json as the package name 1`] = ` +exports[`win32 global packages default config uses the name in the package.json as the package name 1`] = ` +"Unable to resolve module aPackage from C:\\\\root\\\\index.js: aPackage could not be found within the project. + 1 | import foo from 'bar'; +> 2 | import a from 'aPackage'; + | ^ + 3 | import bar from 'foo';" +`; + +exports[`win32 global packages explicitly disabled does not resolve global packages 1`] = ` +"Unable to resolve module aPackage from C:\\\\root\\\\index.js: aPackage could not be found within the project. +> 1 |" +`; + +exports[`win32 global packages explicitly disabled does not resolve global packages 2`] = ` +"Unable to resolve module aPackage/ from C:\\\\root\\\\index.js: aPackage/ could not be found within the project. +> 1 |" +`; + +exports[`win32 global packages explicitly disabled does not resolve global packages 3`] = ` +"Unable to resolve module aPackage/other from C:\\\\root\\\\index.js: aPackage/other could not be found within the project. +> 1 |" +`; + +exports[`win32 global packages explicitly enabled uses the name in the package.json as the package name 1`] = ` "Unable to resolve module aPackage from C:\\\\root\\\\index.js: aPackage could not be found within the project. 1 | import foo from 'bar'; > 2 | import a from 'aPackage'; diff --git a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js index 33b77eccf0..885593d17c 100644 --- a/packages/metro/src/DeltaBundler/__tests__/resolver-test.js +++ b/packages/metro/src/DeltaBundler/__tests__/resolver-test.js @@ -1633,326 +1633,409 @@ function dep(name: string): TransformResultDependency { }); describe('global packages', () => { - it('treats any folder with a package.json as a global package', async () => { - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js', - }), - 'main.js': '', - 'other.js': '', + describe.each([ + {name: 'default config', config: {}}, + { + name: 'explicitly enabled', + config: { + resolver: { + enableGlobalPackages: true, + }, }, - }); + }, + ])('$name', ({config}) => { + it('treats any folder with a package.json as a global package', async () => { + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': '', + 'other.js': '', + }, + }); - resolver = await createResolver(); - expect(resolver.resolve(p('/root/index.js'), dep('aPackage'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/main.js'), - }); - expect(resolver.resolve(p('/root/index.js'), dep('aPackage/'))).toEqual( - { + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toEqual({ type: 'sourceFile', filePath: p('/root/aPackage/main.js'), - }, - ); - expect( - resolver.resolve(p('/root/index.js'), dep('aPackage/other')), - ).toEqual({type: 'sourceFile', filePath: p('/root/aPackage/other.js')}); - }); + }); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage/')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/main.js'), + }); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage/other')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/other.js'), + }); + }); - it('resolves main package module to index.js by default', async () => { - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({name: 'aPackage'}), + it('resolves main package module to index.js by default', async () => { + setMockFileSystem({ 'index.js': '', - }, + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + }); + + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/index.js'), + }); }); - resolver = await createResolver(); - expect(resolver.resolve(p('/root/index.js'), dep('aPackage'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/index.js'), + it('uses the name in the package.json as the package name', async () => { + setMockFileSystem({ + 'index.js': mockFileImport("import a from 'aPackage';"), + aPackage: { + 'package.json': JSON.stringify({}), + 'index.js': '', + }, + randomFolderName: { + 'package.json': JSON.stringify({name: 'bPackage'}), + 'index.js': '', + }, + }); + + resolver = await createResolver(config); + expect(() => + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toThrowErrorMatchingSnapshot(); + expect( + resolver.resolve(p('/root/index.js'), dep('bPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/randomFolderName/index.js'), + }); }); - }); - it('uses the name in the package.json as the package name', async () => { - setMockFileSystem({ - 'index.js': mockFileImport("import a from 'aPackage';"), - aPackage: { - 'package.json': JSON.stringify({}), - 'index.js': '', - }, - randomFolderName: { - 'package.json': JSON.stringify({name: 'bPackage'}), + it('uses main field from the package.json', async () => { + setMockFileSystem({ 'index.js': '', - }, - }); + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage', main: 'lib/'}), + lib: { + 'index.js': '', + }, + }, + }); - resolver = await createResolver(); - expect(() => - resolver.resolve(p('/root/index.js'), dep('aPackage')), - ).toThrowErrorMatchingSnapshot(); - expect(resolver.resolve(p('/root/index.js'), dep('bPackage'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/randomFolderName/index.js'), + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/lib/index.js'), + }); }); - }); - it('uses main field from the package.json', async () => { - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({name: 'aPackage', main: 'lib/'}), - lib: { + it('supports package names with dots', async () => { + setMockFileSystem({ + 'index.js': '', + 'leftpad.js': { + 'package.json': JSON.stringify({name: 'leftpad.js'}), 'index.js': '', }, - }, - }); + 'x.y.z': { + 'package.json': JSON.stringify({name: 'x.y.z'}), + 'index.js': '', + }, + }); - resolver = await createResolver(); - expect(resolver.resolve(p('/root/index.js'), dep('aPackage'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/lib/index.js'), + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('leftpad.js')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/leftpad.js/index.js'), + }); + expect(resolver.resolve(p('/root/index.js'), dep('x.y.z'))).toEqual({ + type: 'sourceFile', + filePath: p('/root/x.y.z/index.js'), + }); }); - }); - it('supports package names with dots', async () => { - setMockFileSystem({ - 'index.js': '', - 'leftpad.js': { - 'package.json': JSON.stringify({name: 'leftpad.js'}), - 'index.js': '', - }, - 'x.y.z': { - 'package.json': JSON.stringify({name: 'x.y.z'}), + it('allows relative requires against packages', async () => { + setMockFileSystem({ 'index.js': '', - }, - }); - - resolver = await createResolver(); - expect( - resolver.resolve(p('/root/index.js'), dep('leftpad.js')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/leftpad.js/index.js'), - }); - expect(resolver.resolve(p('/root/index.js'), dep('x.y.z'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/x.y.z/index.js'), - }); - }); + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage', main: 'main'}), + 'main.js': '', + }, + anotherPackage: { + 'package.json': JSON.stringify({name: 'bPackage', main: 'main'}), + 'main.js': '', + }, + }); - it('allows relative requires against packages', async () => { - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({name: 'aPackage', main: 'main'}), - 'main.js': '', - }, - anotherPackage: { - 'package.json': JSON.stringify({name: 'bPackage', main: 'main'}), - 'main.js': '', - }, + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('./aPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/main.js'), + }); + expect( + resolver.resolve( + p('/root/aPackage/index.js'), + dep('../anotherPackage'), + ), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/anotherPackage/main.js'), + }); }); - resolver = await createResolver(); - expect( - resolver.resolve(p('/root/index.js'), dep('./aPackage')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/main.js'), - }); - expect( - resolver.resolve( - p('/root/aPackage/index.js'), - dep('../anotherPackage'), - ), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/anotherPackage/main.js'), - }); - }); + it('fatals on multiple packages with the same name', async () => { + // $FlowFixMe[cannot-write] + console.warn = jest.fn(); + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage'}), + }, + anotherPackage: { + 'package.json': JSON.stringify({name: 'aPackage', main: 'main'}), + 'main.js': '', + }, + }); - it('fatals on multiple packages with the same name', async () => { - // $FlowFixMe[cannot-write] - console.warn = jest.fn(); - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({name: 'aPackage'}), - }, - anotherPackage: { - 'package.json': JSON.stringify({name: 'aPackage', main: 'main'}), - 'main.js': '', - }, + await expect(createResolver(config)).rejects.toThrow( + 'Duplicated files or mocks. Please check the console for more info', + ); + expect(console.error).toHaveBeenCalledWith( + [ + 'metro-file-map: Haste module naming collision: aPackage', + ' The following files share their name; please adjust your hasteImpl:', + ` * ${joinPath('', 'aPackage', 'package.json')}`, + ` * ${joinPath( + '', + 'anotherPackage', + 'package.json', + )}`, + '', + ].join('\n'), + ); }); - await expect(createResolver()).rejects.toThrow( - 'Duplicated files or mocks. Please check the console for more info', - ); - expect(console.error).toHaveBeenCalledWith( - [ - 'metro-file-map: Haste module naming collision: aPackage', - ' The following files share their name; please adjust your hasteImpl:', - ` * ${joinPath('', 'aPackage', 'package.json')}`, - ` * ${joinPath('', 'anotherPackage', 'package.json')}`, - '', - ].join('\n'), - ); - }); - - it('does not support multiple global packages for different platforms', async () => { - setMockFileSystem({ - 'index.js': '', - 'aPackage.android.js': { - 'package.json': JSON.stringify({name: 'aPackage'}), - 'index.js': '', - }, - 'aPackage.ios.js': { - 'package.json': JSON.stringify({name: 'aPackage'}), + it('does not support multiple global packages for different platforms', async () => { + setMockFileSystem({ 'index.js': '', - }, - }); + 'aPackage.android.js': { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + 'aPackage.ios.js': { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + }); - await expect(createResolver()).rejects.toThrow( - 'Duplicated files or mocks. Please check the console for more info', - ); - expect(console.error).toHaveBeenCalledWith( - [ - 'metro-file-map: Haste module naming collision: aPackage', - ' The following files share their name; please adjust your hasteImpl:', - ` * ${joinPath( - '', - 'aPackage.android.js', - 'package.json', - )}`, - ` * ${joinPath('', 'aPackage.ios.js', 'package.json')}`, - '', - ].join('\n'), - ); - }); + await expect(createResolver()).rejects.toThrow( + 'Duplicated files or mocks. Please check the console for more info', + ); + expect(console.error).toHaveBeenCalledWith( + [ + 'metro-file-map: Haste module naming collision: aPackage', + ' The following files share their name; please adjust your hasteImpl:', + ` * ${joinPath( + '', + 'aPackage.android.js', + 'package.json', + )}`, + ` * ${joinPath( + '', + 'aPackage.ios.js', + 'package.json', + )}`, + '', + ].join('\n'), + ); + }); - it('resolves global packages before node_modules packages', async () => { - setMockFileSystem({ - 'index.js': '', - node_modules: { + it('resolves global packages before node_modules packages', async () => { + setMockFileSystem({ + 'index.js': '', + node_modules: { + foo: { + 'package.json': JSON.stringify({name: 'foo'}), + 'index.js': '', + }, + }, foo: { 'package.json': JSON.stringify({name: 'foo'}), 'index.js': '', }, - }, - foo: { - 'package.json': JSON.stringify({name: 'foo'}), - 'index.js': '', - }, - }); + }); - resolver = await createResolver(); - expect(resolver.resolve(p('/root/index.js'), dep('foo'))).toEqual({ - type: 'sourceFile', - filePath: p('/root/foo/index.js'), + resolver = await createResolver(config); + expect(resolver.resolve(p('/root/index.js'), dep('foo'))).toEqual({ + type: 'sourceFile', + filePath: p('/root/foo/index.js'), + }); }); - }); - it('allows to require global package sub-dirs', async () => { - // $FlowFixMe[cannot-write] - console.warn = jest.fn(); - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({name: 'aPackage'}), - lib: {foo: {'bar.js': ''}}, - }, - }); + it('allows to require global package sub-dirs', async () => { + // $FlowFixMe[cannot-write] + console.warn = jest.fn(); + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({name: 'aPackage'}), + lib: {foo: {'bar.js': ''}}, + }, + }); - resolver = await createResolver(); - expect( - resolver.resolve(p('/root/index.js'), dep('aPackage/lib/foo/bar')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/lib/foo/bar.js'), + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage/lib/foo/bar')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/lib/foo/bar.js'), + }); }); - }); - ['browser', 'react-native'].forEach(browserField => { - describe(`${browserField} field in global packages`, () => { - it('supports simple field', async () => { - setMockFileSystem({ - 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({ - name: 'aPackage', - [(browserField: string)]: 'client.js', - }), - 'client.js': '', - }, + ['browser', 'react-native'].forEach(browserField => { + describe(`${browserField} field in global packages`, () => { + it('supports simple field', async () => { + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({ + name: 'aPackage', + [(browserField: string)]: 'client.js', + }), + 'client.js': '', + }, + }); + + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/client.js'), + }); }); - resolver = await createResolver(); - expect( - resolver.resolve(p('/root/index.js'), dep('aPackage')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/client.js'), + it('resolves mappings without extensions', async () => { + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + [(browserField: string)]: {'./main': './client'}, + }), + 'client.js': '', + 'main.js': '', + }, + }); + + resolver = await createResolver(config); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/client.js'), + }); + expect( + resolver.resolve(p('/root/index.js'), dep('aPackage/main')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/client.js'), + }); }); }); + }); - it('resolves mappings without extensions', async () => { - setMockFileSystem({ + it('works with custom main fields', async () => { + setMockFileSystem({ + aPackage: { + 'package.json': JSON.stringify({ + name: 'aPackage', + 'custom-field': {'left-pad': './left-pad-custom'}, + browser: {'left-pad': './left-pad-browser'}, + }), 'index.js': '', - aPackage: { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js', - [(browserField: string)]: {'./main': './client'}, - }), - 'client.js': '', - 'main.js': '', - }, - }); + './left-pad-custom.js': '', + }, + }); - resolver = await createResolver(); - expect( - resolver.resolve(p('/root/index.js'), dep('aPackage')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/client.js'), - }); - expect( - resolver.resolve(p('/root/index.js'), dep('aPackage/main')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/client.js'), - }); + resolver = await createResolver( + mergeConfig(defaultConfig, config, { + resolver: {resolverMainFields: ['custom-field', 'browser']}, + }), + ); + + expect( + resolver.resolve(p('/root/aPackage/index.js'), dep('left-pad')), + ).toEqual({ + type: 'sourceFile', + filePath: p('/root/aPackage/left-pad-custom.js'), }); }); }); - it('works with custom main fields', async () => { - setMockFileSystem({ - aPackage: { - 'package.json': JSON.stringify({ - name: 'aPackage', - 'custom-field': {'left-pad': './left-pad-custom'}, - browser: {'left-pad': './left-pad-browser'}, - }), - 'index.js': '', - './left-pad-custom.js': '', + describe('explicitly disabled', () => { + const config = { + resolver: { + enableGlobalPackages: false, }, - }); + }; - resolver = await createResolver({ - resolver: {resolverMainFields: ['custom-field', 'browser']}, + test('does not resolve global packages', async () => { + setMockFileSystem({ + 'index.js': '', + aPackage: { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + }), + 'main.js': '', + 'other.js': '', + }, + }); + + resolver = await createResolver(config); + expect(() => + resolver.resolve(p('/root/index.js'), dep('aPackage')), + ).toThrowErrorMatchingSnapshot(); + expect(() => + resolver.resolve(p('/root/index.js'), dep('aPackage/')), + ).toThrowErrorMatchingSnapshot(); + expect(() => + resolver.resolve(p('/root/index.js'), dep('aPackage/other')), + ).toThrowErrorMatchingSnapshot(); }); - expect( - resolver.resolve(p('/root/aPackage/index.js'), dep('left-pad')), - ).toEqual({ - type: 'sourceFile', - filePath: p('/root/aPackage/left-pad-custom.js'), + test('does not report duplicates', async () => { + setMockFileSystem({ + 'index.js': '', + 'aPackage.android.js': { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + 'aPackage.ios.js': { + 'package.json': JSON.stringify({name: 'aPackage'}), + 'index.js': '', + }, + }); + + await expect(createResolver(config)).resolves; + expect(console.error).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/metro/src/node-haste/DependencyGraph/createFileMap.js b/packages/metro/src/node-haste/DependencyGraph/createFileMap.js index 92daa163f3..2296273ceb 100644 --- a/packages/metro/src/node-haste/DependencyGraph/createFileMap.js +++ b/packages/metro/src/node-haste/DependencyGraph/createFileMap.js @@ -82,6 +82,7 @@ function createFileMap( computeDependencies, computeSha1: true, dependencyExtractor: config.resolver.dependencyExtractor, + enableHastePackages: config?.resolver.enableGlobalPackages, enableSymlinks: config.resolver.unstable_enableSymlinks, extensions: Array.from( new Set([