From b21ada02b73764f3a24930e2d78cef8c062db83c Mon Sep 17 00:00:00 2001 From: Shigma Date: Wed, 25 Oct 2023 00:28:48 +0800 Subject: [PATCH] feat: extended builtin service --- package.json | 2 +- src/context.ts | 42 +++++++++++++++-------------- tests/plugin.spec.ts | 1 - tests/service.spec.ts | 62 ++++++++++++++++++++++++++----------------- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 18c3312..15952f6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cordis", "description": "AOP Framework for Modern JavaScript Applications", - "version": "2.10.2", + "version": "3.0.0", "sideEffects": false, "main": "lib/index.cjs", "module": "lib/index.mjs", diff --git a/src/context.ts b/src/context.ts index 543132e..b3a8fed 100644 --- a/src/context.ts +++ b/src/context.ts @@ -21,6 +21,7 @@ export namespace Context { export interface Service { type: 'service' key: symbol + builtin?: boolean prototype?: {} } @@ -43,15 +44,15 @@ export interface Context { } export class Context { - static readonly config = Symbol('config') - static readonly events = Symbol('events') - static readonly static = Symbol('static') - static readonly filter = Symbol('filter') - static readonly source = Symbol('source') - static readonly expose = Symbol('expose') - static readonly shadow = Symbol('shadow') - static readonly current = Symbol('current') - static readonly internal = Symbol('internal') + static readonly config = Symbol.for('cordis.config') + static readonly events = Symbol.for('cordis.events') + static readonly static = Symbol.for('cordis.static') + static readonly filter = Symbol.for('cordis.filter') + static readonly source = Symbol.for('cordis.source') + static readonly expose = Symbol.for('cordis.expose') + static readonly shadow = Symbol.for('cordis.shadow') + static readonly current = Symbol.for('cordis.current') + static readonly internal = Symbol.for('cordis.internal') private static ensureInternal(): Context[typeof Context.internal] { const ctx = this.prototype || this @@ -90,14 +91,14 @@ export class Context { if (typeof name !== 'string') return Reflect.get(target, name, ctx) const checkInject = (name: string) => { - // Case 1: a normal property defined on `target` + // Case 1: a normal property defined on context if (Reflect.has(target, name)) return // Case 2: built-in services and special properties // - prototype: prototype detection // - then: async function return if (['prototype', 'then', 'registry', 'lifecycle'].includes(name)) return - // Case 3: declared as plugin injection - if (ctx.runtime.inject.has(name)) return + // Case 3: access from root or declared as `inject` + if (!ctx.runtime.plugin || ctx.runtime.inject.has(name)) return ctx.emit('internal/warning', new Error(`property ${name} is not registered, declare it as \`inject\` to suppress this warning`)) } @@ -114,13 +115,14 @@ export class Context { if (typeof value !== 'function') return value return value.bind(service) } else if (internal.type === 'service') { - checkInject(name) - return ctx.get(name) + if (!internal.builtin) checkInject(name) + return ctx.get(name as any) } }, set(target, name, value, ctx: Context) { if (typeof name !== 'string') return Reflect.set(target, name, value, ctx) + const internal = ctx[Context.internal][name] if (!internal) return Reflect.set(target, name, value, ctx) if (internal.type === 'mixin') { @@ -165,8 +167,8 @@ export class Context { self.mixin('scope', ['config', 'runtime', 'collect', 'accept', 'decline']) self.mixin('registry', ['using', 'plugin', 'dispose']) self.mixin('lifecycle', ['on', 'once', 'off', 'after', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop']) - self.provide('registry', new Registry(self, config!)) - self.provide('lifecycle', new Lifecycle(self)) + self.provide('registry', new Registry(self, config!), true) + self.provide('lifecycle', new Lifecycle(self), true) const attach = (internal: Context[typeof Context.internal]) => { if (!internal) return @@ -203,10 +205,10 @@ export class Context { return this.scope } - get(name: string) { + get(name: K): undefined | this[K] { const internal = this[Context.internal][name] if (internal?.type !== 'service') return - const key = this[Context.shadow][name] || internal.key + const key: symbol = this[Context.shadow][name] || internal.key const value = this.root[key] if (!value || typeof value !== 'object') return value if (isUnproxyable(value)) { @@ -221,10 +223,10 @@ export class Context { }) } - provide(name: string, value?: any) { + provide(name: string, value?: any, builtin?: boolean) { const internal = Context.ensureInternal.call(this) const key = Symbol(name) - internal[name] = { type: 'service', key } + internal[name] = { type: 'service', key, builtin } this[key] = value } diff --git a/tests/plugin.spec.ts b/tests/plugin.spec.ts index 3bb7ab7..7f0f88f 100644 --- a/tests/plugin.spec.ts +++ b/tests/plugin.spec.ts @@ -87,7 +87,6 @@ describe('Plugin', () => { }) await root.lifecycle.flush() - expect(root.registry.size).to.equal(5) root.registry.forEach((scope) => { if (scope.error) throw scope.error }) diff --git a/tests/service.spec.ts b/tests/service.spec.ts index 995077b..c3a3c18 100644 --- a/tests/service.spec.ts +++ b/tests/service.spec.ts @@ -5,43 +5,57 @@ import * as jest from 'jest-mock' import { getHookSnapshot } from './utils' describe('Service', () => { - it('service access', () => { + it('service access', async () => { const root = new Context() const warn = jest.fn() root.on('internal/warning', warn) root.provide('foo') - // service should be proxyable - root.foo = new Set() - expect(warn.mock.calls).to.have.length(1) - - // `foo` is not declared as injection - root.foo.add(1) - expect(warn.mock.calls).to.have.length(2) + root.plugin((ctx) => { + // service should be proxyable + ctx.foo = new Set() + expect(warn.mock.calls).to.have.length(1) + + // `foo` is not declared as injection + ctx.foo.add(1) + expect(warn.mock.calls).to.have.length(2) + + // service cannot be overwritten + expect(() => ctx.foo = new Set()).to.throw() + }) - // service cannot be overwritten - expect(() => root.foo = new Set()).to.throw() + await root.lifecycle.flush() + root.registry.forEach((scope) => { + if (scope.error) throw scope.error + }) }) - it('non-service access', () => { + it('non-service access', async () => { const root = new Context() const warn = jest.fn() root.on('internal/warning', warn) - // `bar` is neither defined on context nor declared as injection - root.bar - expect(warn.mock.calls).to.have.length(1) - - // non-service can be unproxyable - root.bar = new Set() - expect(warn.mock.calls).to.have.length(1) - - // non-service can be accessed if defined on context - root.bar.add(1) - expect(warn.mock.calls).to.have.length(1) + root.plugin((ctx) => { + // `bar` is neither defined on context nor declared as injection + ctx.bar + expect(warn.mock.calls).to.have.length(1) + + // non-service can be unproxyable + ctx.bar = new Set() + expect(warn.mock.calls).to.have.length(1) + + // non-service can be accessed if defined on context + ctx.bar.add(1) + expect(warn.mock.calls).to.have.length(1) + + // non-service can be overwritten + expect(() => ctx.bar = new Set()).to.not.throw() + }) - // non-service can be overwritten - expect(() => root.bar = new Set()).to.not.throw() + await root.lifecycle.flush() + root.registry.forEach((scope) => { + if (scope.error) throw scope.error + }) }) it('normal service', async () => {