Vendor Cordis framework packages as source

cordis 4.0.0-rc.6, plugin-loader, -include, -group, -timer, -hmr,
-logger-console, cosmokit 1.8.1, schemastery 3.18.0 — copied from the
cordis-workspace checkout, flattened under vendor/, original npm names,
private: true. vendor/README.md is the manifest: upstream repos +
commit SHAs, local-modification log, sync procedure.

Local modification: hmr's locale YAML imports and .i18n() call removed
(avoids a runtime YAML import hook we don't vendor).
This commit is contained in:
Tianyi Cui
2026-06-11 10:53:32 +08:00
parent ae2e08b4d6
commit 72688a3888
69 changed files with 6659 additions and 0 deletions

184
vendor/loader/src/config/entry.ts vendored Normal file
View File

@@ -0,0 +1,184 @@
import { Context, Fiber, Inject } from 'cordis'
import { deepEqual, isNullable } from 'cosmokit'
import { Loader } from '../index.ts'
import { EntryGroup } from './group.ts'
import { EntryTree } from './tree.ts'
import { evaluate, interpolate } from './utils.ts'
/** Serialized plugin entry options stored in loader config files. */
export interface EntryOptions {
/** Stable id inside the containing entry tree. */
id: string
/** Module specifier imported by the entry tree. */
name: string
/** Config passed to the plugin. */
config?: any
/** Marks this entry as a nested group. */
group?: boolean | null
/** Prevents this entry and descendants from running. */
disabled?: boolean | null
/** Required services or service intercept config for this entry. */
inject?: Inject | null
}
function takeEntries(object: {}, keys: string[]) {
const result: [string, any][] = []
for (const key of keys) {
if (!(key in object)) continue
result.push([key, object[key]])
delete object[key]
}
return result
}
function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
const part1 = takeEntries(object, prepend)
const part2 = takeEntries(object, append)
const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
}
/** One configured plugin node inside an `EntryTree`. */
export class Entry {
static readonly key = Symbol.for('cordis.entry')
public ctx: Context
public fiber?: Fiber
public parent!: EntryGroup
// safety: call `entry.update()` immediately after creating an entry
public options = {} as EntryOptions
public subgroup?: EntryGroup
public subtree?: EntryTree
_initTask?: Promise<void>
constructor(public loader: Loader) {
this.ctx = loader.ctx.extend({ [Entry.key]: this })
this.context.emit('loader/entry-init', this)
}
get context(): Context {
return this.ctx
}
get id() {
let id = this.options.id
if (this.parent.tree.ctx.fiber.entry) {
id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
}
return id
}
/** True when this entry or any owning parent entry is disabled. */
get disabled() {
// group is always enabled
if (this.options.group) return false
let entry: Entry | undefined = this
do {
if (entry.options.disabled) return true
entry = entry.parent.ctx.fiber.entry
} while (entry)
return false
}
evaluate(expr: string) {
return evaluate(this.ctx, expr)
}
_resolveConfig(plugin: any): [any, any?] {
if (plugin[EntryGroup.key]) return this.options.config
return interpolate(this.ctx, this.options.config)
}
private _patchContext(diff: string[]) {
this.context.waterfall('loader/patch-context', this, () => {
Object.setPrototypeOf(this.ctx, this.parent.ctx)
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
}
})
}
async refresh() {
if (this.fiber) return
if (this.disabled) return
await this.init()
}
/** Merge new options, restart as needed, and persist through the parent tree. */
async update(options: Partial<EntryOptions>, create = false, force = false) {
const legacy = { ...this.options }
// step 1: update options
if (create) {
this.options = options as EntryOptions
} else {
for (const [key, value] of Object.entries(options)) {
if (isNullable(value)) {
delete this.options[key]
} else {
this.options[key] = value
}
}
}
sortKeys(this.options)
// step 2: execute
if (this.disabled) {
this.fiber?.dispose()
return
}
// step 3: check if options are changed
if (this.fiber?.uid) {
const diff = Object
.keys({ ...this.options, ...legacy })
.filter(key => !deepEqual(this.options[key], legacy[key]))
if (!diff.length && !force) return
this.context.emit('loader/partial-dispose', this, legacy, true)
this._patchContext(diff)
} else {
await this.init()
}
}
getOuterStack = () => {
let entry: Entry | undefined = this
const result: string[] = []
do {
result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
entry = entry.parent.ctx.fiber.entry
} while (entry)
return result
}
/** Import and start the configured plugin if it is not already running. */
async init() {
try {
await (this._initTask ??= this._init())
} finally {
this._initTask = undefined
}
this.fiber?.await().finally(() => {
if (this.loader.getTasks().length) return
this.ctx.reflect.notify(['loader'])
})
}
private async _init() {
let exports: any
try {
exports = await this.parent.tree.import(this.options.name, this.getOuterStack)
} catch (error) {
this.ctx.logger.error(error)
return
} finally {
this._initTask = undefined
}
const plugin = this.loader.unwrapExports(exports)
this._patchContext([])
this.loader.showLog(this, 'apply')
this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
}
}

