entry.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { Context, Fiber, Inject } from 'cordis'
  2. import { deepEqual, isNullable } from 'cosmokit'
  3. import { Loader } from '../index.ts'
  4. import { EntryGroup } from './group.ts'
  5. import { EntryTree } from './tree.ts'
  6. import { evaluate, interpolate } from './utils.ts'
  7. /** Serialized plugin entry options stored in loader config files. */
  8. export interface EntryOptions {
  9. /** Stable id inside the containing entry tree. */
  10. id: string
  11. /** Module specifier imported by the entry tree. */
  12. name: string
  13. /** Config passed to the plugin. */
  14. config?: any
  15. /** Marks this entry as a nested group. */
  16. group?: boolean | null
  17. /** Prevents this entry and descendants from running. */
  18. disabled?: boolean | null
  19. /** Required services or service intercept config for this entry. */
  20. inject?: Inject | null
  21. }
  22. function takeEntries(object: {}, keys: string[]) {
  23. const result: [string, any][] = []
  24. for (const key of keys) {
  25. if (!(key in object)) continue
  26. result.push([key, object[key]])
  27. delete object[key]
  28. }
  29. return result
  30. }
  31. function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
  32. const part1 = takeEntries(object, prepend)
  33. const part2 = takeEntries(object, append)
  34. const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
  35. return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
  36. }
  37. /** One configured plugin node inside an `EntryTree`. */
  38. export class Entry {
  39. static readonly key = Symbol.for('cordis.entry')
  40. public ctx: Context
  41. public fiber?: Fiber
  42. public parent!: EntryGroup
  43. // safety: call `entry.update()` immediately after creating an entry
  44. public options = {} as EntryOptions
  45. public subgroup?: EntryGroup
  46. public subtree?: EntryTree
  47. _initTask?: Promise<void>
  48. constructor(public loader: Loader) {
  49. this.ctx = loader.ctx.extend({ [Entry.key]: this })
  50. this.context.emit('loader/entry-init', this)
  51. }
  52. get context(): Context {
  53. return this.ctx
  54. }
  55. get id() {
  56. let id = this.options.id
  57. if (this.parent.tree.ctx.fiber.entry) {
  58. id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
  59. }
  60. return id
  61. }
  62. /** True when this entry or any owning parent entry is disabled. */
  63. get disabled() {
  64. // group is always enabled
  65. if (this.options.group) return false
  66. let entry: Entry | undefined = this
  67. do {
  68. if (entry.options.disabled) return true
  69. entry = entry.parent.ctx.fiber.entry
  70. } while (entry)
  71. return false
  72. }
  73. evaluate(expr: string) {
  74. return evaluate(this.ctx, expr)
  75. }
  76. _resolveConfig(plugin: any): [any, any?] {
  77. if (plugin[EntryGroup.key]) return this.options.config
  78. return interpolate(this.ctx, this.options.config)
  79. }
  80. private _patchContext(diff: string[]) {
  81. this.context.waterfall('loader/patch-context', this, () => {
  82. Object.setPrototypeOf(this.ctx, this.parent.ctx)
  83. if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
  84. this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
  85. }
  86. })
  87. }
  88. async refresh() {
  89. if (this.fiber) return
  90. if (this.disabled) return
  91. await this.init()
  92. }
  93. /** Merge new options, restart as needed, and persist through the parent tree. */
  94. async update(options: Partial<EntryOptions>, create = false, force = false) {
  95. const legacy = { ...this.options }
  96. // step 1: update options
  97. if (create) {
  98. this.options = options as EntryOptions
  99. } else {
  100. for (const [key, value] of Object.entries(options)) {
  101. if (isNullable(value)) {
  102. delete this.options[key]
  103. } else {
  104. this.options[key] = value
  105. }
  106. }
  107. }
  108. sortKeys(this.options)
  109. // step 2: execute
  110. if (this.disabled) {
  111. this.fiber?.dispose()
  112. return
  113. }
  114. // step 3: check if options are changed
  115. if (this.fiber?.uid) {
  116. const diff = Object
  117. .keys({ ...this.options, ...legacy })
  118. .filter(key => !deepEqual(this.options[key], legacy[key]))
  119. if (!diff.length && !force) return
  120. this.context.emit('loader/partial-dispose', this, legacy, true)
  121. this._patchContext(diff)
  122. } else {
  123. await this.init()
  124. }
  125. }
  126. getOuterStack = () => {
  127. let entry: Entry | undefined = this
  128. const result: string[] = []
  129. do {
  130. result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
  131. entry = entry.parent.ctx.fiber.entry
  132. } while (entry)
  133. return result
  134. }
  135. /** Import and start the configured plugin if it is not already running. */
  136. async init() {
  137. try {
  138. await (this._initTask ??= this._init())
  139. } finally {
  140. this._initTask = undefined
  141. }
  142. this.fiber?.await().finally(() => {
  143. if (this.loader.getTasks().length) return
  144. this.ctx.reflect.notify(['loader'])
  145. })
  146. }
  147. private async _init() {
  148. let exports: any
  149. try {
  150. exports = await this.parent.tree.import(this.options.name, this.getOuterStack)
  151. } catch (error) {
  152. this.ctx.logger.error(error)
  153. return
  154. } finally {
  155. this._initTask = undefined
  156. }
  157. const plugin = this.loader.unwrapExports(exports)
  158. this._patchContext([])
  159. this.loader.showLog(this, 'apply')
  160. this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
  161. }
  162. }