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 030e181e41..78b6c3cb44 100644 --- a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap +++ b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap @@ -10,6 +10,7 @@ Object { "reporter": null, "resetCache": false, "resolver": Object { + "allowPnp": true, "assetExts": Array [ "bmp", "gif", @@ -132,6 +133,7 @@ Object { "reporter": null, "resetCache": false, "resolver": Object { + "allowPnp": true, "assetExts": Array [ "bmp", "gif", diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index 8bc7561706..8468e70200 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -67,6 +67,7 @@ export type Middleware = ( ) => mixed; export type OldConfigT = { + allowPnp: boolean, assetRegistryPath: string, cacheStores: Array>>, cacheVersion: string, @@ -102,6 +103,7 @@ export type OldConfigT = { }; type ResolverConfigT = {| + allowPnp: boolean, assetExts: $ReadOnlyArray, blacklistRE: RegExp, extraNodeModules: {[name: string]: string}, diff --git a/packages/metro-config/src/convertConfig.js b/packages/metro-config/src/convertConfig.js index dcaf7056c0..c775478639 100644 --- a/packages/metro-config/src/convertConfig.js +++ b/packages/metro-config/src/convertConfig.js @@ -44,6 +44,7 @@ async function convertOldToNew({ reporter = new TerminalReporter(new Terminal(process.stdout)), }: PublicMetroOptions): Promise { const { + allowPnp, getBlacklistRE, cacheStores, createModuleIdFactory, @@ -97,6 +98,7 @@ async function convertOldToNew({ return { resolver: { + allowPnp, assetExts, platforms, providesModuleNodeModules, diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index 99dbb3a661..005e20c6c5 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -32,6 +32,7 @@ import type {ConfigT} from '../configTypes.flow'; const getDefaultValues = (projectRoot: ?string): ConfigT => ({ resolver: { + allowPnp: true, assetExts, platforms, sourceExts, diff --git a/packages/metro-config/src/oldConfig.js b/packages/metro-config/src/oldConfig.js index 37ff948c55..5b47602acd 100644 --- a/packages/metro-config/src/oldConfig.js +++ b/packages/metro-config/src/oldConfig.js @@ -23,6 +23,7 @@ const {FileStore} = require('metro-cache'); import type {OldConfigT as ConfigT} from './configTypes.flow.js'; const DEFAULT = ({ + allowPnp: true, assetRegistryPath: 'missing-asset-registry-path', enhanceMiddleware: middleware => middleware, extraNodeModules: {}, diff --git a/packages/metro-resolver/src/__tests__/index-test.js b/packages/metro-resolver/src/__tests__/index-test.js index 5a6928a247..2065471c4c 100644 --- a/packages/metro-resolver/src/__tests__/index-test.js +++ b/packages/metro-resolver/src/__tests__/index-test.js @@ -54,6 +54,7 @@ const CONTEXT: ResolutionContext = (() => { ); return { allowHaste: true, + allowPnp: false, doesFileExist: filePath => fileSet.has(filePath), extraNodeModules: null, getPackageMainPath: dirPath => path.join(path.dirname(dirPath), 'main'), diff --git a/packages/metro-resolver/src/makePnpResolver.js b/packages/metro-resolver/src/makePnpResolver.js new file mode 100644 index 0000000000..e76cb10092 --- /dev/null +++ b/packages/metro-resolver/src/makePnpResolver.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +// $FlowFixMe it exists! +const Module = require('module'); + +const path = require('path'); + +import type {ResolutionContext} from './types'; + +const builtinModules = new Set( + // $FlowFixMe "process.binding" exists + Module.builtinModules || Object.keys(process.binding('natives')), +); + +module.exports = (pnp: any) => ( + context: ResolutionContext, + request: string, + platform: string | null, +) => { + // We don't support builtin modules, so we force pnp to resolve those + // modules as regular npm packages by appending a `/` character + if (builtinModules.has(request)) { + request += '/'; + } + + const unqualifiedPath = pnp.resolveToUnqualified( + request, + context.originModulePath, + ); + + const baseExtensions = context.sourceExts.map(extension => `.${extension}`); + let finalExtensions = [...baseExtensions]; + + if (context.preferNativePlatform) { + finalExtensions = [ + ...baseExtensions.map(extension => `.native${extension}`), + ...finalExtensions, + ]; + } + + if (platform) { + // We must keep a const reference to make Flow happy + const p = platform; + + finalExtensions = [ + ...baseExtensions.map(extension => `.${p}${extension}`), + ...finalExtensions, + ]; + } + + try { + return { + type: 'sourceFile', + filePath: pnp.resolveUnqualified(unqualifiedPath, { + extensions: finalExtensions, + }), + }; + } catch (error) { + // Only catch the error if it was caused by the resolution process + if (error.code !== 'QUALIFIED_PATH_RESOLUTION_FAILED') { + throw error; + } + + const dirname = path.dirname(unqualifiedPath); + const basename = path.basename(unqualifiedPath); + + const assetResolutions = context.resolveAsset(dirname, basename, platform); + + if (assetResolutions) { + return { + type: 'assetFiles', + filePaths: assetResolutions.map(name => `${dirname}/${name}`), + }; + } else { + throw error; + } + } +}; diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index 2de5a77646..258cfa5606 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -16,6 +16,7 @@ const InvalidPackageError = require('./InvalidPackageError'); const formatFileCandidates = require('./formatFileCandidates'); const isAbsolutePath = require('absolute-path'); +const makePnpResolver = require('./makePnpResolver'); const path = require('path'); import type { @@ -39,8 +40,16 @@ function resolve( moduleName: string, platform: string | null, ): Resolution { + let resolveRequest = context.resolveRequest; + + if (!resolveRequest && context.allowPnp && process.versions.pnp) { + // $FlowFixMe `pnpapi` is a builtin under PnP environments + const pnp = require('pnpapi'); + resolveRequest = makePnpResolver(pnp); + } + if ( - !context.resolveRequest && + !resolveRequest && (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) ) { return resolveModulePath(context, moduleName, platform); @@ -59,7 +68,7 @@ function resolve( isRelativeImport(realModuleName) || isAbsolutePath(realModuleName); // We disable the direct file loading to let the custom resolvers deal with it - if (!context.resolveRequest && isDirectImport) { + if (!resolveRequest && isDirectImport) { // derive absolute path /.../node_modules/originModuleDir/realModuleName const fromModuleParentIdx = originModulePath.lastIndexOf('node_modules' + path.sep) + 13; @@ -82,13 +91,9 @@ function resolve( } } - if (context.resolveRequest) { + if (resolveRequest) { try { - const resolution = context.resolveRequest( - context, - realModuleName, - platform, - ); + const resolution = resolveRequest(context, realModuleName, platform); if (resolution) { return resolution; } diff --git a/packages/metro-resolver/src/types.js b/packages/metro-resolver/src/types.js index 1084c9e2e8..68e2d1fff2 100644 --- a/packages/metro-resolver/src/types.js +++ b/packages/metro-resolver/src/types.js @@ -107,6 +107,7 @@ export type ModulePathContext = FileOrDirContext & { export type ResolutionContext = ModulePathContext & HasteContext & { + allowPnp: boolean, allowHaste: boolean, extraNodeModules: ?{[string]: string}, originModulePath: string, diff --git a/packages/metro/src/ModuleGraph/node-haste/node-haste.js b/packages/metro/src/ModuleGraph/node-haste/node-haste.js index 4f8033955e..90596e8811 100644 --- a/packages/metro/src/ModuleGraph/node-haste/node-haste.js +++ b/packages/metro/src/ModuleGraph/node-haste/node-haste.js @@ -32,6 +32,7 @@ import type {Extensions, Path} from './node-haste.flow'; import type {CustomResolver} from 'metro-resolver'; type ResolveOptions = {| + allowPnp: boolean, assetExts: Extensions, extraNodeModules: {[id: string]: string}, mainFields: $ReadOnlyArray, @@ -107,6 +108,7 @@ const createModuleMap = ({files, helpers, moduleCache, sourceExts}) => { exports.createResolveFn = function(options: ResolveOptions): ResolveFn { const { + allowPnp, assetExts, extraNodeModules, transformedFiles, @@ -140,6 +142,7 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn { platforms, }); const moduleResolver = new ModuleResolver({ + allowPnp, dirExists: filePath => hasteFS.dirExists(filePath), doesFileExist: filePath => hasteFS.exists(filePath), extraNodeModules, diff --git a/packages/metro/src/node-haste/DependencyGraph.js b/packages/metro/src/node-haste/DependencyGraph.js index 051ce990eb..e581a8cd73 100644 --- a/packages/metro/src/node-haste/DependencyGraph.js +++ b/packages/metro/src/node-haste/DependencyGraph.js @@ -143,6 +143,7 @@ class DependencyGraph extends EventEmitter { _createModuleResolver() { this._moduleResolver = new ModuleResolver({ + allowPnp: this._config.resolver.allowPnp, dirExists: filePath => { try { return fs.lstatSync(filePath).isDirectory(); diff --git a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js index 811eddff3f..3de7ce05e9 100644 --- a/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js +++ b/packages/metro/src/node-haste/DependencyGraph/ModuleResolution.js @@ -53,6 +53,7 @@ export type ModuleishCache = { }; type Options = {| + +allowPnp: boolean, +dirExists: DirExistsFn, +doesFileExist: DoesFileExist, +extraNodeModules: ?Object,