diff options
author | Jan Krems <jan.krems@gmail.com> | 2019-08-19 20:59:25 -0700 |
---|---|---|
committer | Jan Krems <jan.krems@gmail.com> | 2019-10-24 15:14:38 -0700 |
commit | 71bcd05232b4fc21db20e5acf019f97780050568 (patch) | |
tree | ff58c1a5c27e3e28755395e1b07b0394583d96e1 /lib/internal | |
parent | d53dd8b0a00d3e00e97f46ae4ae67afa31c10526 (diff) | |
download | android-node-v8-71bcd05232b4fc21db20e5acf019f97780050568.tar.gz android-node-v8-71bcd05232b4fc21db20e5acf019f97780050568.tar.bz2 android-node-v8-71bcd05232b4fc21db20e5acf019f97780050568.zip |
module: resolve self-references
Adds the ability to `import` or `require` a package from within its
own source code. This allows tests and examples to be written using
the package name, making them easier to reuse by consumers of the
package.
Assuming the `name` field in `package.json` is set to `my-pkg`, its
test could use `require('my-pkg')` or `import 'my-pkg'` even if
there's no `node_modules/my-pkg` while testing the package itself.
An important difference between this and relative specifiers like
`require('../')` is that self-references use the public interface
of the package as defined in the `exports` field while relative
specifiers don't.
This behavior is guarded by a new experimental flag
(`--experimental-resolve-self`).
PR-URL: https://github.com/nodejs/node/pull/29327
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaƫl Zasso <targos@protonmail.com>
Diffstat (limited to 'lib/internal')
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 166 |
1 files changed, 129 insertions, 37 deletions
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a28383690a..7df91ce4fd 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -58,6 +58,7 @@ const enableSourceMaps = getOptionValue('--enable-source-maps'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const experimentalModules = getOptionValue('--experimental-modules'); +const experimentalSelf = getOptionValue('--experimental-resolve-self'); const manifest = getOptionValue('--experimental-policy') ? require('internal/process/policy').manifest : null; @@ -241,6 +242,7 @@ function readPackage(requestPath) { try { const parsed = JSON.parse(json); const filtered = { + name: parsed.name, main: parsed.main, exports: parsed.exports, type: parsed.type @@ -370,6 +372,125 @@ function findLongestRegisteredExtension(filename) { return '.js'; } +function resolveBasePath(basePath, exts, isMain, trailingSlash, request) { + let filename; + + const rc = stat(basePath); + if (!trailingSlash) { + if (rc === 0) { // File. + if (!isMain) { + if (preserveSymlinks) { + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } else if (preserveSymlinksMain) { + // For the main module, we use the preserveSymlinksMain flag instead + // mainly for backward compatibility, as the preserveSymlinks flag + // historically has not applied to the main module. Most likely this + // was intended to keep .bin/ binaries working, as following those + // symlinks is usually required for the imports in the corresponding + // files to resolve; that said, in some use cases following symlinks + // causes bigger problems which is why the preserveSymlinksMain option + // is needed. + filename = path.resolve(basePath); + } else { + filename = toRealPath(basePath); + } + } + + if (!filename) { + // Try it with each of the extensions + if (exts === undefined) + exts = Object.keys(Module._extensions); + filename = tryExtensions(basePath, exts, isMain); + } + } + + if (!filename && rc === 1) { // Directory. + // try it with each of the extensions at "index" + if (exts === undefined) + exts = Object.keys(Module._extensions); + filename = tryPackage(basePath, exts, isMain, request); + } + + return filename; +} + +function trySelf(paths, exts, isMain, trailingSlash, request) { + if (!experimentalSelf) { + return false; + } + + const { data: pkg, path: basePath } = readPackageScope(paths[0]); + if (!pkg) return false; + if (typeof pkg.name !== 'string') return false; + + let expansion; + if (request === pkg.name) { + expansion = ''; + } else if (StringPrototype.startsWith(request, `${pkg.name}/`)) { + expansion = StringPrototype.slice(request, pkg.name.length); + } else { + return false; + } + + if (exts === undefined) + exts = Object.keys(Module._extensions); + + if (expansion) { + // Use exports + const fromExports = applyExports(basePath, expansion); + if (!fromExports) return false; + return resolveBasePath(fromExports, exts, isMain, trailingSlash, request); + } else { + // Use main field + return tryPackage(basePath, exts, isMain, request); + } +} + +function applyExports(basePath, expansion) { + const pkgExports = readPackageExports(basePath); + const mappingKey = `.${expansion}`; + + if (typeof pkgExports === 'object' && pkgExports !== null) { + if (ObjectPrototype.hasOwnProperty(pkgExports, mappingKey)) { + const mapping = pkgExports[mappingKey]; + return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', + basePath, mappingKey); + } + + let dirMatch = ''; + for (const candidateKey of Object.keys(pkgExports)) { + if (candidateKey[candidateKey.length - 1] !== '/') continue; + if (candidateKey.length > dirMatch.length && + StringPrototype.startsWith(mappingKey, candidateKey)) { + dirMatch = candidateKey; + } + } + + if (dirMatch !== '') { + const mapping = pkgExports[dirMatch]; + const subpath = StringPrototype.slice(mappingKey, dirMatch.length); + return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, + subpath, basePath, mappingKey); + } + } + if (mappingKey === '.' && typeof pkgExports === 'string') { + return resolveExportsTarget(pathToFileURL(basePath + '/'), pkgExports, + '', basePath, mappingKey); + } + if (pkgExports != null) { + // eslint-disable-next-line no-restricted-syntax + const e = new Error(`Package exports for '${basePath}' do not define ` + + `a '${mappingKey}' subpath`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + } + + return path.resolve(basePath, mappingKey); +} + // This only applies to requests of a specific form: // 1. name/.* // 2. @scope/name/.* @@ -384,43 +505,7 @@ function resolveExports(nmPath, request, absoluteRequest) { } const basePath = path.resolve(nmPath, name); - const pkgExports = readPackageExports(basePath); - const mappingKey = `.${expansion}`; - - if (typeof pkgExports === 'object' && pkgExports !== null) { - if (ObjectPrototype.hasOwnProperty(pkgExports, mappingKey)) { - const mapping = pkgExports[mappingKey]; - return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '', - basePath, mappingKey); - } - - let dirMatch = ''; - for (const candidateKey of Object.keys(pkgExports)) { - if (candidateKey[candidateKey.length - 1] !== '/') continue; - if (candidateKey.length > dirMatch.length && - StringPrototype.startsWith(mappingKey, candidateKey)) { - dirMatch = candidateKey; - } - } - - if (dirMatch !== '') { - const mapping = pkgExports[dirMatch]; - const subpath = StringPrototype.slice(mappingKey, dirMatch.length); - return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, - subpath, basePath, mappingKey); - } - } - if (mappingKey === '.' && typeof pkgExports === 'string') { - return resolveExportsTarget(pathToFileURL(basePath + '/'), pkgExports, - '', basePath, mappingKey); - } - if (pkgExports != null) { - // eslint-disable-next-line no-restricted-syntax - const e = new Error(`Package exports for '${basePath}' do not define ` + - `a '${mappingKey}' subpath`); - e.code = 'MODULE_NOT_FOUND'; - throw e; - } + return applyExports(basePath, expansion); } return path.resolve(nmPath, request); @@ -536,6 +621,13 @@ Module._findPath = function(request, paths, isMain) { return filename; } } + + const selfFilename = trySelf(paths, exts, isMain, trailingSlash, request); + if (selfFilename) { + Module._pathCache[cacheKey] = selfFilename; + return selfFilename; + } + return false; }; |