Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
357 lines
10 KiB
Plaintext
357 lines
10 KiB
Plaintext
'use strict'
|
|
|
|
import getEsmExports from './get-esm-exports.mjs'
|
|
import { parse as parseCjs, init as parserInit } from 'cjs-module-lexer'
|
|
import { readFileSync, existsSync } from 'fs'
|
|
import { builtinModules, createRequire } from 'module'
|
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
import { dirname, join } from 'path'
|
|
|
|
const nodeMajor = Number(process.versions.node.split('.')[0])
|
|
export const hasModuleExportsCJSDefault = nodeMajor >= 23
|
|
|
|
let parserInitialized = false
|
|
|
|
function addDefault (arr) {
|
|
return new Set(['default', ...arr])
|
|
}
|
|
|
|
function hasEsmSyntax (source) {
|
|
// Lightweight scan (no full parse) to determine if the *source code*
|
|
// contains ESM-specific syntax. This is used only when:
|
|
// - the loader chain didn't tell us a `format`, and
|
|
// - `getEsmExports()` found no exports.
|
|
//
|
|
// Notes:
|
|
// - We ignore comments and strings to reduce false positives.
|
|
// - We treat `import.meta` and static `import ...` as ESM.
|
|
// - We do NOT treat `import(` (dynamic import) as ESM because it is allowed
|
|
// in CJS as an expression.
|
|
if (source.indexOf('import') === -1) return false
|
|
|
|
const isIdentCharCode = (code) => (
|
|
(code >= 48 && code <= 57) || // 0-9
|
|
(code >= 65 && code <= 90) || // A-Z
|
|
(code >= 97 && code <= 122) || // a-z
|
|
code === 95 || // _
|
|
code === 36 // $
|
|
)
|
|
|
|
const skipWhitespace = (idx) => {
|
|
while (idx < source.length) {
|
|
const c = source.charCodeAt(idx)
|
|
// space, tab, cr, lf
|
|
if (c !== 32 && c !== 9 && c !== 13 && c !== 10) break
|
|
idx++
|
|
}
|
|
return idx
|
|
}
|
|
|
|
let i = 0
|
|
while (i < source.length) {
|
|
const ch = source[i]
|
|
|
|
// Line comment
|
|
if (ch === '/' && source[i + 1] === '/') {
|
|
i += 2
|
|
while (i < source.length && source[i] !== '\n') i++
|
|
continue
|
|
}
|
|
|
|
// Block comment
|
|
if (ch === '/' && source[i + 1] === '*') {
|
|
i += 2
|
|
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++
|
|
i += 2
|
|
continue
|
|
}
|
|
|
|
// Strings: '...' or "..."
|
|
if (ch === '\'' || ch === '"') {
|
|
const quote = ch
|
|
i++
|
|
while (i < source.length) {
|
|
const c = source[i]
|
|
if (c === '\\') {
|
|
i += 2
|
|
continue
|
|
}
|
|
if (c === quote) {
|
|
i++
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Template strings: `...`
|
|
if (ch === '`') {
|
|
i++
|
|
while (i < source.length) {
|
|
const c = source[i]
|
|
if (c === '\\') {
|
|
i += 2
|
|
continue
|
|
}
|
|
if (c === '`') {
|
|
i++
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Keyword scan (word-boundary): import
|
|
if (ch === 'i') {
|
|
const prev = source.charCodeAt(i - 1)
|
|
if (i > 0 && isIdentCharCode(prev)) {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
if (source.startsWith('import', i)) {
|
|
const next = source.charCodeAt(i + 6)
|
|
if (isIdentCharCode(next)) {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
const j = skipWhitespace(i + 6)
|
|
// `import.meta` is ESM-only
|
|
if (source[j] === '.') return true
|
|
// `import(` is dynamic import, allowed in CJS
|
|
if (source[j] === '(') {
|
|
i = j + 1
|
|
continue
|
|
}
|
|
// Otherwise assume it's a static import form
|
|
return true
|
|
}
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Cached exports for Node built-in modules
|
|
const BUILT_INS = new Map()
|
|
|
|
let require
|
|
|
|
function getExportsForNodeBuiltIn (name) {
|
|
let exports = BUILT_INS.get(name)
|
|
|
|
if (!require) {
|
|
require = createRequire(import.meta.url)
|
|
}
|
|
|
|
if (!exports) {
|
|
exports = new Set(addDefault(Object.keys(require(name))))
|
|
BUILT_INS.set(name, exports)
|
|
}
|
|
|
|
return exports
|
|
}
|
|
|
|
const urlsBeingProcessed = new Set() // Guard against circular imports.
|
|
|
|
/**
|
|
* This function looks for the package.json which contains the specifier trying to resolve.
|
|
* Once the package.json file has been found, we extract the file path from the specifier
|
|
* @param {string} specifier The specifier that is being search for inside the imports object
|
|
* @param {URL|string} fromUrl The url from which the search starts from
|
|
* @returns array with url and resolvedExport
|
|
*/
|
|
function resolvePackageImports (specifier, fromUrl) {
|
|
try {
|
|
const fromPath = fileURLToPath(fromUrl)
|
|
let currentDir = dirname(fromPath)
|
|
|
|
// search for package.json file which has the real url to export
|
|
while (currentDir !== dirname(currentDir)) {
|
|
const packageJsonPath = join(currentDir, 'package.json')
|
|
|
|
if (existsSync(packageJsonPath)) {
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
if (packageJson.imports && packageJson.imports[specifier]) {
|
|
const imports = packageJson.imports[specifier]
|
|
|
|
// Look for path inside packageJson
|
|
let resolvedExport
|
|
if (imports && typeof imports === 'object') {
|
|
const requireExport = imports.require
|
|
const importExport = imports.import
|
|
// look for the possibility of require and import which is standard for CJS/ESM
|
|
if (requireExport || importExport) {
|
|
// trying to resolve based on order of importance
|
|
resolvedExport = requireExport.node || requireExport.default || importExport.node || importExport.default
|
|
} else if (imports.node || imports.default) {
|
|
resolvedExport = imports.node || imports.default
|
|
}
|
|
} else if (typeof imports === 'string') {
|
|
resolvedExport = imports
|
|
}
|
|
|
|
if (resolvedExport) {
|
|
const url = resolvedExport.startsWith('.')
|
|
? pathToFileURL(join(currentDir, resolvedExport))
|
|
: fromUrl
|
|
return [url, resolvedExport]
|
|
}
|
|
}
|
|
// return if we find a package.json but did not find an import
|
|
return null
|
|
}
|
|
|
|
currentDir = dirname(currentDir)
|
|
}
|
|
} catch (cause) {
|
|
throw Error(`Failed to find export: ${specifier}`, { cause })
|
|
}
|
|
return null
|
|
}
|
|
|
|
async function getCjsExports (url, context, parentLoad, source) {
|
|
if (urlsBeingProcessed.has(url)) {
|
|
return new Set()
|
|
}
|
|
urlsBeingProcessed.add(url)
|
|
|
|
try {
|
|
if (!parserInitialized) {
|
|
await parserInit()
|
|
parserInitialized = true
|
|
}
|
|
const result = parseCjs(source)
|
|
const full = addDefault(result.exports)
|
|
|
|
await Promise.all(result.reexports.map(async re => {
|
|
if (re.startsWith('node:') || builtinModules.includes(re)) {
|
|
for (const each of getExportsForNodeBuiltIn(re)) {
|
|
full.add(each)
|
|
}
|
|
} else {
|
|
if (re === '.') {
|
|
re = './'
|
|
}
|
|
|
|
// Entries in the import field should always start with #
|
|
if (re.startsWith('#')) {
|
|
const resolved = resolvePackageImports(re, url)
|
|
if (!resolved) return
|
|
[url, re] = resolved
|
|
}
|
|
|
|
// Resolve the re-exported module relative to the current module.
|
|
if (!require) {
|
|
require = createRequire(import.meta.url)
|
|
}
|
|
const newUrl = pathToFileURL(require.resolve(re, { paths: [dirname(fileURLToPath(url))] })).href
|
|
|
|
if (newUrl.endsWith('.node') || newUrl.endsWith('.json')) {
|
|
return
|
|
}
|
|
|
|
for (const each of await getExports(newUrl, context, parentLoad)) {
|
|
full.add(each)
|
|
}
|
|
}
|
|
}))
|
|
|
|
// added in node 23 as alias for default in cjs modules
|
|
if (full.has('default') && hasModuleExportsCJSDefault) {
|
|
full.add('module.exports')
|
|
}
|
|
|
|
// we know that it's commonjs at this point, because ESM failed
|
|
context.format = 'commonjs'
|
|
return full
|
|
} finally {
|
|
urlsBeingProcessed.delete(url)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inspects a module for its type (commonjs or module), attempts to get the
|
|
* source code for said module from the loader API, and parses the result
|
|
* for the entities exported from that module.
|
|
*
|
|
* @param {string} url A file URL string pointing to the module that
|
|
* we should get the exports of.
|
|
* @param {object} context Context object as provided by the `load`
|
|
* hook from the loaders API.
|
|
* @param {Function} parentLoad Next hook function in the loaders API
|
|
* hook chain.
|
|
*
|
|
* @returns {Promise<Set<string>>} An array of identifiers exported by the module.
|
|
* Please see {@link getEsmExports} for caveats on special identifiers that may
|
|
* be included in the result set.
|
|
*/
|
|
export async function getExports (url, context, parentLoad) {
|
|
// `parentLoad` gives us the possibility of getting the source
|
|
// from an upstream loader. This doesn't always work though,
|
|
// so later on we fall back to reading it from disk.
|
|
const parentCtx = await parentLoad(url, context)
|
|
let source = parentCtx.source
|
|
const format = parentCtx.format
|
|
|
|
// Loader hooks can return ArrayBuffer / TypedArray sources. Normalize to a
|
|
// string for parsing.
|
|
if (source && typeof source !== 'string') {
|
|
// Avoid copies where possible:
|
|
// - Buffer.from(Uint8Array) copies
|
|
// - Buffer.from(ArrayBuffer, offset, length) wraps the existing memory
|
|
if (Buffer.isBuffer(source)) {
|
|
source = source.toString('utf8')
|
|
} else if (ArrayBuffer.isView(source)) {
|
|
source = Buffer.from(source.buffer, source.byteOffset, source.byteLength).toString('utf8')
|
|
} else {
|
|
source = Buffer.from(source).toString('utf8')
|
|
}
|
|
}
|
|
|
|
if (!source) {
|
|
if (format === 'builtin') {
|
|
// Builtins don't give us the source property, so we're stuck
|
|
// just requiring it to get the exports.
|
|
return getExportsForNodeBuiltIn(url)
|
|
}
|
|
|
|
// Sometimes source is retrieved by parentLoad, CommonJs isn't.
|
|
source = readFileSync(fileURLToPath(url), 'utf8')
|
|
}
|
|
|
|
try {
|
|
if (format === 'module') {
|
|
return getEsmExports(source)
|
|
}
|
|
|
|
if (format === 'commonjs') {
|
|
return await getCjsExports(url, context, parentLoad, source)
|
|
}
|
|
|
|
// At this point our `format` is either undefined or not known by us. Fall
|
|
// back to parsing as ESM/CJS.
|
|
const esmExports = getEsmExports(source)
|
|
if (!esmExports.size) {
|
|
// If there's strong evidence this is ESM (static import/import.meta),
|
|
// prefer returning the empty ESM export set over incorrectly treating it
|
|
// as CJS.
|
|
if (!hasEsmSyntax(source)) {
|
|
// It might be possible to get here if the format
|
|
// isn't set at first and yet we have an ESM module with no exports.
|
|
return await getCjsExports(url, context, parentLoad, source)
|
|
}
|
|
}
|
|
return esmExports
|
|
} catch (cause) {
|
|
const err = new Error(`Failed to parse '${url}'`)
|
|
err.cause = cause
|
|
throw err
|
|
}
|
|
}
|