index.ts 6.5 KB

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