Skip to content

Commit

Permalink
Backport changes from Node
Browse files Browse the repository at this point in the history
The main change is that `resolve` now returns a URL for files that do
not exist (such as `/whatever`) and for protocols that are unknown
(such as `xss:alert(1)`).
Given that we used to fail and not anymore, and that resolving only
makes sense if you try to load afterwards, which will now perhaps throw,
I don’t think it’s perhaps *technically* breaking.

Commits with particular interest:

54e0cb5051886fd6f9c8219561554d0481e998c2
85301803e1788f6918917b44179ab9d138801e40
9c683204dbd63e9a516213d75cb32be1236adc8f
02926d3c6aaf70eba6d80423beb2d5df97e1ebc7
08ae8401f187051d98942d7b0d213e276e63a86f
951da5282c7b00eb86a989336d628218fb2df057
3ce51ae9c08fb1281591f563568d760192ea07a2
ccada8bccc3ec90fa895b9f19ae37a460c318f60
a2a8e31cbc441cd2b513227be09d90568aa77b5a
ffb1929b6a9b42d9c751723b842a4405154f0d15
6aafb6fbbb0d697fe6b696020a986f46859b480f
569267d04832345e0984049ba50dd5587db7c13a
645b788bea836a2121fd49b935ae204dee36a9c0
dcaded006eb78dc23703ed537b34a503df6f6417
  • Loading branch information