90
vendor/loader/src/config/group.ts vendored Normal file
View File

@@ -0,0 +1,90 @@
import { Context, Service } from 'cordis'
import { Entry, EntryOptions } from './entry.ts'
import { EntryTree } from './tree.ts'
/** Runtime owner for a list of child loader entries. */
export class EntryGroup {
static readonly key = Symbol.for('cordis.group')
public data: EntryOptions[] = []
constructor(public ctx: Context, public tree: EntryTree) {
const entry = ctx.fiber.entry
if (entry) entry.subgroup = this
}
get context(): Context {
return this.ctx
}
async create(options: Omit<EntryOptions, 'id'>) {
const id = this.tree.ensureId(options)
const entry: Entry = this.tree.store[id] ??= new Entry(this.ctx.loader)
// Entry may be moved from another group,
// so we need to update the parent reference.
entry.parent = this
// Use `create: true` to replace existing entry.options.
await entry.update(options, true, true)
return entry.id
}
unlink(options: EntryOptions) {
const config = this.data
const index = config.indexOf(options)
if (index >= 0) config.splice(index, 1)
}
remove(id: string, isDispose = false) {
const entry = this.tree.store[id]
if (!entry) return
entry.fiber?.dispose()
if (!isDispose) {
this.unlink(entry.options)
}
delete this.tree.store[id]
this.context.emit('loader/partial-dispose', entry, entry.options, false)
}
async update(config: EntryOptions[]) {
const oldConfig = this.data as EntryOptions[]
this.data = config
const oldMap = Object.fromEntries(oldConfig.map(options => [options.id, options]))
const newMap = Object.fromEntries(config.map(options => [options.id ?? Symbol('anonymous'), options]))
// update inner plugins
const ids = Reflect.ownKeys({ ...oldMap, ...newMap }) as string[]
await Promise.all(ids.map(async (id) => {
if (newMap[id]) {
await this.create(newMap[id]).catch((error) => {
this.ctx.logger.error(error)
})
} else {
this.remove(id)
}
}))
}
stop() {
for (const options of this.data) {
this.remove(options.id, true)
}
}
}
/** Plugin that mounts a nested loader entry group. */
export class Group extends EntryGroup {
static initial: Omit<EntryOptions, 'id'>[] = []
static readonly [EntryGroup.key] = true
constructor(public ctx: Context, public config: EntryOptions[]) {
super(ctx, ctx.fiber.entry!.parent.tree)
ctx.on('internal/update', (config) => {
this.update(config)
})
}
async* [Service.init]() {
yield () => this.stop()
await this.update(this.config)
}
}

173
vendor/loader/src/config/isolate.ts vendored Normal file
View File

