| 1 | import { Context, Inject, Service, type Fiber } from '@deepseek-ai/cordis' |
| 2 | import { defineProperty, isNullable, type Dict } from '@deepseek-ai/cosmokit' |
| 3 | import { ModuleLoader } from './internal.ts' |
| 4 | import { Entry, type EntryOptions } from './config/entry.ts' |
| 5 | import { EntryGroup } from './config/group.ts' |
| 6 | import isolate from './config/isolate.ts' |
| 7 | import { EntryTree } from './config/tree.ts' |
| 8 | import { interpolate } from './config/utils.ts' |
| 9 | import { FiberState } from '../../../fiber-state.ts' |
| 10 | |
| 11 | /** Re-export entry node APIs. */ |
| 12 | export * from './config/entry.ts' |
| 13 | /** Re-export nested entry group APIs. */ |
| 14 | export * from './config/group.ts' |
| 15 | /** Re-export service isolation helpers. */ |
| 16 | export * from './config/isolate.ts' |
| 17 | /** Re-export entry tree persistence APIs. */ |
| 18 | export * from './config/tree.ts' |
| 19 | /** Re-export loader config expression helpers. */ |
| 20 | export * from './config/utils.ts' |
| 21 | /** Re-export Node internal module loader compatibility types. */ |
| 22 | export * from './internal.ts' |
| 23 | |
| 24 | declare module '@deepseek-ai/cordis' { |
| 25 | interface Events { |
| 26 | 'exit'(signal: NodeJS.Signals): Promise<void> |
| 27 | 'loader/config-update'(): void |
| 28 | 'loader/entry-init'(entry: Entry): void |
| 29 | 'loader/partial-dispose'(entry: Entry, legacy: Partial<EntryOptions>, active: boolean): void |
| 30 | 'loader/patch-context'(entry: Entry, next: () => void): void |
| 31 | } |
| 32 | |
| 33 | interface Context { |
| 34 | loader: Loader |
| 35 | [Entry.key]?: Entry |
| 36 | } |
| 37 | |
| 38 | interface EnvData { |
| 39 | startTime?: number |
| 40 | } |
| 41 | |
| 42 | interface Fiber { |
| 43 | entry?: Entry |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /** Loader config and dependency intercept namespace. */ |
| 48 | export namespace Loader { |
| 49 | /** Root loader configuration. */ |
| 50 | export interface Config { |
| 51 | /** Base URL used to resolve relative plugin specifiers and config paths. */ |
| 52 | baseUrl?: string |
| 53 | } |
| 54 | |
| 55 | /** Intercept config used when other plugins depend on `loader`. */ |
| 56 | export interface Intercept { |
| 57 | /** Keep dependent plugins pending while loader entries are still loading. */ |
| 58 | await?: boolean |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Service that owns a loader entry tree and imports configured plugins. |
| 64 | * |
| 65 | * Subclasses provide persistence by implementing `write()` on `EntryTree`. |
| 66 | */ |
| 67 | export class Loader extends EntryTree { |
| 68 | declare [Service.config]: Loader.Intercept |
| 69 | |
| 70 | public envData = process.env.CORDIS_SHARED |
| 71 | ? JSON.parse(process.env.CORDIS_SHARED) |
| 72 | : { startTime: Date.now() } |
| 73 | |
| 74 | public name = 'loader' |
| 75 | // Codewhale host delta: native internals/addon probing is unavailable. |
| 76 | // EntryTree retains upstream's documented native ESM fallback. |
| 77 | public internal: ModuleLoader | undefined = undefined |
| 78 | |
| 79 | public builtins: Dict<any> = Object.create(null) |
| 80 | |
| 81 | constructor(ctx: Context, public config: Loader.Config = {}) { |
| 82 | super(ctx) |
| 83 | if (config.baseUrl) { |
| 84 | this.ctx.baseUrl = config.baseUrl |
| 85 | } |
| 86 | const self = this |
| 87 | |
| 88 | defineProperty(this, Service.tracker, { |
| 89 | associate: 'loader', |
| 90 | property: 'ctx', |
| 91 | noShadow: true, |
| 92 | }) |
| 93 | |
| 94 | ctx.reflect.provide('loader', this, this[Service.check]) |
| 95 | |
| 96 | ctx.on('internal/config', function (this: Fiber, _config, next) { |
| 97 | const config = next() |
| 98 | if (!this.entry || this.parent.fiber?.entry === this.entry) return config |
| 99 | // Tree carriers (Group, Include) keep their configs literal: their |
| 100 | // entry and patch lists hold other rows' configs, whose `!!js` |
| 101 | // expressions belong to those rows' own fibers. |
| 102 | const plugin = this.runtime?.callback as Record<PropertyKey, unknown> | undefined |
| 103 | if (plugin?.[EntryGroup.key]) return config |
| 104 | return interpolate(this.ctx, config) |
| 105 | }, { global: true }) |
| 106 | |
| 107 | ctx.on('internal/update', function (config, noSave, next) { |
| 108 | if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next() |
| 109 | const unparse = (this.runtime?.Config as { simplify?: (value: any) => any } | undefined)?.simplify |
| 110 | this.entry.options.config = unparse ? unparse(config) : config |
| 111 | this.entry.parent.tree.write() |
| 112 | return next() |
| 113 | }, { global: true, prepend: true }) |
| 114 | |
| 115 | ctx.on('internal/update', function (config, _, next) { |
| 116 | if (!this.entry || this.parent.fiber?.entry === this.entry) return next() |
| 117 | self.showLog(this.entry, 'reload') |
| 118 | return next() |
| 119 | }, { global: true }) |
| 120 | |
| 121 | ctx.on('internal/plugin', (fiber) => { |
| 122 | // 1. set `fiber.entry` |
| 123 | if (fiber.parent[Entry.key] && !fiber.entry) { |
| 124 | fiber.entry = fiber.parent[Entry.key] |
| 125 | // FIXME merge config |
| 126 | Inject.resolve(fiber.entry!.options.inject, fiber.inject) |
| 127 | } |
| 128 | |
| 129 | // 2. handle self-dispose |
| 130 | // We only care about `ctx.fiber.dispose()`, so we need to filter out other cases. |
| 131 | |
| 132 | // case 1: fiber is created |
| 133 | if (fiber.uid) return |
| 134 | |
| 135 | // case 2: fiber is not tracked by loader |
| 136 | if (!fiber.entry) return |
| 137 | |
| 138 | // case 3: fiber is a child plugin under the entry (not the entry's root fiber) |
| 139 | if (fiber.parent.fiber?.entry === fiber.entry) return |
| 140 | |
| 141 | // case 4: fiber is disposed on behalf of plugin deletion (such as plugin hmr) |
| 142 | // self-dispose: ctx.fiber.dispose() -> fiber / runtime dispose -> delete(plugin) |
| 143 | // plugin hmr: delete(plugin) -> runtime dispose -> fiber dispose |
| 144 | if (!ctx.registry.has(fiber.runtime!.callback)) return |
| 145 | |
| 146 | // case 5: the entry's tree is being disposed |
| 147 | const treeOwner = fiber.entry.parent.tree.ctx.fiber |
| 148 | if (!treeOwner.uid || treeOwner.state === FiberState.UNLOADING) return |
| 149 | |
| 150 | this.showLog(fiber.entry, 'unload') |
| 151 | |
| 152 | // case 6: fiber is disposed by loader behavior |
| 153 | // such as inject checker, config file update, ancestor group disable |
| 154 | if (fiber.entry.disabled) return |
| 155 | |
| 156 | fiber.entry.options.disabled = true |
| 157 | fiber.entry.parent.tree.write() |
| 158 | }) |
| 159 | |
| 160 | ctx.plugin(isolate) |
| 161 | } |
| 162 | |
| 163 | write() { |
| 164 | // Loader's root tree is in-memory; writes are no-ops. |
| 165 | } |
| 166 | |
| 167 | [Service.check]() { |
| 168 | const config: Loader.Intercept = Service.prototype[Service.resolveConfig].call(this) |
| 169 | if (config.await && this.getTasks().length) return false |
| 170 | return true |
| 171 | } |
| 172 | |
| 173 | showLog(entry: Entry, type: string) { |
| 174 | if (entry.options.group || !entry.parent.tree.enableLogs) return |
| 175 | this.ctx.root.logger?.('loader').info('%s plugin %C', type, entry.options.name) |
| 176 | } |
| 177 | |
| 178 | /** Return the loader entry id that owns `fiber`, if any. */ |
| 179 | locate(fiber = this.ctx.fiber) { |
| 180 | while (1) { |
| 181 | if (fiber.entry) return fiber.entry.id |
| 182 | const next = fiber.parent.fiber |
| 183 | if (fiber === next) return |
| 184 | fiber = next |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /** Hook for hosts that can restart the process on full-reload requests. */ |
| 189 | exit() { |
| 190 | } |
| 191 | |
| 192 | /** Normalize ESM/CJS/default export shapes before applying a plugin. */ |
| 193 | unwrapExports(exports: any) { |
| 194 | if (isNullable(exports)) return exports |
| 195 | exports = exports.default ?? exports |
| 196 | // https://github.com/evanw/esbuild/issues/2623 |
| 197 | // https://esbuild.github.io/content-types/#default-interop |
| 198 | if (!exports.__esModule) return exports |
| 199 | return exports.default ?? exports |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | export default Loader |
| 204 |