wooorm committed Nov 2, 2023
1 parent 4146122 commit 4ba7a54
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 302 deletions.
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ export function resolve(specifier, parent) {
try {
return defaultResolve(specifier, {parentURL: parent}).url
} catch (error) {
// See: <https://github.com/nodejs/node/blob/45f5c9b/lib/internal/modules/esm/initialize_import_meta.js#L34>
const exception = /** @type {ErrnoException} */ (error)

if (
exception.code === 'ERR_UNSUPPORTED_DIR_IMPORT' &&
(exception.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ||
exception.code === 'ERR_MODULE_NOT_FOUND') &&
typeof exception.url === 'string'
) {
return exception.url
Expand Down
36 changes: 7 additions & 29 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@
*/

// Manually “tree shaken” from:
// <https://github.com/nodejs/node/blob/6668c4d/lib/internal/errors.js>
// Last checked on: Jan 6, 2023.
// <https://github.com/nodejs/node/blob/45f5c9b/lib/internal/errors.js>
// Last checked on: Nov 2, 2023.
import v8 from 'node:v8'
import process from 'node:process'
import assert from 'node:assert'
// Needed for types.
// eslint-disable-next-line no-unused-vars
import {URL} from 'node:url'
import {format, inspect} from 'node:util'

const isWindows = process.platform === 'win32'

const own = {}.hasOwnProperty

const classRegExp = /^([A-Z][a-z\d]*)+$/
Expand Down Expand Up @@ -228,10 +225,12 @@ codes.ERR_MODULE_NOT_FOUND = createError(
/**
* @param {string} path
* @param {string} base
* @param {string} [type]
* @param {boolean} [exactUrl]
*/
(path, base, type = 'package') => {
return `Cannot find ${type} '${path}' imported from ${base}`
(path, base, exactUrl = false) => {
return `Cannot find ${
exactUrl ? 'module' : 'package'
} '${path}' imported from ${base}`
},
Error
)
Expand Down Expand Up @@ -318,27 +317,6 @@ codes.ERR_INVALID_ARG_VALUE = createError(
// , RangeError
)

codes.ERR_UNSUPPORTED_ESM_URL_SCHEME = createError(
'ERR_UNSUPPORTED_ESM_URL_SCHEME',
/**
* @param {URL} url
* @param {Array<string>} supported
*/
(url, supported) => {
let message = `Only URLs with a scheme in: ${formatList(
supported
)} are supported by the default ESM loader`

if (isWindows && url.protocol.length === 2) {
message += '. On Windows, absolute paths must be valid file:// URLs'
}

message += `. Received protocol '${url.protocol}'`
return message
},
Error
)

/**
* Utility function for registering the error codes. Only used here. Exported
* *only* to allow for testing.
Expand Down
48 changes: 28 additions & 20 deletions lib/get-format.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Manually “tree shaken” from:
// <https://github.com/nodejs/node/blob/3e74590/lib/internal/modules/esm/get_format.js>
// Last checked on: Apr 24, 2023.
// <https://github.com/nodejs/node/blob/45f5c9b/lib/internal/modules/esm/get_format.js>
// Last checked on: Nov 2, 2023.

import {URL, fileURLToPath} from 'node:url'
import {fileURLToPath} from 'node:url'
import {getPackageType} from './resolve-get-package-type.js'
import {codes} from './errors.js'

Expand Down Expand Up @@ -37,7 +37,7 @@ function mimeToFormat(mime) {
/**
* @callback ProtocolHandler
* @param {URL} parsed
* @param {{parentURL: string}} context
* @param {{parentURL: string, source?: Buffer}} context
* @param {boolean} ignoreErrors
* @returns {string | null | void}
*/
Expand Down Expand Up @@ -105,7 +105,26 @@ function getFileProtocolModuleFormat(url, _context, ignoreErrors) {
const ext = extname(url)

if (ext === '.js') {
return getPackageType(url) === 'module' ? 'module' : 'commonjs'
const packageType = getPackageType(url)

if (packageType !== 'none') {
return packageType
}

return 'commonjs'
}

if (ext === '') {
const packageType = getPackageType(url)

// Legacy behavior
if (packageType === 'none' || packageType === 'commonjs') {
return 'commonjs'
}

// Note: we don’t implement WASM, so we don’t need
// `getFormatOfExtensionlessFile` from `formats`.
return 'module'
}

const format = extensionFormatMap[ext]
Expand All @@ -130,22 +149,11 @@ function getHttpProtocolModuleFormat() {
* @returns {string | null}
*/
export function defaultGetFormatWithoutErrors(url, context) {
if (!hasOwnProperty.call(protocolHandlers, url.protocol)) {
const protocol = url.protocol

if (!hasOwnProperty.call(protocolHandlers, protocol)) {
return null
}

return protocolHandlers[url.protocol](url, context, true) || null
}

/**
* @param {string} url
* @param {{parentURL: string}} context
* @returns {null | string | void}
*/
export function defaultGetFormat(url, context) {
const parsed = new URL(url)

return hasOwnProperty.call(protocolHandlers, parsed.protocol)
? protocolHandlers[parsed.protocol](parsed, context, false)
: null
return protocolHandlers[protocol](url, context, true) || null
}
124 changes: 25 additions & 99 deletions lib/package-config.js
Original file line number Diff line number Diff line change
@@ -1,129 +1,55 @@
// Manually “tree shaken” from:
// <https://github.com/nodejs/node/blob/3e74590/lib/internal/modules/esm/package_config.js>
// Last checked on: Apr 24, 2023.
// <https://github.com/nodejs/node/blob/45f5c9b/lib/internal/modules/esm/package_config.js>
// Last checked on: Nov 2, 2023.

/**
* @typedef {import('./errors.js').ErrnoException} ErrnoException
*
* @typedef {'commonjs' | 'module' | 'none'} PackageType
*
* @typedef PackageConfig
* @property {string} pjsonPath
* @property {boolean} exists
* @property {string | undefined} main
* @property {string | undefined} name
* @property {PackageType} type
* @property {Record<string, unknown> | undefined} exports
* @property {Record<string, unknown> | undefined} imports
* @typedef {import('./package-json-reader.js').PackageConfig} PackageConfig
*/

import {URL, fileURLToPath} from 'node:url'
import {codes} from './errors.js'
import packageJsonReader from './package-json-reader.js'

const {ERR_INVALID_PACKAGE_CONFIG} = codes

/** @type {Map<string, PackageConfig>} */
const packageJsonCache = new Map()

/**
* @param {string} path
* @param {URL | string} specifier Note: `specifier` is actually optional, not base.
* @param {URL} [base]
* @returns {PackageConfig}
*/
export function getPackageConfig(path, specifier, base) {
const existing = packageJsonCache.get(path)
if (existing !== undefined) {
return existing
}

const source = packageJsonReader.read(path).string

if (source === undefined) {
/** @type {PackageConfig} */
const packageConfig = {
pjsonPath: path,
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined,
imports: undefined
}
packageJsonCache.set(path, packageConfig)
return packageConfig
}

/** @type {Record<string, unknown>} */
let packageJson
try {
packageJson = JSON.parse(source)
} catch (error) {
const exception = /** @type {ErrnoException} */ (error)

throw new ERR_INVALID_PACKAGE_CONFIG(
path,
(base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
exception.message
)
}

const {exports, imports, main, name, type} = packageJson

/** @type {PackageConfig} */
const packageConfig = {
pjsonPath: path,
exists: true,
main: typeof main === 'string' ? main : undefined,
name: typeof name === 'string' ? name : undefined,
type: type === 'module' || type === 'commonjs' ? type : 'none',
// @ts-expect-error Assume `Record<string, unknown>`.
exports,
// @ts-expect-error Assume `Record<string, unknown>`.
imports: imports && typeof imports === 'object' ? imports : undefined
}
packageJsonCache.set(path, packageConfig)
return packageConfig
}

/**
* @param {URL} resolved
* @param {URL | string} resolved
* @returns {PackageConfig}
*/
export function getPackageScopeConfig(resolved) {
let packageJsonUrl = new URL('package.json', resolved)
let packageJSONUrl = new URL('package.json', resolved)

while (true) {
const packageJsonPath = packageJsonUrl.pathname

if (packageJsonPath.endsWith('node_modules/package.json')) break
const packageJSONPath = packageJSONUrl.pathname
if (packageJSONPath.endsWith('node_modules/package.json')) {
break
}

const packageConfig = getPackageConfig(
fileURLToPath(packageJsonUrl),
resolved
const packageConfig = packageJsonReader.read(
fileURLToPath(packageJSONUrl),
{specifier: resolved}
)
if (packageConfig.exists) return packageConfig

const lastPackageJsonUrl = packageJsonUrl
packageJsonUrl = new URL('../package.json', packageJsonUrl)
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
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) {
break
}
}

const packageJsonPath = fileURLToPath(packageJsonUrl)
/** @type {PackageConfig} */
const packageConfig = {
pjsonPath: packageJsonPath,
const packageJSONPath = fileURLToPath(packageJSONUrl)

return {
pjsonPath: packageJSONPath,
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined,
imports: undefined
}
packageJsonCache.set(packageJsonPath, packageConfig)
return packageConfig
}
Loading

0 comments on commit 4ba7a54

Please sign in to comment.