@@ -0,0 +1,173 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { Entry } from './entry.ts'
declare module './entry.ts' {
interface EntryOptions {
intercept?: Dict | null
isolate?: Dict<true | string> | null
}
interface Entry {
realm: LocalRealm
}
}
function swap<T extends {}>(target: T, source?: T | null) {
for (const key of Reflect.ownKeys(target)) {
Reflect.deleteProperty(target, key)
}
for (const key of Reflect.ownKeys(source || {})) {
Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!)
}
}
/** Symbol realm used to isolate service implementations by entry or label. */
export abstract class Realm {
protected store: Dict<symbol> = Object.create(null)
abstract get suffix(): string
access(key: string, create = false) {
if (create) {
return this.store[key] ??= Symbol(`${key}${this.suffix}`)
} else {
return this.store[key] ?? Symbol(`${key}${this.suffix}`)
}
}
delete(key: string) {
delete this.store[key]
}
get size() {
return Object.keys(this.store).length
}
}
/** Entry-local isolation realm. */
export class LocalRealm extends Realm {
constructor(private entry: Entry) {
super()
}
get suffix() {
return '#' + this.entry.options.id
}
}
/** Named isolation realm shared by entries that use the same label. */
export class GlobalRealm extends Realm {
constructor(public label: string) {
super()
}
get suffix() {
return '@' + this.label
}
}
/** Install loader hooks that apply `intercept` and `isolate` entry options. */
export default function isolate(ctx: Context) {
const realms: Dict<GlobalRealm> = Object.create(null)
const delims: Dict<symbol> = Object.create(null)
function access(entry: Entry, name: string, create: true): symbol
function access(entry: Entry, name: string, create?: boolean): symbol | undefined
function access(entry: Entry, name: string, create = false) {
let realm: Realm | undefined
const label = entry.options.isolate?.[name]
if (!label) return
if (label === true) {
realm = entry.realm ??= new LocalRealm(entry)
} else if (create) {
realm = realms[label] ??= new GlobalRealm(label)
} else {
realm = realms[label]
}
return realm?.access(name, create)
}
ctx.on('loader/entry-init', (entry) => {
entry.ctx[Context.intercept] = Object.create(entry.ctx[Context.intercept])
entry.ctx[Context.isolate] = Object.create(entry.ctx[Context.isolate])
})
ctx.on('loader/patch-context', (entry, next) => {
// step 1: generate new isolate map
const newMap: Dict<symbol> = Object.create(entry.parent.ctx[Context.isolate])
for (const name of Object.keys(entry.options.isolate ?? {})) {
newMap[name] = access(entry, name, true)
}
// step 2: generate service diff
const diff: Dict<[symbol, symbol, symbol, symbol]> = Object.create(null)
const oldMap = entry.ctx[Context.isolate]
for (const name in { ...newMap, ...delims }) {
if (newMap[name] === oldMap[name]) continue
const delim = delims[name] ??= Symbol(`delim:${name}`)
entry.ctx[delim] = Symbol(`${name}#${entry.id}`)
for (const symbol of [oldMap[name], newMap[name]]) {
const impl = symbol && entry.ctx.reflect.store[symbol]
if (!impl) continue
if (!impl.fiber) {
entry.ctx.logger.warn(new Error(`expected service ${name} to be implemented`))
continue
}
diff[name] = [oldMap[name], newMap[name], entry.ctx[delim], impl.fiber.ctx[delim]]
if (entry.ctx[delim] !== impl.fiber.ctx[delim]) break
}
}
// step 3: set prototype for transferred context
Object.setPrototypeOf(entry.ctx[Context.isolate], entry.parent.ctx[Context.isolate])
Object.setPrototypeOf(entry.ctx[Context.intercept], entry.parent.ctx[Context.intercept])
swap(entry.ctx[Context.isolate], newMap)
swap(entry.ctx[Context.intercept], entry.options.intercept)
// step 4: reload fiber
next()
// step 5: replace service impl
for (const [symbol1, symbol2, flag1, flag2] of Object.values(diff)) {
if (flag1 === flag2 && entry.ctx.reflect.store[symbol1] && !entry.ctx.reflect.store[symbol2]) {
entry.ctx.reflect.store[symbol2] = entry.ctx.reflect.store[symbol1]
delete entry.ctx.reflect.store[symbol1]
}
}
// step 6: reflect notify
ctx.reflect.notify(Object.keys(diff), (ctx, name) => {
const [symbol1, symbol2, flag1, flag2] = diff[name]
const symbol3 = ctx[Context.isolate][name]
const flag3 = ctx[delims[name]]
return (symbol1 === symbol3 || symbol2 === symbol3) && (flag1 === flag3) !== (flag1 === flag2)
})
// step 7: clean up delimiters
for (const name in delims) {
if (!Reflect.ownKeys(newMap).includes(name)) {
delete entry.ctx[delims[name]]
}
}
})
ctx.on('loader/partial-dispose', (entry, legacy, active) => {
for (const [name, label] of Object.entries(legacy.isolate ?? {})) {
if (label === true) continue
if (active && entry.options.isolate?.[name] === label) continue
const realm = realms[label]
if (!realm) continue
// realm garbage collection
for (const entry of ctx.loader.entries()) {
// has reference to this realm
if (entry.options.isolate?.[name] === realm.label) return
}
realm.delete(name)
if (!realm.size) {
delete realms[realm.label]
}
}
})
}

