| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- import { Context, Fiber, Inject } from 'cordis'
- import { deepEqual, isNullable } from 'cosmokit'
- import { Loader } from '../index.ts'
- import { EntryGroup } from './group.ts'
- import { EntryTree } from './tree.ts'
- import { evaluate, interpolate } from './utils.ts'
- /** Serialized plugin entry options stored in loader config files. */
- export interface EntryOptions {
- /** Stable id inside the containing entry tree. */
- id: string
- /** Module specifier imported by the entry tree. */
- name: string
- /** Config passed to the plugin. */
- config?: any
- /** Marks this entry as a nested group. */
- group?: boolean | null
- /** Prevents this entry and descendants from running. */
- disabled?: boolean | null
- /** Required services or service intercept config for this entry. */
- inject?: Inject | null
- }
- function takeEntries(object: {}, keys: string[]) {
- const result: [string, any][] = []
- for (const key of keys) {
- if (!(key in object)) continue
- result.push([key, object[key]])
- delete object[key]
- }
- return result
- }
- function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
- const part1 = takeEntries(object, prepend)
- const part2 = takeEntries(object, append)
- const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
- return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
- }
- /** One configured plugin node inside an `EntryTree`. */
- export class Entry {
- static readonly key = Symbol.for('cordis.entry')
- public ctx: Context
- public fiber?: Fiber
- public parent!: EntryGroup
- // safety: call `entry.update()` immediately after creating an entry
- public options = {} as EntryOptions
- public subgroup?: EntryGroup
- public subtree?: EntryTree
- _initTask?: Promise<void>
- constructor(public loader: Loader) {
- this.ctx = loader.ctx.extend({ [Entry.key]: this })
- this.context.emit('loader/entry-init', this)
- }
- get context(): Context {
- return this.ctx
- }
- get id() {
- let id = this.options.id
- if (this.parent.tree.ctx.fiber.entry) {
- id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
- }
- return id
- }
- /** True when this entry or any owning parent entry is disabled. */
- get disabled() {
- // group is always enabled
- if (this.options.group) return false
- let entry: Entry | undefined = this
- do {
- if (entry.options.disabled) return true
- entry = entry.parent.ctx.fiber.entry
- } while (entry)
- return false
- }
- evaluate(expr: string) {
- return evaluate(this.ctx, expr)
- }
- _resolveConfig(plugin: any): [any, any?] {
- if (plugin[EntryGroup.key]) return this.options.config
- return interpolate(this.ctx, this.options.config)
- }
- private _patchContext(diff: string[]) {
- this.context.waterfall('loader/patch-context', this, () => {
- Object.setPrototypeOf(this.ctx, this.parent.ctx)
- if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
- this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
- }
- })
- }
- async refresh() {
- if (this.fiber) return
- if (this.disabled) return
- await this.init()
- }
- /** Merge new options, restart as needed, and persist through the parent tree. */
- async update(options: Partial<EntryOptions>, create = false, force = false) {
- const legacy = { ...this.options }
- // step 1: update options
- if (create) {
- this.options = options as EntryOptions
- } else {
- for (const [key, value] of Object.entries(options)) {
- if (isNullable(value)) {
- delete this.options[key]
- } else {
- this.options[key] = value
- }
- }
- }
- sortKeys(this.options)
- // step 2: execute
- if (this.disabled) {
- this.fiber?.dispose()
- return
- }
- // step 3: check if options are changed
- if (this.fiber?.uid) {
- const diff = Object
- .keys({ ...this.options, ...legacy })
- .filter(key => !deepEqual(this.options[key], legacy[key]))
- if (!diff.length && !force) return
- this.context.emit('loader/partial-dispose', this, legacy, true)
- this._patchContext(diff)
- } else {
- await this.init()
- }
- }
- getOuterStack = () => {
- let entry: Entry | undefined = this
- const result: string[] = []
- do {
- result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
- entry = entry.parent.ctx.fiber.entry
- } while (entry)
- return result
- }
- /** Import and start the configured plugin if it is not already running. */
- async init() {
- try {
- await (this._initTask ??= this._init())
- } finally {
- this._initTask = undefined
- }
- this.fiber?.await().finally(() => {
- if (this.loader.getTasks().length) return
- this.ctx.reflect.notify(['loader'])
- })
- }
- private async _init() {
- let exports: any
- try {
- exports = await this.parent.tree.import(this.options.name, this.getOuterStack)
- } catch (error) {
- this.ctx.logger.error(error)
- return
- } finally {
- this._initTask = undefined
- }
- const plugin = this.loader.unwrapExports(exports)
- this._patchContext([])
- this.loader.showLog(this, 'apply')
- this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
- }
- }
|