import { defineProperty, Dict } from 'cosmokit' import { Context } from './context.ts' import { ForkScope, MainScope } from './scope.ts' import { resolveConfig, symbols, withProps } from './utils.ts' function isApplicable(object: Plugin) { return object && typeof object === 'object' && typeof object.apply === 'function' } export type Inject = string[] | Dict export function Inject(inject: Inject) { return function (value: any, ctx: ClassDecoratorContext | ClassMethodDecoratorContext) { if (ctx.kind === 'class') { value.inject = inject } else if (ctx.kind === 'method') { ctx.addInitializer(function () { const property = this[symbols.tracker]?.property if (!property) throw new Error('missing context tracker') ;(this[property] as Context).inject(inject, (ctx) => { value.call(withProps(this, { [property]: ctx })) }) }) } else { throw new Error('@Inject can only be used on class or class methods') } } } export namespace Inject { export interface Meta { required: boolean } export function resolve(inject: Inject | null | undefined) { if (!inject) return {} if (Array.isArray(inject)) { return Object.fromEntries(inject.map(name => [name, { required: true }])) } const { required, optional, ...rest } = inject if (Array.isArray(required)) { Object.assign(rest, Object.fromEntries(required.map(name => [name, { required: true }]))) } if (Array.isArray(optional)) { Object.assign(rest, Object.fromEntries(optional.map(name => [name, { required: false }]))) } return rest } } export type Plugin = | Plugin.Function | Plugin.Constructor | Plugin.Object export namespace Plugin { export interface Base { name?: string reactive?: boolean reusable?: boolean Config?: (config: any) => T inject?: Inject intercept?: Dict } export interface Transform { schema?: true Config: (config: S) => T } export interface Function extends Base { (ctx: C, config: T): void } export interface Constructor extends Base { new (ctx: C, config: T): void } export interface Object extends Base { apply: (ctx: C, config: T) => void } } export type Spread = undefined extends T ? [config?: T] : [config: T] declare module './context.ts' { export interface Context { /** @deprecated use `ctx.inject()` instead */ using(deps: Inject, callback: Plugin.Function): ForkScope inject(deps: Inject, callback: Plugin.Function): ForkScope plugin(plugin: Plugin.Function & Plugin.Transform, ...args: Spread): ForkScope plugin(plugin: Plugin.Constructor & Plugin.Transform, ...args: Spread): ForkScope plugin(plugin: Plugin.Object & Plugin.Transform, ...args: Spread): ForkScope plugin(plugin: Plugin.Function, ...args: Spread): ForkScope plugin(plugin: Plugin.Constructor, ...args: Spread): ForkScope plugin(plugin: Plugin.Object, ...args: Spread): ForkScope } } class Registry { private _counter = 0 private _internal = new Map>() protected context: Context constructor(public ctx: C, config: any) { defineProperty(this, symbols.tracker, { associate: 'registry', property: 'ctx', }) this.context = ctx const runtime = new MainScope(ctx, null!, config) ctx.scope = runtime runtime.ctx = ctx this.set(null!, runtime) } get counter() { return ++this._counter } get size() { return this._internal.size } resolve(plugin: Plugin, assert = false): Function | undefined { // Allow `null` as a special case. if (plugin === null) return plugin if (typeof plugin === 'function') return plugin if (isApplicable(plugin)) return plugin.apply if (assert) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin) } get(plugin: Plugin) { const key = this.resolve(plugin) return key && this._internal.get(key) } has(plugin: Plugin) { const key = this.resolve(plugin) return !!key && this._internal.has(key) } set(plugin: Plugin, state: MainScope) { const key = this.resolve(plugin) this._internal.set(key!, state) } delete(plugin: Plugin) { const key = this.resolve(plugin) const runtime = key && this._internal.get(key) if (!runtime) return this._internal.delete(key) runtime.dispose() return runtime } keys() { return this._internal.keys() } values() { return this._internal.values() } entries() { return this._internal.entries() } forEach(callback: (value: MainScope, key: Function, map: Map>) => void) { return this._internal.forEach(callback) } using(inject: Inject, callback: Plugin.Function) { return this.inject(inject, callback) } inject(inject: Inject, callback: Plugin.Function) { return this.plugin({ inject, apply: callback, name: callback.name }) } plugin(plugin: Plugin, config?: any, error?: any) { // check if it's a valid plugin this.resolve(plugin, true) this.ctx.scope.assertActive() // resolve plugin config if (!error) { try { config = resolveConfig(plugin, config) } catch (reason) { this.context.emit(this.ctx, 'internal/error', reason) error = reason config = null } } // check duplication let runtime = this.get(plugin) if (runtime) { if (!runtime.isForkable) { this.context.emit(this.ctx, 'internal/warning', new Error(`duplicate plugin detected: ${plugin.name}`)) } return runtime.fork(this.ctx, config, error) } runtime = new MainScope(this.ctx, plugin, config, error) this.set(plugin, runtime) return runtime.fork(this.ctx, config, error) } } export default Registry