133
vendor/loader/src/config/tree.ts vendored Normal file
View File

@@ -0,0 +1,133 @@
import { composeError, Context } from 'cordis'
import { Dict, isNonNullable } from 'cosmokit'
import { Entry, EntryOptions } from './entry.ts'
import { EntryGroup } from './group.ts'
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
export abstract class EntryTree {
static readonly sep = ':'
public ctx: Context
public enableLogs?: boolean
public root: EntryGroup
public store: Dict<Entry> = Object.create(null)
constructor(ctx: Context) {
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })
this.root = new EntryGroup(this.ctx, this)
const entry = this.ctx.fiber.entry
if (entry) entry.subtree = this
}
get context(): Context {
return this.ctx
}
/** Iterate entries in this tree and any nested subtrees. */
* entries(): Generator<Entry, void, void> {
for (const entry of Object.values(this.store)) {
yield entry
if (!entry.subtree) continue
yield* entry.subtree.entries()
}
}
/** Return pending import and lifecycle tasks owned by this tree. */
getTasks() {
return [...this.entries()]
.map(entry => entry._initTask || entry.fiber?.inertia)
.filter(isNonNullable)
}
/** Wait until this tree has no pending import or lifecycle tasks. */
async await() {
while (true) {
const tasks = this.getTasks()
if (!tasks.length) return
await Promise.allSettled(tasks)
}
}
ensureId(options: Partial<EntryOptions>) {
if (!options.id) {
do {
options.id = Math.random().toString(16).slice(2, 10)
} while (this.store[options.id])
}
return options.id!
}
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
resolve(id: string) {
const parts = id.split(EntryTree.sep)
let tree: EntryTree | undefined = this
const final = parts.pop()!
for (const part of parts) {
tree = tree.store[part]?.subtree
if (!tree) throw new Error(`cannot resolve entry ${id}`)
}
const entry = tree.store[final]
if (!entry) throw new Error(`cannot resolve entry ${id}`)
return entry
}
resolveGroup(id: string | null) {
if (!id) return this.root
const entry = this.resolve(id)
if (!entry.subgroup) throw new Error(`entry ${id} is not a group`)
return entry.subgroup
}
/** Create an entry in the root group or a nested group. */
async create(options: Omit<EntryOptions, 'id'>, parent: string | null = null, position = Infinity) {
const group = this.resolveGroup(parent)
group.data.splice(position, 0, options as EntryOptions)
group.tree.write()
return group.create(options)
}
/** Stop and remove an entry from its parent group. */
remove(id: string) {
const entry = this.resolve(id)
entry.parent.remove(id)
entry.parent.tree.write()
}
/** Update an entry and optionally move it to another group. */
async update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number) {
const entry = this.resolve(id)
const source = entry.parent
if (parent !== undefined) {
const target = this.resolveGroup(parent)
source.unlink(entry.options)
target.data.splice(position ?? Infinity, 0, entry.options)
target.tree.write()
entry.parent = target
}
source.tree.write()
return entry.update(options, false, true)
}
/** Import a plugin module from a specifier or `cordis:` builtin. */
import(name: string, getOuterStack?: () => string[]) {
if (name.startsWith('cordis:')) {
return this.ctx.loader.builtins[name.slice(7)]
}
return composeError(async (info) => {
// ModuleJob.run
// onImport.tracePromise.__proto__
// internal.import
info.offset += 3
if (this.ctx.loader.internal) {
return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {})
} else if (name.startsWith('.')) {
return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)
} else {
return await import(/* @vite-ignore */name)
}
}, getOuterStack)
}
/** Persist current tree state. In-memory trees may implement this as a no-op. */
abstract write(): void
}

