返回 CodeWhale
index.ts
根目录 / crates / tui / extension-host / src / dsh / upstream / loader / src / index.ts
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
204 lines TYPESCRIPT