index.ts 5.7 KB

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