32
vendor/loader/src/config/utils.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
import { valueMap } from 'cosmokit'
// eslint-disable-next-line no-new-func
/** Evaluate a JavaScript expression against a loader context scope. */
export const evaluate = new Function('ctx', 'expr', `
with (ctx) {
return eval(expr)
}
`) as ((ctx: object, expr: string) => any)
/** Recursively replace YAML `!js` expression nodes with evaluated values. */
export function interpolate(ctx: object, value: any) {
if (isJsExpr(value)) {
return evaluate(ctx, value.__jsExpr)
} else if (!value || typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(item => interpolate(ctx, item))
} else {
return valueMap(value, item => interpolate(ctx, item))
}
}
/** Return true when a value is a serialized loader JavaScript expression. */
export function isJsExpr(value: any): value is JsExpr {
return value instanceof Object && '__jsExpr' in value
}
/** Serialized JavaScript expression produced by the include YAML tag. */
export interface JsExpr {
__jsExpr: string
}

185
vendor/loader/src/index.ts vendored Normal file
View File

@@ -0,0 +1,185 @@
import { Context, Inject, Service } from 'cordis'
import { defineProperty, Dict, isNullable } from 'cosmokit'
import { ModuleLoader } from './internal.ts'
import { Entry, EntryOptions } from './config/entry.ts'
import isolate from './config/isolate.ts'
import { EntryTree } from './config/tree.ts'
/** Re-export entry node APIs. */
export * from './config/entry.ts'
/** Re-export nested entry group APIs. */
export * from './config/group.ts'
/** Re-export service isolation helpers. */
export * from './config/isolate.ts'
/** Re-export entry tree persistence APIs. */
export * from './config/tree.ts'
/** Re-export loader config expression helpers. */
export * from './config/utils.ts'
/** Re-export Node internal module loader compatibility types. */
export * from './internal.ts'
declare module 'cordis' {
interface Events {
'exit'(signal: NodeJS.Signals): Promise<void>
'loader/config-update'(): void
'loader/entry-init'(entry: Entry): void
'loader/partial-dispose'(entry: Entry, legacy: Partial<EntryOptions>, active: boolean): void
'loader/patch-context'(entry: Entry, next: () => void): void
}
interface Context {
loader: Loader
}
interface EnvData {
startTime?: number
}
interface Fiber {
entry?: Entry
}
}
/** Loader config and dependency intercept namespace. */
export namespace Loader {
/** Root loader configuration. */
export interface Config {
/** Base URL used to resolve relative plugin specifiers and config paths. */
baseUrl?: string
}
/** Intercept config used when other plugins depend on `loader`. */
export interface Intercept {
/** Keep dependent plugins pending while loader entries are still loading. */
await?: boolean
}
}
/**
* Service that owns a loader entry tree and imports configured plugins.
*
* Subclasses provide persistence by implementing `write()` on `EntryTree`.
*/
export class Loader extends EntryTree {
declare [Service.config]: Loader.Intercept
public envData = process.env.CORDIS_SHARED
? JSON.parse(process.env.CORDIS_SHARED)
: { startTime: Date.now() }
public name = 'loader'
public internal = ModuleLoader.fromInternal()
public builtins: Dict<any> = Object.create(null)
constructor(ctx: Context, public config: Loader.Config = {}) {
super(ctx)
if (config.baseUrl) {
this.ctx.baseUrl = config.baseUrl
}
const self = this
defineProperty(this, Service.tracker, {
associate: 'loader',
property: 'ctx',
noShadow: true,
})
ctx.reflect.provide('loader', this, this[Service.check])
ctx.on('internal/update', function (config, noSave, next) {
if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next()
const unparse = this.runtime?.Config?.['simplify']
this.entry.options.config = unparse ? unparse(config) : config
this.entry.parent.tree.write()
return next()
}, { global: true, prepend: true })
ctx.on('internal/update', function (config, _, next) {
if (!this.entry || this.parent.fiber?.entry === this.entry) return next()
self.showLog(this.entry, 'reload')
return next()
}, { global: true })
ctx.on('internal/plugin', (fiber) => {
// 1. set `fiber.entry`
if (fiber.parent[Entry.key] && !fiber.entry) {
fiber.entry = fiber.parent[Entry.key]
// FIXME merge config
Inject.resolve(fiber.entry!.options.inject, fiber.inject)
}
// 2. handle self-dispose
// We only care about `ctx.fiber.dispose()`, so we need to filter out other cases.
// case 1: fiber is created
if (fiber.uid) return
// case 2: fiber is not tracked by loader
if (!fiber.entry) return
// case 3: fiber is a child plugin under the entry (not the entry's root fiber)
if (fiber.parent.fiber?.entry === fiber.entry) return
// case 4: fiber is disposed on behalf of plugin deletion (such as plugin hmr)
// self-dispose: ctx.fiber.dispose() -> fiber / runtime dispose -> delete(plugin)
// plugin hmr: delete(plugin) -> runtime dispose -> fiber dispose
if (!ctx.registry.has(fiber.runtime!.callback)) return
// case 5: the entry's tree is being disposed
if (!fiber.entry.parent.tree.ctx.fiber.uid) return
this.showLog(fiber.entry, 'unload')
// case 6: fiber is disposed by loader behavior
// such as inject checker, config file update, ancestor group disable
if (fiber.entry.disabled) return
fiber.entry.options.disabled = true
fiber.entry.parent.tree.write()
})
ctx.plugin(isolate)
}
write() {
// Loader's root tree is in-memory; writes are no-ops.
}
[Service.check]() {
const config: Loader.Intercept = Service.prototype[Service.resolveConfig].call(this)
if (config.await && this.getTasks().length) return false
return true
}
showLog(entry: Entry, type: string) {
if (entry.options.group || !entry.parent.tree.enableLogs) return
this.ctx.root.logger?.('loader').info('%s plugin %C', type, entry.options.name)
}
/** Return the loader entry id that owns `fiber`, if any. */
locate(fiber = this.ctx.fiber) {
while (1) {
if (fiber.entry) return fiber.entry.id
const next = fiber.parent.fiber
if (fiber === next) return
fiber = next
}
}
/** Hook for hosts that can restart the process on full-reload requests. */
exit() {
}
/** Normalize ESM/CJS/default export shapes before applying a plugin. */
unwrapExports(exports: any) {
if (isNullable(exports)) return exports
exports = exports.default ?? exports
// https://github.com/evanw/esbuild/issues/2623
// https://esbuild.github.io/content-types/#default-interop
if (!exports.__esModule) return exports
return exports.default ?? exports
}
}
export default Loader

