| 1 | import { createRequire, type LoadHookContext } from 'node:module' |
| 2 | import type { Dict } from '@deepseek-ai/cosmokit' |
| 3 | |
| 4 | /** Node internal module format names handled by loader hooks. */ |
| 5 | export type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm' |
| 6 | /** Source payload accepted by Node internal module load hooks. */ |
| 7 | export type ModuleSource = string | ArrayBuffer |
| 8 | |
| 9 | /** Result returned by a Node internal resolve hook. */ |
| 10 | export interface ResolveResult { |
| 11 | format: ModuleFormat |
| 12 | url: string |
| 13 | } |
| 14 | |
| 15 | /** Result returned by a Node internal load hook. */ |
| 16 | export interface LoadResult { |
| 17 | format: ModuleFormat |
| 18 | source?: ModuleSource |
| 19 | } |
| 20 | |
| 21 | type LoadCacheData = ModuleJob // | Function |
| 22 | |
| 23 | /** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_map.js */ |
| 24 | interface LoadCache extends Omit<Map<string, Dict<LoadCacheData>>, 'get' | 'set' | 'has'> { |
| 25 | get(url: string, type?: string): LoadCacheData | undefined |
| 26 | set(url: string, type?: string, job?: LoadCacheData): this |
| 27 | has(url: string, type?: string): boolean |
| 28 | } |
| 29 | |
| 30 | /** Minimal Node internal ModuleWrap surface used by HMR helpers. */ |
| 31 | export interface ModuleWrap { |
| 32 | url: string |
| 33 | getNamespace(): any |
| 34 | } |
| 35 | |
| 36 | /** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_job.js */ |
| 37 | export interface ModuleJob { |
| 38 | url: string |
| 39 | loader: ModuleLoader |
| 40 | module?: ModuleWrap |
| 41 | importAttributes: ImportAttributes |
| 42 | linked: Promise<ModuleJob[]> |
| 43 | instantiate(): Promise<void> |
| 44 | run(): Promise<{ module: ModuleWrap }> |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Node 22/23 ModuleLoader interface. |
| 49 | * |
| 50 | * Key methods: |
| 51 | * - getModuleJobForImport(specifier, parentURL, importAttributes) |
| 52 | * - resolve(specifier, parentURL, importAttributes) → Promise<ResolveResult> |
| 53 | * - resolveSync(specifier, parentURL, importAttributes) → ResolveResult |
| 54 | */ |
| 55 | export interface ModuleLoaderV1 { |
| 56 | version: 'v1' |
| 57 | loadCache: LoadCache |
| 58 | import(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<any> |
| 59 | register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[]): void |
| 60 | getModuleJobForImport(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ModuleJob> |
| 61 | resolve(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ResolveResult> |
| 62 | resolveSync(specifier: string, parentURL: string, importAttributes: ImportAttributes): ResolveResult |
| 63 | load(specifier: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult> |
| 64 | } |
| 65 | |
| 66 | /** Node 24+ module request object. */ |
| 67 | export interface ModuleRequest { |
| 68 | specifier: string |
| 69 | attributes?: ImportAttributes |
| 70 | phase?: ModulePhase |
| 71 | } |
| 72 | |
| 73 | /** @see https://github.com/nodejs/node/blob/main/src/module_wrap.h */ |
| 74 | export const enum ModulePhase { |
| 75 | Source = 1, |
| 76 | Evaluation = 2, |
| 77 | } |
| 78 | |
| 79 | /** Opaque Node internal module request type marker. */ |
| 80 | export type ModuleRequestType = unknown // internal symbols |
| 81 | |
| 82 | /** |
| 83 | * Node 24+ ModuleLoader interface. |
| 84 | * |
| 85 | * Breaking changes from v1: |
| 86 | * - getModuleJobForImport removed → getOrCreateModuleJob(parentURL, request, requestType) |
| 87 | * - resolve removed (became private #resolve) → resolveSync(parentURL, request) |
| 88 | * - Parameter order reversed for resolveSync, request object { specifier, attributes } |
| 89 | * - LoadCache became typed Map<url, { [type]: ModuleJob }> with delete only setting undefined |
| 90 | */ |
| 91 | export interface ModuleLoaderV2 { |
| 92 | version: 'v2' |
| 93 | loadCache: LoadCache |
| 94 | import(specifier: string, parentURL: string, importAttributes: ImportAttributes, phase?: ModulePhase, isEntryPoint?: boolean): Promise<any> |
| 95 | register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[], isInternal?: boolean): void |
| 96 | getOrCreateModuleJob(parentURL: string, request: ModuleRequest, requestType?: ModuleRequestType): Promise<ModuleJob> |
| 97 | resolveSync(parentURL: string, request: ModuleRequest): ResolveResult |
| 98 | load(url: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult> |
| 99 | } |
| 100 | |
| 101 | /** Supported Node internal ESM loader shapes. */ |
| 102 | export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2 |
| 103 | |
| 104 | /** Helpers for locating the current Node internal module loader. */ |
| 105 | export namespace ModuleLoader { |
| 106 | let _cachedLoader: ModuleLoader | undefined |
| 107 | |
| 108 | function requireInternal(id: string): any { |
| 109 | const require = createRequire(import.meta.url) |
| 110 | if (process.execArgv.includes('--expose-internals')) { |
| 111 | try { |
| 112 | return require(id) |
| 113 | } catch {} |
| 114 | } |
| 115 | try { |
| 116 | return require('node-addon-require-builtin').requireBuiltin(id) |
| 117 | } catch {} |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * Locate and classify the running Node internal module loader. |
| 122 | * |
| 123 | * The shape is decided by which module-job API the loader owns, never by the |
| 124 | * Node version: v2 landed in 24.12.0, so a major-version test mistags every |
| 125 | * 24.0–24.11.1 loader as v2 and makes consumers call `resolveSync` with |
| 126 | * reversed parameters. Arity is not usable either — `resolveSync` reports 2 |
| 127 | * under both shapes. A loader owning neither API is left unclassified rather |
| 128 | * than guessed, so consumers take their documented no-internals path. |
| 129 | * @returns the classified loader, or `undefined` when none is reachable or its shape is unknown. |
| 130 | */ |
| 131 | export function fromInternal(): ModuleLoader | undefined { |
| 132 | if (_cachedLoader) return _cachedLoader |
| 133 | const [major] = process.versions.node.split('.').map(Number) |
| 134 | if (major < 22) return |
| 135 | |
| 136 | const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() |
| 137 | if (!raw) return |
| 138 | const version = typeof raw.getOrCreateModuleJob === 'function' |
| 139 | ? 'v2' |
| 140 | : typeof raw.getModuleJobForImport === 'function' ? 'v1' : undefined |
| 141 | if (!version) return |
| 142 | return _cachedLoader = Object.assign(raw, { version }) |
| 143 | } |
| 144 | } |
| 145 |