diff --git a/lib/errors.js b/lib/errors.js index 83a1f68..41ed75c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -282,6 +282,12 @@ codes.ERR_UNSUPPORTED_DIR_IMPORT = createError( Error ) +codes.ERR_UNSUPPORTED_RESOLVE_REQUEST = createError( + 'ERR_UNSUPPORTED_RESOLVE_REQUEST', + 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', + TypeError +) + codes.ERR_UNKNOWN_FILE_EXTENSION = createError( 'ERR_UNKNOWN_FILE_EXTENSION', /** diff --git a/lib/get-format.js b/lib/get-format.js index e8c73cf..f81edca 100644 --- a/lib/get-format.js +++ b/lib/get-format.js @@ -1,9 +1,9 @@ // Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. +// +// Last checked on: Apr 29, 2023. import {fileURLToPath} from 'node:url' -import {getPackageType} from './resolve-get-package-type.js' +import {getPackageType} from './package-json-reader.js' import {codes} from './errors.js' const {ERR_UNKNOWN_FILE_EXTENSION} = codes diff --git a/lib/package-config.js b/lib/package-config.js deleted file mode 100644 index 6bd8edd..0000000 --- a/lib/package-config.js +++ /dev/null @@ -1,55 +0,0 @@ -// Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. - -/** - * @typedef {import('./package-json-reader.js').PackageConfig} PackageConfig - */ - -import {URL, fileURLToPath} from 'node:url' -import packageJsonReader from './package-json-reader.js' - -/** - * @param {URL | string} resolved - * @returns {PackageConfig} - */ -export function getPackageScopeConfig(resolved) { - let packageJSONUrl = new URL('package.json', resolved) - - while (true) { - const packageJSONPath = packageJSONUrl.pathname - if (packageJSONPath.endsWith('node_modules/package.json')) { - break - } - - const packageConfig = packageJsonReader.read( - fileURLToPath(packageJSONUrl), - {specifier: 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) - - return { - pjsonPath: packageJSONPath, - exists: false, - main: undefined, - name: undefined, - type: 'none', - exports: undefined, - imports: undefined - } -} diff --git a/lib/package-json-reader.js b/lib/package-json-reader.js index 1e60487..b30aff1 100644 --- a/lib/package-json-reader.js +++ b/lib/package-json-reader.js @@ -1,6 +1,6 @@ // Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. +// +// Last checked on: Apr 29, 2023. // Removed the native dependency. // Also: no need to cache, we do that in resolve already. @@ -12,11 +12,11 @@ * @typedef PackageConfig * @property {string} pjsonPath * @property {boolean} exists - * @property {string | undefined} main - * @property {string | undefined} name + * @property {string | undefined} [main] + * @property {string | undefined} [name] * @property {PackageType} type - * @property {Record | undefined} exports - * @property {Record | undefined} imports + * @property {Record | undefined} [exports] + * @property {Record | undefined} [imports] */ import fs from 'node:fs' @@ -31,15 +31,12 @@ const {ERR_INVALID_PACKAGE_CONFIG} = codes /** @type {Map} */ const cache = new Map() -const reader = {read} -export default reader - /** * @param {string} jsonPath * @param {{specifier: URL | string, base?: URL}} options * @returns {PackageConfig} */ -function read(jsonPath, {base, specifier}) { +export function read(jsonPath, {base, specifier}) { const existing = cache.get(jsonPath) if (existing) { @@ -83,7 +80,6 @@ function read(jsonPath, {base, specifier}) { (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), cause.message ) - // @ts-expect-error: fine. error.cause = cause throw error } @@ -127,3 +123,55 @@ function read(jsonPath, {base, specifier}) { return result } + +/** + * @param {URL | string} resolved + * @returns {PackageConfig} + */ +export function getPackageScopeConfig(resolved) { + // Note: in Node, this is now a native module. + let packageJSONUrl = new URL('package.json', resolved) + + while (true) { + const packageJSONPath = packageJSONUrl.pathname + if (packageJSONPath.endsWith('node_modules/package.json')) { + break + } + + const packageConfig = read(fileURLToPath(packageJSONUrl), { + specifier: 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) + // ^^ Note: in Node, this is now a native module. + + return { + pjsonPath: packageJSONPath, + exists: false, + type: 'none' + } +} + +/** + * Returns the package type for a given URL. + * @param {URL} url - The URL to get the package type for. + * @returns {PackageType} + */ +export function getPackageType(url) { + // To do @anonrig: Write a C++ function that returns only "type". + return getPackageScopeConfig(url).type +} diff --git a/lib/resolve-get-package-type.js b/lib/resolve-get-package-type.js deleted file mode 100644 index 4844c53..0000000 --- a/lib/resolve-get-package-type.js +++ /dev/null @@ -1,23 +0,0 @@ -// Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. -// -// This file solves a circular dependency. -// In Node.js, `getPackageType` is in `resolve.js`. -// `resolve.js` imports `get-format.js`, which needs `getPackageType`. -// We split that up so that bundlers don’t fail. - -/** - * @typedef {import('./package-json-reader.js').PackageType} PackageType - */ - -import {getPackageScopeConfig} from './package-config.js' - -/** - * @param {URL} url - * @returns {PackageType} - */ -export function getPackageType(url) { - const packageConfig = getPackageScopeConfig(url) - return packageConfig.type -} diff --git a/lib/resolve.js b/lib/resolve.js index 59b6390..7c91c2e 100644 --- a/lib/resolve.js +++ b/lib/resolve.js @@ -1,10 +1,10 @@ // Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. +// +// Last checked on: Apr 29, 2023. /** * @typedef {import('./errors.js').ErrnoException} ErrnoException - * @typedef {import('./package-config.js').PackageConfig} PackageConfig + * @typedef {import('./package-json-reader.js').PackageConfig} PackageConfig */ import assert from 'node:assert' @@ -15,8 +15,7 @@ import path from 'node:path' import {builtinModules} from 'node:module' import {defaultGetFormatWithoutErrors} from './get-format.js' import {codes} from './errors.js' -import {getPackageScopeConfig} from './package-config.js' -import packageJsonReader from './package-json-reader.js' +import {getPackageScopeConfig, read} from './package-json-reader.js' import {getConditionsSet} from './utils.js' const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace] @@ -29,7 +28,8 @@ const { ERR_MODULE_NOT_FOUND, ERR_PACKAGE_IMPORT_NOT_DEFINED, ERR_PACKAGE_PATH_NOT_EXPORTED, - ERR_UNSUPPORTED_DIR_IMPORT + ERR_UNSUPPORTED_DIR_IMPORT, + ERR_UNSUPPORTED_RESOLVE_REQUEST } = codes const own = {}.hasOwnProperty @@ -909,10 +909,6 @@ function packageImportsResolve(name, base, conditions) { throw importNotDefined(name, packageJsonUrl, base) } -// Note: In Node.js, `getPackageType` is here. -// To prevent a circular dependency, we move it to -// `resolve-get-package-type.js`. - /** * @param {string} specifier * @param {URL} base @@ -1013,10 +1009,7 @@ function packageResolve(specifier, base, conditions) { } // Package match. - const packageConfig = packageJsonReader.read(packageJsonPath, { - base, - specifier - }) + const packageConfig = read(packageJsonPath, {base, specifier}) if (packageConfig.exports !== undefined && packageConfig.exports !== null) { return packageExportsResolve( packageJsonUrl, @@ -1082,24 +1075,38 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) { * A URL object to the found thing. */ export function moduleResolve(specifier, base, conditions, preserveSymlinks) { + // Note: The Node code supports `base` as a string (in this internal API) too, + // we don’t. const protocol = base.protocol - const isRemote = protocol === 'http:' || protocol === 'https:' + const isData = protocol === 'data:' + const isRemote = isData || protocol === 'http:' || protocol === 'https:' // Order swapped from spec for minor perf gain. // Ok since relative URLs cannot parse as URLs. /** @type {URL | undefined} */ let resolved if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { - resolved = new URL(specifier, base) - } else if (!isRemote && specifier[0] === '#') { + try { + resolved = new URL(specifier, base) + } catch (error_) { + const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base) + error.cause = error_ + throw error + } + } else if (protocol === 'file:' && specifier[0] === '#') { resolved = packageImportsResolve(specifier, base, conditions) } else { try { resolved = new URL(specifier) - } catch { - if (!isRemote) { - resolved = packageResolve(specifier, base, conditions) + } catch (error_) { + // Note: actual code uses `canBeRequiredWithoutScheme`. + if (isRemote && !builtinModules.includes(specifier)) { + const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base) + error.cause = error_ + throw error } + + resolved = packageResolve(specifier, base, conditions) } } @@ -1232,13 +1239,16 @@ export function defaultResolve(specifier, context = {}) { /** @type {URL | undefined} */ let parsed + /** @type {string | undefined} */ + let protocol + try { parsed = shouldBeTreatedAsRelativeOrAbsolutePath(specifier) ? new URL(specifier, parsedParentURL) : new URL(specifier) // Avoid accessing the `protocol` property due to the lazy getters. - const protocol = parsed.protocol + protocol = parsed.protocol if (protocol === 'data:') { return {url: parsed.href, format: null} @@ -1258,6 +1268,15 @@ export function defaultResolve(specifier, context = {}) { if (maybeReturn) return maybeReturn + // This must come after checkIfDisallowedImport + if (protocol === undefined && parsed) { + protocol = parsed.protocol + } + + if (protocol === 'node:') { + return {url: specifier} + } + // This must come after checkIfDisallowedImport if (parsed && parsed.protocol === 'node:') return {url: specifier} diff --git a/lib/utils.js b/lib/utils.js index 0bcad26..3281c9e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ // Manually “tree shaken” from: -// -// Last checked on: Nov 2, 2023. +// +// Last checked on: Apr 29, 2023. import {codes} from './errors.js' diff --git a/tsconfig.json b/tsconfig.json index bf100ab..e66560f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, - "lib": ["es2020"], + "lib": ["es2022"], "module": "node16", "newLine": "lf", "strict": true,