122
vendor/loader/src/internal.ts vendored Normal file
View File

@@ -0,0 +1,122 @@
import { createRequire, LoadHookContext } from 'node:module'
import { Dict } from 'cosmokit'
/** Node internal module format names handled by loader hooks. */
export type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
/** Source payload accepted by Node internal module load hooks. */
export type ModuleSource = string | ArrayBuffer
/** Result returned by a Node internal resolve hook. */
export interface ResolveResult {
format: ModuleFormat
url: string
}
/** Result returned by a Node internal load hook. */
export interface LoadResult {
format: ModuleFormat
source?: ModuleSource
}
type LoadCacheData = ModuleJob // | Function
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_map.js */
interface LoadCache extends Omit<Map<string, Dict<LoadCacheData>>, 'get' | 'set' | 'has'> {
get(url: string, type?: string): LoadCacheData | undefined
set(url: string, type?: string, job?: LoadCacheData): this
has(url: string, type?: string): boolean
}
/** Minimal Node internal ModuleWrap surface used by HMR helpers. */
export interface ModuleWrap {
url: string
getNamespace(): any
}
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_job.js */
export interface ModuleJob {
url: string
loader: ModuleLoader
module?: ModuleWrap
importAttributes: ImportAttributes
linked: Promise<ModuleJob[]>
instantiate(): Promise<void>
run(): Promise<{ module: ModuleWrap }>
}
/**
* Node 22/23 ModuleLoader interface.
*
* Key methods:
* - getModuleJobForImport(specifier, parentURL, importAttributes)
* - resolve(specifier, parentURL, importAttributes) → Promise<ResolveResult>
* - resolveSync(specifier, parentURL, importAttributes) → ResolveResult
*/
export interface ModuleLoaderV1 {
version: 'v1'
loadCache: LoadCache
import(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<any>
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[]): void
getModuleJobForImport(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ModuleJob>
resolve(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ResolveResult>
resolveSync(specifier: string, parentURL: string, importAttributes: ImportAttributes): ResolveResult
load(specifier: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
}
/** Node 24+ module request object. */
export interface ModuleRequest {
specifier: string
attributes?: ImportAttributes
phase?: ModulePhase
}
/** @see https://github.com/nodejs/node/blob/main/src/module_wrap.h */
export const enum ModulePhase {
Source = 1,
Evaluation = 2,
}
/** Opaque Node internal module request type marker. */
export type ModuleRequestType = unknown // internal symbols
/**
* Node 24+ ModuleLoader interface.
*
* Breaking changes from v1:
* - getModuleJobForImport removed → getOrCreateModuleJob(parentURL, request, requestType)
* - resolve removed (became private #resolve) → resolveSync(parentURL, request)
* - Parameter order reversed for resolveSync, request object { specifier, attributes }
* - LoadCache became typed Map<url, { [type]: ModuleJob }> with delete only setting undefined
*/
export interface ModuleLoaderV2 {
version: 'v2'
loadCache: LoadCache
import(specifier: string, parentURL: string, importAttributes: ImportAttributes, phase?: ModulePhase, isEntryPoint?: boolean): Promise<any>
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[], isInternal?: boolean): void
getOrCreateModuleJob(parentURL: string, request: ModuleRequest, requestType?: ModuleRequestType): Promise<ModuleJob>
resolveSync(parentURL: string, request: ModuleRequest): ResolveResult
load(url: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
}
/** Supported Node internal ESM loader shapes. */
export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2
/** Helpers for locating the current Node internal module loader. */
export namespace ModuleLoader {
let _cachedLoader: ModuleLoader | undefined
export function fromInternal(): ModuleLoader | undefined {
if (!process.execArgv.includes('--expose-internals')) return
if (_cachedLoader) return _cachedLoader
const require = createRequire(import.meta.url)
const [major] = process.versions.node.split('.').map(Number)
if (major >= 24) {
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
return _cachedLoader = Object.assign(raw, { version: 'v2' })
} else if (major >= 22) {
const raw = require('internal/modules/esm/loader').getOrInitializeCascadedLoader()
return _cachedLoader = Object.assign(raw, { version: 'v1' })
}
}
}