// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. /** * NOTE(bartlomieju): * Functionality of this file is ported in Rust in `cli/compat/esm_resolver.ts`. * Unfortunately we have no way to call ESM resolution in Rust from TypeScript code. */ import { fileURLToPath, pathToFileURL, } from "internal:deno_node/polyfills/url.ts"; import { ERR_INVALID_MODULE_SPECIFIER, ERR_INVALID_PACKAGE_CONFIG, ERR_INVALID_PACKAGE_TARGET, ERR_MODULE_NOT_FOUND, ERR_PACKAGE_IMPORT_NOT_DEFINED, ERR_PACKAGE_PATH_NOT_EXPORTED, NodeError, } from "internal:deno_node/polyfills/internal/errors.ts"; const { hasOwn } = Object; export const encodedSepRegEx = /%2F|%2C/i; function throwInvalidSubpath( subpath: string, packageJSONUrl: string, internal: boolean, base: string, ) { const reason = `request is not a valid subpath for the "${ internal ? "imports" : "exports" }" resolution of ${fileURLToPath(packageJSONUrl)}`; throw new ERR_INVALID_MODULE_SPECIFIER( subpath, reason, base && fileURLToPath(base), ); } function throwInvalidPackageTarget( subpath: string, // deno-lint-ignore no-explicit-any target: any, packageJSONUrl: string, internal: boolean, base: string, ) { if (typeof target === "object" && target !== null) { target = JSON.stringify(target, null, ""); } else { target = `${target}`; } throw new ERR_INVALID_PACKAGE_TARGET( fileURLToPath(new URL(".", packageJSONUrl)), subpath, target, internal, base && fileURLToPath(base), ); } function throwImportNotDefined( specifier: string, packageJSONUrl: URL | undefined, base: string | URL, ): TypeError & { code: string } { throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( specifier, packageJSONUrl && fileURLToPath(new URL(".", packageJSONUrl)), fileURLToPath(base), ); } function throwExportsNotFound( subpath: string, packageJSONUrl: string, base?: string, ): Error & { code: string } { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( subpath, fileURLToPath(new URL(".", packageJSONUrl)), base && fileURLToPath(base), ); } function patternKeyCompare(a: string, b: string): number { const aPatternIndex = a.indexOf("*"); const bPatternIndex = b.indexOf("*"); const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; if (baseLenA > baseLenB) return -1; if (baseLenB > baseLenA) return 1; if (aPatternIndex === -1) return 1; if (bPatternIndex === -1) return -1; if (a.length > b.length) return -1; if (b.length > a.length) return 1; return 0; } function fileExists(url: string | URL): boolean { try { const info = Deno.statSync(url); return info.isFile; } catch { return false; } } function tryStatSync(path: string): { isDirectory: boolean } { try { const info = Deno.statSync(path); return { isDirectory: info.isDirectory }; } catch { return { isDirectory: false }; } } /** * Legacy CommonJS main resolution: * 1. let M = pkg_url + (json main field) * 2. TRY(M, M.js, M.json, M.node) * 3. TRY(M/index.js, M/index.json, M/index.node) * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) * 5. NOT_FOUND */ function legacyMainResolve( packageJSONUrl: URL, packageConfig: PackageConfig, base: string | URL, ): URL { let guess; if (packageConfig.main !== undefined) { // Note: fs check redundances will be handled by Descriptor cache here. if ( fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl)) ) { return guess; } else if ( fileExists(guess = new URL(`./${packageConfig.main}.js`, packageJSONUrl)) ) { // pass } else if ( fileExists( guess = new URL(`./${packageConfig.main}.json`, packageJSONUrl), ) ) { // pass } else if ( fileExists( guess = new URL(`./${packageConfig.main}.node`, packageJSONUrl), ) ) { // pass } else if ( fileExists( guess = new URL(`./${packageConfig.main}/index.js`, packageJSONUrl), ) ) { // pass } else if ( fileExists( guess = new URL(`./${packageConfig.main}/index.json`, packageJSONUrl), ) ) { // pass } else if ( fileExists( guess = new URL(`./${packageConfig.main}/index.node`, packageJSONUrl), ) ) { // pass } else guess = undefined; if (guess) { // TODO(bartlomieju): // emitLegacyIndexDeprecation(guess, packageJSONUrl, base, // packageConfig.main); return guess; } // Fallthrough. } if (fileExists(guess = new URL("./index.js", packageJSONUrl))) { // pass } // So fs. else if (fileExists(guess = new URL("./index.json", packageJSONUrl))) { // pass } else if (fileExists(guess = new URL("./index.node", packageJSONUrl))) { // pass } else guess = undefined; if (guess) { // TODO(bartlomieju): // emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main); return guess; } // Not found. throw new ERR_MODULE_NOT_FOUND( fileURLToPath(new URL(".", packageJSONUrl)), fileURLToPath(base), ); } function parsePackageName( specifier: string, base: string | URL, ): { packageName: string; packageSubpath: string; isScoped: boolean } { let separatorIndex = specifier.indexOf("/"); let validPackageName = true; let isScoped = false; if (specifier[0] === "@") { isScoped = true; if (separatorIndex === -1 || specifier.length === 0) { validPackageName = false; } else { separatorIndex = specifier.indexOf("/", separatorIndex + 1); } } const packageName = separatorIndex === -1 ? specifier : specifier.slice(0, separatorIndex); // Package name cannot have leading . and cannot have percent-encoding or // separators. for (let i = 0; i < packageName.length; i++) { if (packageName[i] === "%" || packageName[i] === "\\") { validPackageName = false; break; } } if (!validPackageName) { throw new ERR_INVALID_MODULE_SPECIFIER( specifier, "is not a valid package name", fileURLToPath(base), ); } const packageSubpath = "." + (separatorIndex === -1 ? "" : specifier.slice(separatorIndex)); return { packageName, packageSubpath, isScoped }; } function packageResolve( specifier: string, base: string, conditions: Set, ): URL | undefined { const { packageName, packageSubpath, isScoped } = parsePackageName( specifier, base, ); // ResolveSelf const packageConfig = getPackageScopeConfig(base); if (packageConfig.exists) { const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); if ( packageConfig.name === packageName && packageConfig.exports !== undefined && packageConfig.exports !== null ) { return packageExportsResolve( packageJSONUrl.toString(), packageSubpath, packageConfig, base, conditions, ); } } let packageJSONUrl = new URL( "./node_modules/" + packageName + "/package.json", base, ); let packageJSONPath = fileURLToPath(packageJSONUrl); let lastPath; do { const stat = tryStatSync( packageJSONPath.slice(0, packageJSONPath.length - 13), ); if (!stat.isDirectory) { lastPath = packageJSONPath; packageJSONUrl = new URL( (isScoped ? "../../../../node_modules/" : "../../../node_modules/") + packageName + "/package.json", packageJSONUrl, ); packageJSONPath = fileURLToPath(packageJSONUrl); continue; } // Package match. const packageConfig = getPackageConfig(packageJSONPath, specifier, base); if (packageConfig.exports !== undefined && packageConfig.exports !== null) { return packageExportsResolve( packageJSONUrl.toString(), packageSubpath, packageConfig, base, conditions, ); } if (packageSubpath === ".") { return legacyMainResolve(packageJSONUrl, packageConfig, base); } return new URL(packageSubpath, packageJSONUrl); // Cross-platform root check. } while (packageJSONPath.length !== lastPath.length); // TODO(bartlomieju): this is false positive // deno-lint-ignore no-unreachable throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); } const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; const patternRegEx = /\*/g; function resolvePackageTargetString( target: string, subpath: string, match: string, packageJSONUrl: string, base: string, pattern: boolean, internal: boolean, conditions: Set, ): URL | undefined { if (subpath !== "" && !pattern && target[target.length - 1] !== "/") { throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } if (!target.startsWith("./")) { if ( internal && !target.startsWith("../") && !target.startsWith("/") ) { let isURL = false; try { new URL(target); isURL = true; } catch { // pass } if (!isURL) { const exportTarget = pattern ? target.replace(patternRegEx, () => subpath) : target + subpath; return packageResolve(exportTarget, packageJSONUrl, conditions); } } throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } if (invalidSegmentRegEx.test(target.slice(2))) { throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } const resolved = new URL(target, packageJSONUrl); const resolvedPath = resolved.pathname; const packagePath = new URL(".", packageJSONUrl).pathname; if (!resolvedPath.startsWith(packagePath)) { throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } if (subpath === "") return resolved; if (invalidSegmentRegEx.test(subpath)) { const request = pattern ? match.replace("*", () => subpath) : match + subpath; throwInvalidSubpath(request, packageJSONUrl, internal, base); } if (pattern) { return new URL(resolved.href.replace(patternRegEx, () => subpath)); } return new URL(subpath, resolved); } function isArrayIndex(key: string): boolean { const keyNum = +key; if (`${keyNum}` !== key) return false; return keyNum >= 0 && keyNum < 0xFFFF_FFFF; } function resolvePackageTarget( packageJSONUrl: string, // deno-lint-ignore no-explicit-any target: any, subpath: string, packageSubpath: string, base: string, pattern: boolean, internal: boolean, conditions: Set, ): URL | undefined { if (typeof target === "string") { return resolvePackageTargetString( target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, conditions, ); } else if (Array.isArray(target)) { if (target.length === 0) { return undefined; } let lastException; for (let i = 0; i < target.length; i++) { const targetItem = target[i]; let resolved; try { resolved = resolvePackageTarget( packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, internal, conditions, ); } catch (e: unknown) { lastException = e; if (e instanceof NodeError && e.code === "ERR_INVALID_PACKAGE_TARGET") { continue; } throw e; } if (resolved === undefined) { continue; } if (resolved === null) { lastException = null; continue; } return resolved; } if (lastException === undefined || lastException === null) { return undefined; } throw lastException; } else if (typeof target === "object" && target !== null) { const keys = Object.getOwnPropertyNames(target); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (isArrayIndex(key)) { throw new ERR_INVALID_PACKAGE_CONFIG( fileURLToPath(packageJSONUrl), base, '"exports" cannot contain numeric property keys.', ); } } for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key === "default" || conditions.has(key)) { const conditionalTarget = target[key]; const resolved = resolvePackageTarget( packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, pattern, internal, conditions, ); if (resolved === undefined) { continue; } return resolved; } } return undefined; } else if (target === null) { return undefined; } throwInvalidPackageTarget( packageSubpath, target, packageJSONUrl, internal, base, ); } export function packageExportsResolve( packageJSONUrl: string, packageSubpath: string, packageConfig: PackageConfig, base: string, conditions: Set, // @ts-ignore `URL` needs to be forced due to control flow ): URL { let exports = packageConfig.exports; if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { exports = { ".": exports }; } if ( hasOwn(exports, packageSubpath) && !packageSubpath.includes("*") && !packageSubpath.endsWith("/") ) { const target = exports[packageSubpath]; const resolved = resolvePackageTarget( packageJSONUrl, target, "", packageSubpath, base, false, false, conditions, ); if (resolved === null || resolved === undefined) { throwExportsNotFound(packageSubpath, packageJSONUrl, base); } return resolved!; } let bestMatch = ""; let bestMatchSubpath = ""; const keys = Object.getOwnPropertyNames(exports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const patternIndex = key.indexOf("*"); if ( patternIndex !== -1 && packageSubpath.startsWith(key.slice(0, patternIndex)) ) { // When this reaches EOL, this can throw at the top of the whole function: // // if (StringPrototypeEndsWith(packageSubpath, '/')) // throwInvalidSubpath(packageSubpath) // // To match "imports" and the spec. if (packageSubpath.endsWith("/")) { // TODO(@bartlomieju): // emitTrailingSlashPatternDeprecation( // packageSubpath, // packageJSONUrl, // base, // ); } const patternTrailer = key.slice(patternIndex + 1); if ( packageSubpath.length >= key.length && packageSubpath.endsWith(patternTrailer) && patternKeyCompare(bestMatch, key) === 1 && key.lastIndexOf("*") === patternIndex ) { bestMatch = key; bestMatchSubpath = packageSubpath.slice( patternIndex, packageSubpath.length - patternTrailer.length, ); } } } if (bestMatch) { const target = exports[bestMatch]; const resolved = resolvePackageTarget( packageJSONUrl, target, bestMatchSubpath, bestMatch, base, true, false, conditions, ); if (resolved === null || resolved === undefined) { throwExportsNotFound(packageSubpath, packageJSONUrl, base); } return resolved!; } throwExportsNotFound(packageSubpath, packageJSONUrl, base); } export interface PackageConfig { pjsonPath: string; exists: boolean; name?: string; main?: string; // deno-lint-ignore no-explicit-any exports?: any; // deno-lint-ignore no-explicit-any imports?: any; type?: string; } const packageJSONCache = new Map(); /* string -> PackageConfig */ function getPackageConfig( path: string, specifier: string | URL, base?: string | URL, ): PackageConfig { const existing = packageJSONCache.get(path); if (existing !== undefined) { return existing; } let source: string | undefined; try { source = new TextDecoder().decode( Deno.readFileSync(path), ); } catch { // pass } if (source === undefined) { const packageConfig = { pjsonPath: path, exists: false, main: undefined, name: undefined, type: "none", exports: undefined, imports: undefined, }; packageJSONCache.set(path, packageConfig); return packageConfig; } let packageJSON; try { packageJSON = JSON.parse(source); } catch (error) { throw new ERR_INVALID_PACKAGE_CONFIG( path, (base ? `"${specifier}" from ` : "") + fileURLToPath(base || specifier), // @ts-ignore there's no assertion for type and `error` is thus `unknown` error.message, ); } let { imports, main, name, type } = packageJSON; const { exports } = packageJSON; if (typeof imports !== "object" || imports === null) imports = undefined; if (typeof main !== "string") main = undefined; if (typeof name !== "string") name = undefined; // Ignore unknown types for forwards compatibility if (type !== "module" && type !== "commonjs") type = "none"; const packageConfig = { pjsonPath: path, exists: true, main, name, type, exports, imports, }; packageJSONCache.set(path, packageConfig); return packageConfig; } function getPackageScopeConfig(resolved: URL | string): PackageConfig { let packageJSONUrl = new URL("./package.json", resolved); while (true) { const packageJSONPath = packageJSONUrl.pathname; if (packageJSONPath.endsWith("node_modules/package.json")) { break; } const packageConfig = getPackageConfig( fileURLToPath(packageJSONUrl), resolved, ); if (packageConfig.exists) return packageConfig; const lastPackageJSONUrl = packageJSONUrl; packageJSONUrl = new URL("../package.json", packageJSONUrl); // Terminates at root where ../package.json equals ../../package.json // (can't just check "/package.json" for Windows support). if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; } const packageJSONPath = fileURLToPath(packageJSONUrl); const packageConfig = { pjsonPath: packageJSONPath, exists: false, main: undefined, name: undefined, type: "none", exports: undefined, imports: undefined, }; packageJSONCache.set(packageJSONPath, packageConfig); return packageConfig; } export function packageImportsResolve( name: string, base: string, conditions: Set, // @ts-ignore `URL` needs to be forced due to control flow ): URL { if ( name === "#" || name.startsWith("#/") || name.startsWith("/") ) { const reason = "is not a valid internal imports specifier name"; throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); } let packageJSONUrl; const packageConfig = getPackageScopeConfig(base); if (packageConfig.exists) { packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); const imports = packageConfig.imports; if (imports) { if ( hasOwn(imports, name) && !name.includes("*") ) { const resolved = resolvePackageTarget( packageJSONUrl.toString(), imports[name], "", name, base, false, true, conditions, ); if (resolved !== null && resolved !== undefined) { return resolved; } } else { let bestMatch = ""; let bestMatchSubpath = ""; const keys = Object.getOwnPropertyNames(imports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const patternIndex = key.indexOf("*"); if ( patternIndex !== -1 && name.startsWith( key.slice(0, patternIndex), ) ) { const patternTrailer = key.slice(patternIndex + 1); if ( name.length >= key.length && name.endsWith(patternTrailer) && patternKeyCompare(bestMatch, key) === 1 && key.lastIndexOf("*") === patternIndex ) { bestMatch = key; bestMatchSubpath = name.slice( patternIndex, name.length - patternTrailer.length, ); } } } if (bestMatch) { const target = imports[bestMatch]; const resolved = resolvePackageTarget( packageJSONUrl.toString(), target, bestMatchSubpath, bestMatch, base, true, true, conditions, ); if (resolved !== null && resolved !== undefined) { return resolved; } } } } } throwImportNotDefined(name, packageJSONUrl, base); } function isConditionalExportsMainSugar( // deno-lint-ignore no-explicit-any exports: any, packageJSONUrl: string, base: string, ): boolean { if (typeof exports === "string" || Array.isArray(exports)) return true; if (typeof exports !== "object" || exports === null) return false; const keys = Object.getOwnPropertyNames(exports); let isConditionalSugar = false; let i = 0; for (let j = 0; j < keys.length; j++) { const key = keys[j]; const curIsConditionalSugar = key === "" || key[0] !== "."; if (i++ === 0) { isConditionalSugar = curIsConditionalSugar; } else if (isConditionalSugar !== curIsConditionalSugar) { const message = "\"exports\" cannot contain some keys starting with '.' and some not." + " The exports object must either be an object of package subpath keys" + " or an object of main entry condition name keys only."; throw new ERR_INVALID_PACKAGE_CONFIG( fileURLToPath(packageJSONUrl), base, message, ); } } return isConditionalSugar; }