diff --git a/packages/core/README.md b/packages/core/README.md index 738c951..8b42e30 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -204,7 +204,7 @@ ctx.plugin(Bar) #### Unload a plugin [↑](#contents) -`ctx.plugin()` returns a `ForkScope` instance. To unload a plugin, we can use the `dispose()` method of it: +`ctx.plugin()` returns a `EffectScope` instance. To unload a plugin, we can use the `dispose()` method of it: ```ts // load a plugin @@ -218,7 +218,7 @@ const fork = ctx.plugin((ctx) => { fork.dispose() ``` -Some plugins can be loaded multiple times. To unload every fork of a plugin without access to the `ForkScope` instance, we can use `ctx.registry`: +Some plugins can be loaded multiple times. To unload every fork of a plugin without access to the `EffectScope` instance, we can use `ctx.registry`: ```ts // remove all forks of the plugin @@ -666,7 +666,7 @@ If any listener is fulfilled with a value other than `false`, `null` or `undefin - plugin: `object` the plugin to apply - config: `object` config for the plugin -- returns: `ForkScope` +- returns: `EffectScope` Apply a plugin. @@ -716,7 +716,7 @@ The plugin runtime associated with the effect scope. If the scope is a runtime, #### scope.dispose() -### ForkScope +### EffectScope ### MainScope @@ -730,7 +730,7 @@ It can be accessed via `ctx.scope.main` or passed-in in some events. #### runtime.children -- type: [`ForkScope[]`](#forkscope) +- type: [`EffectScope[]`](#forkscope) #### runtime.isForkable @@ -777,11 +777,11 @@ See: [Reusable plugins](#reusable-plugins-) - runtime: `MainScope` -#### internal/fork(fork) +#### internal/plugin(fork) -- fork: `ForkScope` +- fork: `EffectScope` #### internal/update(fork, config) -- fork: `ForkScope` +- fork: `EffectScope` - config: `any` diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index c95e83a..a7b5ac0 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -3,6 +3,7 @@ import Lifecycle from './events.ts' import ReflectService from './reflect.ts' import Registry from './registry.ts' import { getTraceable, resolveConfig, symbols } from './utils.ts' +import { EffectScope } from './index.ts' export { Lifecycle, ReflectService, Registry } @@ -93,6 +94,7 @@ export class Context { this[symbols.intercept] = Object.create(null) const self: Context = new Proxy(this, ReflectService.handler) self.root = self + this.scope = new EffectScope(this, config, () => {}) self.reflect = new ReflectService(self) self.registry = new Registry(self, config) self.lifecycle = new Lifecycle(self) @@ -116,11 +118,12 @@ export class Context { } get name() { - let runtime = this.runtime - while (runtime && !runtime.name) { - runtime = runtime.parent.runtime - } - return runtime?.name! + let scope = this.scope + do { + if (scope.meta?.name) return scope.meta.name + scope = scope.parent.scope + } while (scope) + return 'root' } get events() { diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index c54e36e..b1757c5 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -1,6 +1,6 @@ import { Awaitable, defineProperty, Promisify, remove } from 'cosmokit' import { Context } from './context.ts' -import { EffectScope, ForkScope, MainScope, ScopeStatus } from './scope.ts' +import { EffectScope, ScopeStatus } from './scope.ts' import { getTraceable, symbols } from './index.ts' import ReflectService from './reflect.ts' @@ -54,6 +54,7 @@ class Lifecycle { property: 'ctx', }) + // TODO: deprecate these events ctx.scope.leak(this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) { const method = options.prepend ? 'unshift' : 'push' if (name === 'ready') { @@ -63,9 +64,6 @@ class Lifecycle { this.scope.disposables[method](listener as any) defineProperty(listener, 'name', 'event ') return () => remove(this.scope.disposables, listener) - } else if (name === 'fork') { - this.scope.runtime.forkables[method](listener as any) - return this.scope.collect('event ', () => remove(this.scope.runtime.forkables, listener)) } })) @@ -79,10 +77,9 @@ class Lifecycle { // non-reusable plugin forks are not responsive to isolated service changes ctx.scope.leak(this.on('internal/before-service', function (this: Context, name) { - for (const runtime of this.registry.values()) { - if (!runtime.inject[name]?.required) continue - const scopes = runtime.isReusable ? runtime.children : [runtime] - for (const scope of scopes) { + for (const meta of this.registry.values()) { + if (!meta.inject[name]?.required) continue + for (const scope of meta.scopes) { if (!this[symbols.filter](scope.ctx)) continue scope.updateStatus() scope.reset() @@ -91,10 +88,9 @@ class Lifecycle { }, { global: true })) ctx.scope.leak(this.on('internal/service', function (this: Context, name) { - for (const runtime of this.registry.values()) { - if (!runtime.inject[name]?.required) continue - const scopes = runtime.isReusable ? runtime.children : [runtime] - for (const scope of scopes) { + for (const meta of this.registry.values()) { + if (!meta.inject[name]?.required) continue + for (const scope of meta.scopes) { if (!this[symbols.filter](scope.ctx)) continue scope.start() } @@ -114,8 +110,8 @@ class Lifecycle { // inject in ancestor contexts const checkInject = (scope: EffectScope, name: string) => { - if (!scope.runtime.plugin) return false - for (const key in scope.runtime.inject) { + if (!scope.meta) return false + for (const key in scope.meta.inject) { if (name === ReflectService.resolveInject(scope.ctx, key)[0]) return true } return checkInject(scope.parent.scope, name) @@ -218,19 +214,17 @@ class Lifecycle { export default Lifecycle export interface Events { - 'fork'(ctx: C, config: C['config']): void 'ready'(): Awaitable 'dispose'(): Awaitable - 'internal/fork'(fork: ForkScope): void - 'internal/runtime'(runtime: MainScope): void + 'internal/plugin'(fork: EffectScope): void 'internal/status'(scope: EffectScope, oldValue: ScopeStatus): void 'internal/info'(this: C, format: any, ...param: any[]): void 'internal/error'(this: C, format: any, ...param: any[]): void 'internal/warning'(this: C, format: any, ...param: any[]): void 'internal/before-service'(this: C, name: string, value: any): void 'internal/service'(this: C, name: string, value: any): void - 'internal/before-update'(fork: ForkScope, config: any): void - 'internal/update'(fork: ForkScope, oldConfig: any): void + 'internal/before-update'(fork: EffectScope, config: any): void + 'internal/update'(fork: EffectScope, oldConfig: any): void 'internal/inject'(this: C, name: string): boolean | undefined 'internal/listener'(this: C, name: string, listener: any, prepend: boolean): void 'internal/event'(type: 'emit' | 'parallel' | 'serial' | 'bail', name: string, args: any[], thisArg: any): void diff --git a/packages/core/src/reflect.ts b/packages/core/src/reflect.ts index 3c86e20..e13a1b9 100644 --- a/packages/core/src/reflect.ts +++ b/packages/core/src/reflect.ts @@ -37,7 +37,7 @@ class ReflectService { // Case 2: `$` or `_` prefix if (name[0] === '$' || name[0] === '_') return // Case 3: access directly from root - if (!ctx.runtime.plugin) return + if (!ctx.scope.meta) return // Case 4: custom inject checks if (ctx.bail(ctx, 'internal/inject', name)) return const lines = error.stack!.split('\n') diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index b6afe5d..7d0f073 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -1,7 +1,7 @@ import { defineProperty, Dict } from 'cosmokit' import { Context } from './context.ts' -import { ForkScope, MainScope, ScopeStatus } from './scope.ts' -import { resolveConfig, symbols, withProps } from './utils.ts' +import { EffectScope } from './scope.ts' +import { isConstructor, resolveConfig, symbols, withProps } from './utils.ts' function isApplicable(object: Plugin) { return object && typeof object === 'object' && typeof object.apply === 'function' @@ -10,11 +10,11 @@ function isApplicable(object: Plugin) { export type Inject = string[] | Dict export function Inject(inject: Inject) { - return function (value: any, ctx: ClassDecoratorContext | ClassMethodDecoratorContext) { - if (ctx.kind === 'class') { + return function (value: any, decorator: ClassDecoratorContext | ClassMethodDecoratorContext) { + if (decorator.kind === 'class') { value.inject = inject - } else if (ctx.kind === 'method') { - ctx.addInitializer(function () { + } else if (decorator.kind === 'method') { + decorator.addInitializer(function () { const property = this[symbols.tracker]?.property if (!property) throw new Error('missing context tracker') ;(this[symbols.initHooks] ??= []).push(() => { @@ -72,7 +72,7 @@ export namespace Plugin { } export interface Function extends Base { - (ctx: C, config: T): void + (ctx: C, config: T): void | Promise } export interface Constructor extends Base { @@ -80,7 +80,25 @@ export namespace Plugin { } export interface Object extends Base { - apply: (ctx: C, config: T) => void + apply: (ctx: C, config: T) => void | Promise + } + + export interface Meta { + name?: string + schema: any + inject: Dict + isReactive?: boolean + scopes: EffectScope[] + plugin: Plugin + } + + export function resolve(plugin: Plugin): Meta { + let name = plugin.name + if (name === 'apply') name = undefined + const schema = plugin['Config'] || plugin['schema'] + const inject = Inject.resolve(plugin['using'] || plugin['inject']) + const isReactive = plugin['reactive'] + return { name, schema, inject, isReactive, plugin, scopes: [] } } } @@ -89,20 +107,20 @@ 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 + using(deps: Inject, callback: Plugin.Function): EffectScope + inject(deps: Inject, callback: Plugin.Function): EffectScope + plugin(plugin: Plugin.Function & Plugin.Transform, ...args: Spread): EffectScope + plugin(plugin: Plugin.Constructor & Plugin.Transform, ...args: Spread): EffectScope + plugin(plugin: Plugin.Object & Plugin.Transform, ...args: Spread): EffectScope + plugin(plugin: Plugin.Function, ...args: Spread): EffectScope + plugin(plugin: Plugin.Constructor, ...args: Spread): EffectScope + plugin(plugin: Plugin.Object, ...args: Spread): EffectScope } } class Registry { private _counter = 0 - private _internal = new Map>() + private _internal = new Map>() protected context: Context constructor(public ctx: C, config: any) { @@ -112,11 +130,6 @@ class Registry { }) this.context = ctx - const runtime = new MainScope(ctx, null!, config) - ctx.scope = runtime - runtime.ctx = ctx - runtime.status = ScopeStatus.ACTIVE - this.set(null!, runtime) } get counter() { @@ -127,9 +140,9 @@ class Registry { return this._internal.size } + resolve(plugin: Plugin, assert: true): Function + resolve(plugin: Plugin, assert?: boolean): Function | undefined 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) @@ -145,18 +158,12 @@ class Registry { 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 + const meta = key && this._internal.get(key) + if (!meta) return this._internal.delete(key) - runtime.dispose() - return runtime + return meta } keys() { @@ -171,7 +178,7 @@ class Registry { return this._internal.entries() } - forEach(callback: (value: MainScope, key: Function, map: Map>) => void) { + forEach(callback: (value: Plugin.Meta, key: Function) => void) { return this._internal.forEach(callback) } @@ -185,7 +192,7 @@ class Registry { plugin(plugin: Plugin, config?: any, error?: any) { // check if it's a valid plugin - this.resolve(plugin, true) + const key = this.resolve(plugin, true) this.ctx.scope.assertActive() // resolve plugin config @@ -199,18 +206,44 @@ class Registry { } } - // 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}`)) + const meta = Plugin.resolve(plugin) + this._internal.set(key!, meta) + + const scope = new EffectScope(this.ctx, config, async (ctx, config) => { + if (typeof plugin !== 'function') { + await plugin.apply(ctx, config) + } else if (isConstructor(plugin)) { + // eslint-disable-next-line new-cap + const instance = new plugin(ctx, config) + for (const hook of instance?.[symbols.initHooks] ?? []) { + hook() + } + await instance?.[symbols.setup]?.() + } else { + await plugin(ctx, config) } - return runtime.fork(this.ctx, config, error) + }, meta) + if (!config) { + scope.cancel(error) + } else { + scope.start() } + return scope + } - runtime = new MainScope(this.ctx, plugin, config, error) - this.set(plugin, runtime) - return runtime.fork(this.ctx, config, error) + private async apply(plugin: Plugin, context: C, config: any) { + if (typeof plugin !== 'function') { + await plugin.apply(context, config) + } else if (isConstructor(plugin)) { + // eslint-disable-next-line new-cap + const instance = new plugin(context, config) + for (const hook of instance?.[symbols.initHooks] ?? []) { + hook() + } + await instance?.[symbols.setup]?.() + } else { + await plugin(context, config) + } } } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index eab9b9f..e46a9fc 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -1,12 +1,11 @@ -import { deepEqual, defineProperty, Dict, isNullable, remove } from 'cosmokit' +import { deepEqual, defineProperty, isNullable, remove } from 'cosmokit' import { Context } from './context.ts' -import { Inject, Plugin } from './registry.ts' -import { isConstructor, resolveConfig, symbols } from './utils.ts' +import { Plugin } from './registry.ts' +import { isConstructor, resolveConfig } from './utils.ts' declare module './context.ts' { export interface Context { scope: EffectScope - runtime: MainScope effect(callback: Callable): T effect(callback: Callable, config: R): T /** @deprecated use `ctx.effect()` instead */ @@ -55,13 +54,14 @@ export namespace CordisError { } as const } -export abstract class EffectScope { +export class EffectScope { public uid: number | null public ctx: C public disposables: Disposable[] = [] public error: any public status = ScopeStatus.PENDING public isActive = false + public dispose: () => void // Same as `this.ctx`, but with a more specific type. protected context: Context @@ -70,22 +70,42 @@ export abstract class EffectScope { protected tasks = new Set>() protected hasError = false - abstract runtime: MainScope - abstract activate(): Promise - abstract dispose(): boolean - abstract update(config: C['config'], forced?: boolean): void - - constructor(public parent: C, public config: C['config']) { - this.uid = parent.registry ? parent.registry.counter : 0 - this.ctx = this.context = parent.extend({ scope: this }) - this.proxy = new Proxy({}, { - get: (target, key, receiver) => Reflect.get(this.config, key, receiver), - ownKeys: (target) => Reflect.ownKeys(this.config), - }) + constructor(public parent: C, public config: C['config'], private apply: (ctx: C, config: any) => any, public meta?: Plugin.Meta) { + if (parent.scope) { + this.uid = parent.registry.counter + this.ctx = this.context = parent.extend({ scope: this }) + this.dispose = parent.scope.effect(() => { + this.meta?.scopes.push(this) + return () => { + this.uid = null + this.reset() + this.context.emit('internal/plugin', this) + remove(this.meta?.scopes!, this) + // TODO + // const result = remove(runtime.disposables, this.dispose) + // if (remove(runtime.children, this) && !runtime.children.length) { + // parent.registry.delete(runtime.plugin) + // } + } + }) + this.proxy = new Proxy({}, { + get: (target, key, receiver) => Reflect.get(this.config, key, receiver), + ownKeys: (target) => Reflect.ownKeys(this.config), + }) + this.context.emit('internal/plugin', this) + } else { + this.uid = 0 + this.ctx = this.context = parent + this.isActive = true + this.status = ScopeStatus.ACTIVE + this.dispose = () => { + throw new Error('cannot dispose root scope') + } + } } protected get _config() { - return this.runtime.isReactive ? this.proxy : this.config + return this.meta?.isReactive ? this.proxy : this.config } assertActive() { @@ -93,14 +113,16 @@ export abstract class EffectScope { throw new CordisError('INACTIVE_EFFECT') } - effect(callback: Callable, config?: any) { + effect(callback: Callable, config?: any): D { this.assertActive() const result = isConstructor(callback) // eslint-disable-next-line new-cap ? new callback(this.ctx, config) : callback(this.ctx, config) let disposed = false - const original = typeof result === 'function' ? result : result.dispose.bind(result) + const original: Disposable = typeof result === 'function' + ? result + : result.dispose.bind(result) const wrapped = (...args: []) => { // make sure the original callback is not called twice if (disposed) return @@ -109,7 +131,7 @@ export abstract class EffectScope { return original(...args) } this.disposables.push(wrapped) - if (typeof result === 'function') return wrapped + if (typeof result === 'function') return wrapped as D result.dispose = wrapped return result } @@ -155,13 +177,15 @@ export abstract class EffectScope { } get isReady() { - return Object.entries(this.runtime.inject).every(([name, inject]) => { + if (!this.meta) return true + return Object.entries(this.meta.inject).every(([name, inject]) => { return !inject.required || !isNullable(this.ctx.reflect.get(name, true)) }) } - leak(disposable: T) { - return defineProperty(disposable, Context.static, this) + leak(disposable: Disposable) { + if (remove(this.disposables, disposable)) return + throw new Error('unexpected disposable leak') } async reset() { @@ -174,18 +198,10 @@ export abstract class EffectScope { }) } - protected init(error?: any) { - if (!this.config) { - this.cancel(error) - } else { - this.start() - } - } - async start() { if (!this.isReady || this.isActive || this.uid === null) return true this.isActive = true - await this.activate() + await this.apply(this.ctx, this._config) this.updateStatus() } @@ -218,7 +234,7 @@ export abstract class EffectScope { const ignored = new Set() let hasUpdate = false, shouldRestart = false - let fallback: boolean | null = this.runtime.isReactive || null + let fallback: boolean | null = this.meta?.isReactive || null for (const { keys, callback, passive } of this.acceptors) { if (!keys) { fallback ||= !passive @@ -244,170 +260,22 @@ export abstract class EffectScope { } return [hasUpdate, shouldRestart] } -} - -export class ForkScope extends EffectScope { - dispose: () => boolean - - constructor(parent: Context, public runtime: MainScope, config: C['config'], error?: any) { - super(parent as C, config) - - this.dispose = runtime.leak(parent.scope.collect(`fork <${parent.runtime.name}>`, () => { - this.uid = null - this.reset() - this.context.emit('internal/fork', this) - const result = remove(runtime.disposables, this.dispose) - if (remove(runtime.children, this) && !runtime.children.length) { - parent.registry.delete(runtime.plugin) - } - return result - })) - - runtime.children.push(this) - runtime.disposables.push(this.dispose) - this.context.emit('internal/fork', this) - this.init(error) - } - - async activate() { - for (const fork of this.runtime.forkables) { - const value = await fork(this.context, this._config) - await value?.[symbols.setup]?.() - } - } update(config: any, forced?: boolean) { const oldConfig = this.config - const state: EffectScope = this.runtime.isForkable ? this : this.runtime - if (state.config !== oldConfig) return let resolved: any try { - resolved = resolveConfig(this.runtime.plugin, config) + resolved = resolveConfig(this.meta?.plugin, config) } catch (error) { this.context.emit('internal/error', error) return this.cancel(error) } - const [hasUpdate, shouldRestart] = state.checkUpdate(resolved, forced) + const [hasUpdate, shouldRestart] = this.checkUpdate(resolved, forced) this.context.emit('internal/before-update', this, config) this.config = resolved - state.config = resolved if (hasUpdate) { this.context.emit('internal/update', this, oldConfig) } - if (shouldRestart) state.restart() - } -} - -export class MainScope extends EffectScope { - public value: any - - runtime = this - schema: any - name?: string - inject: Dict = Object.create(null) - forkables: Function[] = [] - children: ForkScope[] = [] - isReusable?: boolean = false - isReactive?: boolean = false - - constructor(ctx: C, public plugin: Plugin, config: any, error?: any) { - super(ctx, config) - if (!plugin) { - this.name = 'root' - this.isActive = true - } else { - this.setup() - this.init(error) - } - } - - get isForkable() { - return this.forkables.length > 0 - } - - fork(parent: Context, config: any, error?: any) { - return new ForkScope(parent, this, config, error) - } - - dispose() { - this.uid = null - this.reset() - this.context.emit('internal/runtime', this) - return true - } - - private setup() { - const { name } = this.plugin - if (name && name !== 'apply') this.name = name - this.schema = this.plugin['Config'] || this.plugin['schema'] - this.inject = Inject.resolve(this.plugin['using'] || this.plugin['inject']) - this.isReusable = this.plugin['reusable'] - this.isReactive = this.plugin['reactive'] - this.context.emit('internal/runtime', this) - - if (this.isReusable) { - this.forkables.push(this.apply) - } - } - - private apply = (context: C, config: any) => { - if (typeof this.plugin !== 'function') { - return this.plugin.apply(context, config) - } else if (isConstructor(this.plugin)) { - // eslint-disable-next-line new-cap - const instance = new this.plugin(context, config) - if (instance['fork']) { - this.forkables.push(instance['fork'].bind(instance)) - } - return instance - } else { - return this.plugin(context, config) - } - } - - async reset() { - super.reset() - for (const fork of this.children) { - fork.reset() - } - } - - async activate() { - if (!this.isReusable && this.plugin) { - const value = await this.apply(this.ctx, this._config) - for (const hook of value?.[symbols.initHooks] ?? []) { - hook() - } - await value?.[symbols.setup]?.() - } - for (const fork of this.children) { - fork.start() - } - } - - update(config: C['config'], forced?: boolean) { - if (this.isForkable) { - const warning = new Error(`attempting to update forkable plugin "${this.plugin.name}", which may lead to unexpected behavior`) - this.context.emit(this.ctx, 'internal/warning', warning) - } - const oldConfig = this.config - let resolved: any - try { - resolved = resolveConfig(this.runtime.plugin || this.context.constructor, config) - } catch (error) { - this.context.emit('internal/error', error) - return this.cancel(error) - } - const [hasUpdate, shouldRestart] = this.checkUpdate(resolved, forced) - const state = this.children.find(fork => fork.config === oldConfig) - this.config = resolved - if (state) { - this.context.emit('internal/before-update', state, config) - state.config = resolved - if (hasUpdate) { - this.context.emit('internal/update', state, oldConfig) - } - } if (shouldRestart) this.restart() } } diff --git a/packages/core/src/service.ts b/packages/core/src/service.ts index 3fc814d..afcadc7 100644 --- a/packages/core/src/service.ts +++ b/packages/core/src/service.ts @@ -32,7 +32,6 @@ export abstract class Service { defineProperty(self, symbols.tracker, tracker) self.ctx.provide(name) - self.ctx.runtime.name = name self.ctx.set(name, self) self.ctx.on('dispose', () => self.stop()) diff --git a/packages/loader/package.json b/packages/loader/package.json index daf7a8b..933bc88 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -39,6 +39,7 @@ "devDependencies": { "@cordisjs/core": "^3.18.1", "@types/chai": "^4.3.16", + "@types/js-yaml": "^4.0.9", "chai": "^5.1.1" }, "peerDependencies": { diff --git a/packages/loader/src/config/entry.ts b/packages/loader/src/config/entry.ts index 388056b..ad4a5e8 100644 --- a/packages/loader/src/config/entry.ts +++ b/packages/loader/src/config/entry.ts @@ -1,4 +1,4 @@ -import { Context, ForkScope } from '@cordisjs/core' +import { Context, EffectScope } from '@cordisjs/core' import { isNullable } from 'cosmokit' import { Loader } from '../loader.ts' import { EntryGroup } from './group.ts' @@ -36,7 +36,7 @@ export class Entry { static readonly key = Symbol.for('cordis.entry') public ctx: C - public fork?: ForkScope + public fork?: EffectScope public suspend = false public parent!: EntryGroup public options!: EntryOptions @@ -101,7 +101,7 @@ export class Entry { if (this.fork && 'config' in options) { // step 2: update fork (when options.config is updated) this.suspend = true - const [config, error] = this._resolveConfig(this.fork.runtime.plugin) + const [config, error] = this._resolveConfig(this.fork.meta?.plugin) if (error) { this.fork.cancel(error) } else { diff --git a/packages/loader/src/loader.ts b/packages/loader/src/loader.ts index 85907f6..0742edc 100644 --- a/packages/loader/src/loader.ts +++ b/packages/loader/src/loader.ts @@ -1,4 +1,4 @@ -import { Context, EffectScope, Service } from '@cordisjs/core' +import { Context, Service } from '@cordisjs/core' import { Dict, isNullable } from 'cosmokit' import { ModuleLoader } from './internal.ts' import { Entry, EntryOptions, EntryUpdateMeta } from './config/entry.ts' @@ -34,10 +34,8 @@ declare module '@cordisjs/core' { startTime?: number } - // Theoretically, these properties will only appear on `ForkScope`. - // We define them directly on `EffectScope` for typing convenience. - interface EffectScope { - entry?: Entry + interface EffectScope { + entry?: Entry } } @@ -81,12 +79,12 @@ export abstract class Loader extends ImportTree ctx.on('internal/before-update', (fork, config) => { if (!fork.entry) return if (fork.entry.suspend) return fork.entry.suspend = false - const { schema } = fork.runtime + const schema = fork.meta?.schema fork.entry.options.config = schema ? schema.simplify(config) : config fork.entry.parent.tree.write() }) - ctx.on('internal/fork', (fork) => { + ctx.on('internal/plugin', (fork) => { // 1. set `fork.entry` if (fork.parent[Entry.key]) { fork.entry = fork.parent[Entry.key] @@ -105,7 +103,7 @@ export abstract class Loader extends ImportTree // case 3: fork is disposed on behalf of plugin deletion (such as plugin hmr) // self-dispose: ctx.scope.dispose() -> fork / runtime dispose -> delete(plugin) // plugin hmr: delete(plugin) -> runtime dispose -> fork dispose - if (!ctx.registry.has(fork.runtime.plugin)) return + if (!ctx.registry.has(fork.meta?.plugin!)) return fork.entry.fork = undefined fork.parent.emit('loader/entry-fork', fork.entry, 'unload') @@ -129,20 +127,13 @@ export abstract class Loader extends ImportTree } locate(ctx = this.ctx) { - return this._locate(ctx.scope).map(entry => entry.id) - } - - _locate(scope: EffectScope): Entry[] { - // root scope - if (!scope.runtime.plugin) return [] - - // runtime scope - if (scope.runtime === scope) { - return scope.runtime.children.flatMap(child => this._locate(child)) + let scope = ctx.scope + while (scope) { + if (scope.entry) return scope.entry.id + const next = scope.parent.scope + if (scope === next) return + scope = next } - - if (scope.entry) return [scope.entry] - return this._locate(scope.parent.scope) } exit() {} diff --git a/packages/loader/tests/utils.ts b/packages/loader/tests/utils.ts index 9c126ae..47b1bf8 100644 --- a/packages/loader/tests/utils.ts +++ b/packages/loader/tests/utils.ts @@ -1,5 +1,5 @@ import { Dict } from 'cosmokit' -import { Context, ForkScope, Plugin } from '@cordisjs/core' +import { Context, EffectScope, Plugin } from '@cordisjs/core' import { EntryOptions, Group, Loader, LoaderFile } from '../src' import { Mock, mock } from 'node:test' import { expect } from 'chai' @@ -9,7 +9,7 @@ declare module '../src/index.ts' { mock(name: string, plugin: F): Mock expectEnable(plugin: any, config?: any): void expectDisable(plugin: any): void - expectFork(id: string): ForkScope + expectFork(id: string): EffectScope } }