diff --git a/.eslintrc.js b/.eslintrc.js index a506cf4..707768a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,5 +24,11 @@ module.exports = { allowedNames: ['self', 'injector'], }, ], + 'no-void': [ + 'error', + { + allowAsStatement: true, + }, + ], }, }; diff --git a/.gitignore b/.gitignore index 5ea00d9..f75d46a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -# 由 https://github.com/msfeldstein/gitignore 自动生成 +lib +esm +types + # Logs logs *.log diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..40e8046 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/test/'], + coverageThreshold: { + global: { + branches: 10, + functions: 10, + lines: 10, + statements: 10, + }, + }, + setupFilesAfterEnv: ['/scripts/jest-setup.ts'], +}; diff --git a/package.json b/package.json index 04456bb..a97ab64 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,16 @@ "version": "1.10.1", "description": "A dependency injection tool for Javascript.", "license": "MIT", - "main": "dist/index.js", + "module": "esm/index.js", + "main": "lib/index.js", + "types": "types/index.d.ts", "scripts": { "prepare": "husky install", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "build": "rm -rf dist && tsc -p tsconfig.prod.json", - "test": "jest --coverage test/**", + "build": "npm run build:lib && npm run build:esm", + "build:lib": "rm -rf lib && tsc -p tsconfig.lib.json", + "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json", + "test": "jest --coverage tests/**", "test:watch": "yarn test --watch", "ci": "npm run lint && npm run test", "prepublishOnly": "npm run build", @@ -16,9 +20,6 @@ "release": "commit-and-tag-version --npmPublishHint 'echo Just Push code to remote repo, npm publish will be done by CI.'", "release:beta": "npm run release -- --prerelease beta" }, - "dependencies": { - "reflect-metadata": "^0.1.13" - }, "devDependencies": { "@commitlint/cli": "17.2.0", "@commitlint/config-conventional": "17.2.0", @@ -34,6 +35,7 @@ "jest": "29.2.2", "lint-staged": "13.0.3", "prettier": "2.7.1", + "reflect-metadata": "^0.1.13", "ts-jest": "29.0.3", "typescript": "4.8.4" }, @@ -55,22 +57,6 @@ "@commitlint/config-conventional" ] }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/test/" - ], - "coverageThreshold": { - "global": { - "branches": 10, - "functions": 10, - "lines": 10, - "statements": 10 - } - } - }, "keywords": [ "di", "injector" diff --git a/scripts/jest-setup.ts b/scripts/jest-setup.ts new file mode 100644 index 0000000..d2c9bc6 --- /dev/null +++ b/scripts/jest-setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/src/helper/compose.ts b/src/compose.ts similarity index 95% rename from src/helper/compose.ts rename to src/compose.ts index ddaf1b4..a8b0198 100644 --- a/src/helper/compose.ts +++ b/src/compose.ts @@ -30,7 +30,7 @@ function dispatch( stack.depth = idx; - let maybePromise: Promise | void; + let maybePromise: Promise | void | undefined; if (idx < middlewareList.length) { const middleware = middlewareList[idx]; diff --git a/src/decorator.ts b/src/decorator.ts index 255e1ff..f074471 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -1,6 +1,3 @@ -import 'reflect-metadata'; -import * as Helper from './helper'; -import * as Error from './error'; import { Token, InstanceOpts, @@ -13,31 +10,42 @@ import { IAfterReturningAspectHookFunction, IAfterThrowingAspectHookFunction, IAroundHookOptions, -} from './declare'; -import { markAsAspect, markAsHook } from './helper'; +} from './types'; +import { + addDeps, + getInjectorOfInstance, + getParameterDeps, + isToken, + markAsAspect, + markAsHook, + markInjectable, + setParameterIn, + setParameters, +} from './helper'; +import { noInjectorError, notInjectError, tokenInvalidError } from './error'; /** - * 装饰一个 Class 是否是可以被依赖注入 + * Decorate a Class to mark it as injectable * @param opts */ export function Injectable(opts?: InstanceOpts): ClassDecorator { return (target: T) => { - Helper.markInjectable(target, opts); + markInjectable(target, opts); const params = Reflect.getMetadata('design:paramtypes', target); if (Array.isArray(params)) { - Helper.setParameters(target, params); + setParameters(target, params); - // 如果支持多例创建,就不检查构造函数依赖的可注入性 + // If it supports multiple instances, do not check the injectability of the constructor dependencies if (opts && opts.multiple) { return; } - // 检查依赖的可注入性 - const depTokens = Helper.getParameterDeps(target); + // Check the injectability of the constructor dependencies + const depTokens = getParameterDeps(target); depTokens.forEach((item, index) => { - if (!Helper.isToken(item)) { - throw Error.notInjectError(target, index); + if (!isToken(item)) { + throw notInjectError(target, index); } }); } @@ -46,7 +54,7 @@ export function Injectable(opts?: InstanceOpts): ClassDecorator { interface InjectOpts { /** - * 默认值 + * Default value when the token is not found */ default?: any; } @@ -57,7 +65,7 @@ interface InjectOpts { */ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator { return (target, _: string | symbol | undefined, index: number) => { - Helper.setParameterIn(target, { ...opts, token }, index); + setParameterIn(target, { ...opts, token }, index); }; } @@ -67,7 +75,7 @@ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator */ export function Optional(token: Token = Symbol()): ParameterDecorator { return (target, _: string | symbol | undefined, index: number) => { - Helper.setParameterIn(target, { default: undefined, token }, index); + setParameterIn(target, { default: undefined, token }, index); }; } @@ -84,22 +92,22 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator realToken = Reflect.getMetadata('design:type', target, propertyKey); } - if (!Helper.isToken(realToken)) { - throw Error.tokenInvalidError(target, propertyKey, realToken); + if (!isToken(realToken)) { + throw tokenInvalidError(target, propertyKey, realToken); } - // 添加构造函数的依赖 - Helper.addDeps(target, realToken); + // Add the dependency of the constructor + addDeps(target, realToken); const descriptor: PropertyDescriptor = { configurable: true, enumerable: true, get(this: any) { if (!this[INSTANCE_KEY]) { - const injector = Helper.getInjectorOfInstance(this); + const injector = getInjectorOfInstance(this); if (!injector) { - throw Error.noInjectorError(this); + throw noInjectorError(this); } this[INSTANCE_KEY] = injector.get(realToken, opts); diff --git a/src/error.ts b/src/error.ts index 5087efd..bb13d74 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { Context, Token } from './declare'; +import { Context, Token } from './types'; function stringify(target: object | Token) { if (typeof target === 'object') { @@ -65,3 +65,8 @@ export function aliasCircularError(paths: Token[], current: Token) { `useAlias registration cycle detected! ${[...paths, current].map((v) => stringify(v)).join(' -> ')}`, ); } + +export function noInstancesInCompletedCreatorError(token: Token) { + /* istanbul ignore next */ + return new Error(`Cannot find value of ${stringify(token)} in a completed creator.`); +} diff --git a/src/factoryHelper.ts b/src/factoryHelper.ts index f43de3f..d620bc8 100644 --- a/src/factoryHelper.ts +++ b/src/factoryHelper.ts @@ -1,4 +1,4 @@ -import { FactoryFunction } from './declare'; +import { FactoryFunction } from './types'; import type { Injector } from './injector'; diff --git a/src/helper/dep-helper.ts b/src/helper/dep-helper.ts index 9fd04a6..d9799d5 100644 --- a/src/helper/dep-helper.ts +++ b/src/helper/dep-helper.ts @@ -1,5 +1,5 @@ -import { flatten, uniq } from './util'; -import { Token } from '../declare'; +import { flatten, uniq } from './utils'; +import { Token } from '../types'; import { getParameterDeps } from './parameter-helper'; import { createConstructorMetadataManager } from './reflect-helper'; diff --git a/src/helper/event.ts b/src/helper/event.ts index 9ec7db7..6051d9e 100644 --- a/src/helper/event.ts +++ b/src/helper/event.ts @@ -1,7 +1,13 @@ -export class EventEmitter { - private _listeners: Map = new Map(); +/** + * Modified from https://github.com/opensumi/utils/blob/main/packages/events/src/index.ts + */ - on(event: T, listener: Function) { +export type Handler = (...args: T) => void; + +export class EventEmitter> { + private _listeners: Map = new Map(); + + on(event: Event, listener: Handler) { if (!this._listeners.has(event)) { this._listeners.set(event, []); } @@ -10,7 +16,7 @@ export class EventEmitter { return () => this.off(event, listener); } - off(event: T, listener: Function) { + off(event: Event, listener: Handler) { if (!this._listeners.has(event)) { return; } @@ -21,8 +27,8 @@ export class EventEmitter { } } - once(event: T, listener: Function) { - const remove: () => void = this.on(event, (...args: any[]) => { + once(event: Event, listener: Handler) { + const remove: () => void = this.on(event, (...args: Parameters>) => { remove(); listener.apply(this, args); }); @@ -30,13 +36,21 @@ export class EventEmitter { return remove; } - emit(event: T, ...args: any[]) { + emit(event: Event, ...args: Parameters>) { if (!this._listeners.has(event)) { return; } [...this._listeners.get(event)!].forEach((listener) => listener.apply(this, args)); } + hasListener(event: Event) { + return this._listeners.has(event); + } + + getListeners(event: Event) { + return this._listeners.get(event) || []; + } + dispose() { this._listeners.clear(); } diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index 7f1ec91..5efb494 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { IHookStore, IDisposable, @@ -22,10 +21,10 @@ import { IAfterThrowingJoinPoint, IInstanceHooks, InstanceCreator, -} from '../declare'; -import compose, { Middleware } from './compose'; +} from '../types'; +import compose, { Middleware } from '../compose'; -export const HOOKED_SYMBOL = Symbol('COMMON_DI_HOOKED'); +export const HOOKED_SYMBOL = Symbol('DI_HOOKED'); export function applyHooks(instance: T, token: Token, hooks: IHookStore): T { if (typeof instance !== 'object') { @@ -76,6 +75,7 @@ export function createHookedFunction( const aroundHooks: Array> = []; const afterReturningHooks: Array> = []; const afterThrowingHooks: Array> = []; + // Onion model hooks.forEach((h) => { if (isBeforeHook(h)) { @@ -120,27 +120,18 @@ export function createHookedFunction( } finally { if (error) { // noop + } else if (promise) { + promise.then( + () => { + runAfterReturning(); + }, + (e) => { + error = e; + runAfterThrowing(); + }, + ); } else { - // 异步逻辑 - let p: Promise | undefined; - if (promise) { - p = promise; - } - - if (p) { - // p is a promise, use Promise's reject and resolve at this time - p.then( - () => { - runAfterReturning(); - }, - (e) => { - error = e; - runAfterThrowing(); - }, - ); - } else { - runAfterReturning(); - } + runAfterReturning(); } } @@ -219,6 +210,7 @@ export function createHookedFunction( if (afterHooks.length === 0) { return; } + let _inThisHook = true; const afterJoinPoint: IAfterJoinPoint = { getArgs: () => { @@ -253,6 +245,7 @@ export function createHookedFunction( if (afterReturningHooks.length === 0) { return; } + const afterReturningJoinPoint: IAfterReturningJoinPoint = { getArgs: () => { return args; @@ -284,6 +277,7 @@ export function createHookedFunction( if (afterThrowingHooks.length === 0) { return; } + const afterThrowingJoinPoint: IAfterThrowingJoinPoint = { getError: () => { return error; @@ -396,17 +390,16 @@ function runOneHook< if (hook.awaitPromise) { promise = promise || Promise.resolve(); promise = promise.then(() => { + // notice: here we return hook's result + // and the return statement on the next condition branch will return undefined. return hook.hook(joinPoint); }); - } else { - if (promise) { - promise = promise.then(() => { - hook.hook(joinPoint); - return; - }); - } else { + } else if (promise) { + promise = promise.then(() => { hook.hook(joinPoint); - } + }); + } else { + hook.hook(joinPoint); } return promise; } @@ -422,33 +415,37 @@ export class HookStore implements IHookStore { }); return { dispose: () => { - disposers.forEach((disposer) => { - disposer.dispose(); - }); + for (const toDispose of disposers) { + toDispose.dispose(); + } }, }; } hasHooks(token: Token) { - if (!this.hooks.has(token)) { - if (this.parent) { - return this.parent.hasHooks(token); - } else { - return false; - } - } else { + if (this.hooks.has(token)) { return true; } + if (this.parent) { + return this.parent.hasHooks(token); + } + return false; } getHooks(token: Token, method: string | number | symbol): IValidAspectHook[] { let hooks: IValidAspectHook[] = []; if (this.parent) { - hooks = this.parent.getHooks(token, method); + hooks = hooks.concat(this.parent.getHooks(token, method)); } + if (this.hooks.get(token)?.has(method)) { hooks = hooks.concat(this.hooks.get(token)!.get(method)!); } + + hooks.sort((a, b) => { + return (b.priority || 0) - (a.priority || 0); + }); + return hooks; } @@ -461,7 +458,6 @@ export class HookStore implements IHookStore { if (!instanceHooks.has(hook.method)) { instanceHooks.set(hook.method, []); } - // TODO: 支持order instanceHooks.get(hook.method)!.push(hook); return { dispose: () => { @@ -517,6 +513,7 @@ export function markAsHook( } hooks.push({ prop, type, target: hookTarget, targetMethod, options }); } + export function isAspectCreator(target: InstanceCreator) { return !!Reflect.getMetadata(ASPECT_KEY, (target as ClassCreator).useClass); } diff --git a/src/helper/index.ts b/src/helper/index.ts index 5bbaaef..18d1b4a 100644 --- a/src/helper/index.ts +++ b/src/helper/index.ts @@ -2,6 +2,7 @@ export * from './dep-helper'; export * from './injector-helper'; export * from './parameter-helper'; export * from './provider-helper'; -export * from './util'; +export * from './utils'; export * from './is-function'; export * from './hook-helper'; +export * from './event'; diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 99aa5d6..36611c3 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -1,5 +1,4 @@ -import 'reflect-metadata'; -import { InstanceOpts } from '../declare'; +import { InstanceOpts } from '../types'; import type { Injector } from '../injector'; import { VERSION } from '../constants'; @@ -35,16 +34,13 @@ export function isInjectable(target: object) { return !!getInjectableOpts(target); } -let index = 0; -export function createId(name: string) { - return `${name}_${index++}`; -} - export function createIdFactory(name: string) { let idx = 0; return { - create() { + next() { return `${name}_${idx++}`; }, }; } + +export const injectorIdGenerator = createIdFactory('Injector'); diff --git a/src/helper/is-function.ts b/src/helper/is-function.ts index 24d0ce3..a0572d0 100644 --- a/src/helper/is-function.ts +++ b/src/helper/is-function.ts @@ -12,7 +12,7 @@ import { CreatorStatus, AliasProvider, AliasCreator, -} from '../declare'; +} from '../types'; import { isInjectable } from './injector-helper'; export function isTypeProvider(provider: Provider | Token): provider is TypeProvider { diff --git a/src/helper/parameter-helper.ts b/src/helper/parameter-helper.ts index 31455b6..44526d3 100644 --- a/src/helper/parameter-helper.ts +++ b/src/helper/parameter-helper.ts @@ -1,4 +1,4 @@ -import { Token, ParameterOpts } from '../declare'; +import { Token, ParameterOpts } from '../types'; import { createConstructorMetadataManager } from './reflect-helper'; const PARAMETER_KEY = Symbol('PARAMETER_KEY'); diff --git a/src/helper/provider-helper.ts b/src/helper/provider-helper.ts index 72d5f29..2bb59f1 100644 --- a/src/helper/provider-helper.ts +++ b/src/helper/provider-helper.ts @@ -1,5 +1,4 @@ -import * as Error from '../error'; -import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../declare'; +import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../types'; import { isValueProvider, isClassProvider, @@ -11,6 +10,7 @@ import { import { getParameterOpts } from './parameter-helper'; import { getAllDeps } from './dep-helper'; import { getInjectableOpts } from './injector-helper'; +import { noInjectableError } from '../error'; export function getProvidersFromTokens(targets: Token[]) { const spreadDeps: Token[] = getAllDeps(...targets); @@ -45,7 +45,7 @@ export function parseCreatorFromProvider(provider: Provider): InstanceCreator { if (isValueProvider(provider)) { return { - instance: provider.useValue, + instances: new Set([provider.useValue]), isDefault: provider.isDefault, status: CreatorStatus.done, ...basicObj, @@ -66,7 +66,7 @@ export function parseCreatorFromProvider(provider: Provider): InstanceCreator { const opts = getInjectableOpts(useClass); if (!opts) { - throw Error.noInjectableError(useClass); + throw noInjectableError(useClass); } const parameters = getParameterOpts(useClass); diff --git a/src/helper/reflect-helper.ts b/src/helper/reflect-helper.ts index 158af40..48d092c 100644 --- a/src/helper/reflect-helper.ts +++ b/src/helper/reflect-helper.ts @@ -1,5 +1,3 @@ -import 'reflect-metadata'; - function findConstructor(target: object) { return typeof target === 'object' ? target.constructor : target; } diff --git a/src/helper/util.ts b/src/helper/utils.ts similarity index 100% rename from src/helper/util.ts rename to src/helper/utils.ts diff --git a/src/index.ts b/src/index.ts index 71a8e87..157c455 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from './declare'; +export * from './types'; export * from './decorator'; export * from './injector'; export * from './factoryHelper'; diff --git a/src/injector.ts b/src/injector.ts index 851ac99..fa48828 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -1,5 +1,23 @@ -import * as Helper from './helper'; -import * as InjectorError from './error'; +import { + flatten, + getAllDeps, + getParameterOpts, + hasTag, + injectorIdGenerator, + isFactoryCreator, + isInjectableToken, + isPromiseLike, + parseCreatorFromProvider, + parseTokenFromProvider, + uniq, +} from './helper'; +import { + aliasCircularError, + circularError, + noProviderError, + noInstancesInCompletedCreatorError, + tagOnlyError, +} from './error'; import { INJECTOR_TOKEN, Provider, @@ -19,7 +37,8 @@ import { Context, ParameterOpts, IDisposable, -} from './declare'; + FactoryCreator, +} from './types'; import { isClassCreator, setInjector, @@ -30,12 +49,11 @@ import { isAspectCreator, getHookMeta, isAliasCreator, + EventEmitter, } from './helper'; -import { EventEmitter } from './helper/event'; export class Injector { - id = Helper.createId('Injector'); - private instanceIdGenerator = Helper.createIdFactory('Instance_' + this.id.slice(9)); + id = injectorIdGenerator.next(); depth = 0; tag?: string; @@ -45,9 +63,8 @@ export class Injector { private tagMatrix = new Map>(); private domainMap = new Map(); creatorMap = new Map(); - instanceRefMap = new Map(); - private instanceDisposedEmitter = new EventEmitter(); + private instanceDisposedEmitter = new EventEmitter(); constructor(providers: Provider[] = [], private opts: InjectorOpts = {}, parent?: Injector) { this.tag = opts.tag; @@ -131,12 +148,12 @@ export class Injector { useClass: token, }; } else { - throw InjectorError.noProviderError(token); + throw noProviderError(token); } } } else { // firstly, use tag to exchange token - if (opts && Helper.hasTag(opts)) { + if (opts && hasTag(opts)) { const tagToken = this.exchangeToken(token, opts.tag); [creator, injector] = this.getCreator(tagToken); } @@ -154,7 +171,7 @@ export class Injector { } if (!creator) { - throw InjectorError.noProviderError(token); + throw noProviderError(token); } const ctx = { @@ -191,7 +208,7 @@ export class Injector { */ hasInstance(instance: any) { for (const creator of this.creatorMap.values()) { - if (creator.instance === instance) { + if (creator.instances?.has(instance)) { return true; } } @@ -208,14 +225,14 @@ export class Injector { } parseDependencies(...targets: Token[]) { - const deepDeps: Token[] = Helper.getAllDeps(...targets); + const deepDeps: Token[] = getAllDeps(...targets); const allDeps = targets.concat(deepDeps); - const providers = Helper.uniq(allDeps.filter(Helper.isInjectableToken)); + const providers = uniq(allDeps.filter(isInjectableToken)); this.setProviders(providers, { deep: true }); - const defaultProviders = Helper.flatten( + const defaultProviders = flatten( providers.map((p) => { - return Helper.getParameterOpts(p); + return getParameterOpts(p); }), ) .filter((opt) => { @@ -232,7 +249,7 @@ export class Injector { // make sure all dependencies have corresponding providers const notProvidedDeps = allDeps.filter((d) => !this.getCreator(d)[0]); if (notProvidedDeps.length) { - throw InjectorError.noProviderError(...notProvidedDeps); + throw noProviderError(...notProvidedDeps); } } @@ -258,45 +275,51 @@ export class Injector { } onceInstanceDisposed(instance: any, cb: () => void) { - const instanceId = this.getInstanceId(instance); - if (!instanceId) { - return; - } - return this.instanceDisposedEmitter.once(instanceId, cb); + return this.instanceDisposedEmitter.once(instance, cb); } disposeOne(token: Token, key = 'dispose') { const [creator] = this.getCreator(token); - if (!creator || creator.status === CreatorStatus.init) { + if (!creator) { return; } - const instance = creator.instance; - const instanceId = this.getInstanceId(instance); - this.instanceRefMap.delete(instance); + const instances = creator.instances; - let maybePromise: Promise | undefined; - if (instance && typeof instance[key] === 'function') { - maybePromise = instance[key](); + let maybePromise: Promise | void | undefined; + if (instances) { + const disposeFns = [] as any[]; + + for (const item of instances.values()) { + let _maybePromise: Promise | undefined; + if (item && typeof item[key] === 'function') { + _maybePromise = item[key](); + } + + if (_maybePromise && isPromiseLike(_maybePromise)) { + disposeFns.push( + _maybePromise.then(() => { + this.instanceDisposedEmitter.emit(item); + }), + ); + } else { + this.instanceDisposedEmitter.emit(item); + } + } + + maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; } - creator.instance = undefined; + creator.instances = undefined; creator.status = CreatorStatus.init; - if (maybePromise && Helper.isPromiseLike(maybePromise)) { - maybePromise = maybePromise.then(() => { - instanceId && this.instanceDisposedEmitter.emit(instanceId); - }); - } else { - instanceId && this.instanceDisposedEmitter.emit(instanceId); - } return maybePromise; } disposeAll(key = 'dispose'): Promise { const creatorMap = this.creatorMap; - const promises: (Promise | undefined)[] = []; + const promises: (Promise | void)[] = []; for (const token of creatorMap.keys()) { promises.push(this.disposeOne(token, key)); @@ -321,13 +344,13 @@ export class Injector { private setProviders(providers: Provider[], opts: AddProvidersOpts = {}) { for (const provider of providers) { - const originToken = Helper.parseTokenFromProvider(provider); - const token = Helper.hasTag(provider) ? this.exchangeToken(originToken, provider.tag) : originToken; + const originToken = parseTokenFromProvider(provider); + const token = hasTag(provider) ? this.exchangeToken(originToken, provider.tag) : originToken; const current = opts.deep ? this.getCreator(token)[0] : this.resolveToken(token)[1]; const shouldBeSet = [ // use provider's override attribute. - Helper.isTypeProvider(provider) ? false : provider.override, + isTypeProvider(provider) ? false : provider.override, // use opts.override. The user explicitly call `overrideProviders`. opts.override, // if this token do not have corresponding creator, use override @@ -336,7 +359,7 @@ export class Injector { ].some(Boolean); if (shouldBeSet) { - const creator = Helper.parseCreatorFromProvider(provider); + const creator = parseCreatorFromProvider(provider); this.creatorMap.set(token, creator); // use effect to Make sure there are no cycles @@ -369,18 +392,19 @@ export class Injector { return instance; }; - const preprocessedHooks: IValidAspectHook[] = hookMetadata.map((metadata) => { + const preprocessedHooks = hookMetadata.map((metadata) => { const wrapped = (...args: any[]) => { const instance = getInstance(); return instance[metadata.prop].call(instance, ...args); }; return { awaitPromise: metadata.options.await, + priority: metadata.options.priority, hook: wrapped, method: metadata.targetMethod, target: metadata.target, type: metadata.type, - }; + } as IValidAspectHook; }); toDispose = this.hookStore.createHooks(preprocessedHooks); } @@ -397,10 +421,6 @@ export class Injector { } } - private getNextInstanceId() { - return this.instanceIdGenerator.create(); - } - private resolveToken(token: Token): [Token, InstanceCreator | undefined] { let creator = this.creatorMap.get(token); @@ -410,7 +430,7 @@ export class Injector { token = creator.useAlias; if (paths.includes(token)) { - throw InjectorError.aliasCircularError(paths, token); + throw aliasCircularError(paths, token); } paths.push(token); creator = this.creatorMap.get(token); @@ -419,7 +439,7 @@ export class Injector { return [token, creator]; } - private getCreator(token: Token): [InstanceCreator | null, Injector] { + getCreator(token: Token): [InstanceCreator | null, Injector] { const creator: InstanceCreator | undefined = this.resolveToken(token)[1]; if (creator) { @@ -433,37 +453,39 @@ export class Injector { return [null, this]; } - private getOrSaveInstanceId(instance: any, id: string) { - if (this.instanceRefMap.has(instance)) { - return this.instanceRefMap.get(instance)!; - } - this.instanceRefMap.set(instance, id); - return id; - } - private createInstance(ctx: Context, defaultOpts?: InstanceOpts, args?: any[]) { - const { creator, token } = ctx; + const { creator } = ctx; if (creator.dropdownForTag && creator.tag !== this.tag) { - throw InjectorError.tagOnlyError(String(creator.tag), String(this.tag)); + throw tagOnlyError(String(creator.tag), String(this.tag)); } - if (Helper.isClassCreator(creator)) { + if (isClassCreator(creator)) { const opts = defaultOpts ?? creator.opts; // if a class creator is singleton, and the instance is already created, return the instance. - if (!opts.multiple && creator.status === CreatorStatus.done) { - return creator.instance; + if (creator.status === CreatorStatus.done && !opts.multiple) { + if (creator.instances) { + return creator.instances.values().next().value; + } + + /* istanbul ignore next */ + throw noInstancesInCompletedCreatorError(ctx.token); } + /* istanbul ignore next */ return this.createInstanceFromClassCreator(ctx as Context, opts, args); } - if (Helper.isFactoryCreator(creator)) { - return applyHooks(creator.useFactory(this), token, this.hookStore); + if (isFactoryCreator(creator)) { + return this.createInstanceFromFactory(ctx as Context); + } + + if (creator.instances) { + return creator.instances.values().next().value; } - // must be ValueCreator, no need to hook. - return creator.instance; + /* istanbul ignore next */ + throw noInstancesInCompletedCreatorError(ctx.token); } private createInstanceFromClassCreator(ctx: Context, opts: InstanceOpts, defaultArgs?: any[]) { @@ -474,23 +496,21 @@ export class Injector { // If you try to create an instance whose status is creating, it must be caused by circular dependencies. if (currentStatus === CreatorStatus.creating) { - throw InjectorError.circularError(cls, ctx); + throw circularError(cls, ctx); } creator.status = CreatorStatus.creating; try { const args = defaultArgs ?? this.getParameters(creator.parameters, ctx); - const nextId = this.getNextInstanceId(); - const instance = this.createInstanceWithInjector(cls, token, injector, args, nextId); - void this.getOrSaveInstanceId(instance, nextId); + const instance = this.createInstanceWithInjector(cls, token, injector, args); creator.status = CreatorStatus.init; // if not allow multiple, save the instance in creator. if (!opts.multiple) { creator.status = CreatorStatus.done; - creator.instance = instance; } + creator.instances ? creator.instances.add(instance) : (creator.instances = new Set([instance])); return instance; } catch (e) { @@ -520,17 +540,11 @@ export class Injector { if (!creator && Object.prototype.hasOwnProperty.call(opts, 'default')) { return opts.default; } - throw InjectorError.noProviderError(opts.token); + throw noProviderError(opts.token); }); } - private createInstanceWithInjector( - cls: ConstructorOf, - token: Token, - injector: Injector, - args: any[], - id: string, - ) { + private createInstanceWithInjector(cls: ConstructorOf, token: Token, injector: Injector, args: any[]) { // when creating an instance, set injector to prototype, so that the constructor can access it. // after creating the instance, remove the injector from prototype to prevent memory leaks. setInjector(cls.prototype, injector); @@ -539,15 +553,15 @@ export class Injector { // mount injector on the instance, so that the inner object can access the injector in the future. setInjector(ret, injector); - Object.assign(ret, { - __id: id, - __injectorId: injector.id, - }); return applyHooks(ret, token, this.hookStore); } - getInstanceId(instance: any) { - return this.instanceRefMap.get(instance); + private createInstanceFromFactory(ctx: Context) { + const { creator, token } = ctx; + + const value = applyHooks(creator.useFactory(this), token, this.hookStore); + creator.instances ? creator.instances.add(value) : (creator.instances = new Set([value])); + return value; } } diff --git a/src/declare.ts b/src/types.ts similarity index 93% rename from src/declare.ts rename to src/types.ts index 2e33d2a..b52ebc7 100644 --- a/src/declare.ts +++ b/src/types.ts @@ -73,9 +73,9 @@ interface BasicCreator { dropdownForTag?: boolean; status?: CreatorStatus; /** - * Store the instantiated object. + * Store the instantiated objects. */ - instance?: any; + instances?: Set; /** * Represent this creator is parsed from `Parameter`. and the params of Inject has set `default` attribution. */ @@ -83,7 +83,6 @@ interface BasicCreator { } export interface ValueCreator extends BasicCreator { - instance: any; status: CreatorStatus.done; } @@ -169,6 +168,7 @@ export interface IAspectHook; } @@ -257,15 +257,23 @@ export interface IDisposable { } export interface IHookOptions { - // Whether to wait for the hook (if the return value of the hook is a promise) + /** + * Whether to wait for the hook (if the return value of the hook is a promise) + */ await?: boolean; + + /** + * The priority of the hook. + * for `before` hooks, the higher the priority, the earlier the execution. + * for `after` and `around` hooks, the higher the priority, the later the execution. + * @default 0 + */ + priority?: number; } -export interface IAroundHookOptions { +export interface IAroundHookOptions extends IHookOptions { /** - * @deprecated AroundHook will always await the promise, it act as the union model. - * - * Whether to wait for the hook (if the return value of the hook is a promise) + * @deprecated around hooks act as the union model, you can just use `ctx.proceed()`(no await) to invoke the next hook. */ await?: boolean; } diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..d2c9bc6 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/test/helper/event.test.ts b/test/helper/event.test.ts deleted file mode 100644 index 1e06a6e..0000000 --- a/test/helper/event.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { EventEmitter } from '../../src/helper/event'; - -describe('event emitter', () => { - it('basic usage', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - emitter.on('test', spy); - emitter.on('foo', spy2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - emitter.off('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - - emitter.once('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - - emitter.off('bar', spy); - - emitter.dispose(); - - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - }); - - it('many listeners listen to one event', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - emitter.on('test', spy); - emitter.on('test', spy2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - expect(spy2).toBeCalledWith('hello'); - - emitter.off('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - expect(spy2).toBeCalledTimes(2); - - emitter.dispose(); - }); - - it('can dispose event listener by using returned function', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - const spy3 = jest.fn(); - const disposeSpy = emitter.on('test', spy); - emitter.on('test', spy2); - - const disposeSpy3 = emitter.once('test', spy3); - disposeSpy3(); - - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - expect(spy2).toBeCalledWith('hello'); - - disposeSpy(); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - expect(spy2).toBeCalledTimes(2); - expect(spy3).toBeCalledTimes(0); - emitter.dispose(); - }); -}); diff --git a/test/aspect.test.ts b/tests/aspect.test.ts similarity index 100% rename from test/aspect.test.ts rename to tests/aspect.test.ts diff --git a/test/cases/issue-115/TestClass.ts b/tests/cases/issue-115/TestClass.ts similarity index 100% rename from test/cases/issue-115/TestClass.ts rename to tests/cases/issue-115/TestClass.ts diff --git a/test/cases/issue-115/const.ts b/tests/cases/issue-115/const.ts similarity index 100% rename from test/cases/issue-115/const.ts rename to tests/cases/issue-115/const.ts diff --git a/test/cases/issue-115/index.test.ts b/tests/cases/issue-115/index.test.ts similarity index 100% rename from test/cases/issue-115/index.test.ts rename to tests/cases/issue-115/index.test.ts diff --git a/test/decorator.test.ts b/tests/decorator.test.ts similarity index 100% rename from test/decorator.test.ts rename to tests/decorator.test.ts diff --git a/test/decorators/Autowired.test.ts b/tests/decorators/Autowired.test.ts similarity index 79% rename from test/decorators/Autowired.test.ts rename to tests/decorators/Autowired.test.ts index 3627dd4..34fdbe5 100644 --- a/test/decorators/Autowired.test.ts +++ b/tests/decorators/Autowired.test.ts @@ -12,7 +12,7 @@ describe('Autowired decorator', () => { return B; }).toThrow(); }); - it('进行依赖注入的时候,没有定义 Token 会报错', () => { + it('will throw error if the Token is not defined, when performing dependency injection.', () => { expect(() => { interface A { log(): void; @@ -26,7 +26,7 @@ describe('Autowired decorator', () => { }).toThrow(Error.tokenInvalidError(class B {}, 'a', Object)); }); - it('使用 null 进行依赖定义,期望报错', () => { + it('define dependencies with null, expect an error', () => { expect(() => { interface A { log(): void; @@ -40,7 +40,7 @@ describe('Autowired decorator', () => { }).toThrow(); }); - it('使用原始 Number 进行依赖定义,期望报错', () => { + it('Define dependencies using the original Number, expect an error', () => { expect(() => { interface A { log(): void; diff --git a/test/decorators/hooks.test.ts b/tests/decorators/hooks.test.ts similarity index 85% rename from test/decorators/hooks.test.ts rename to tests/decorators/hooks.test.ts index 537bb35..a74ab68 100644 --- a/test/decorators/hooks.test.ts +++ b/tests/decorators/hooks.test.ts @@ -17,7 +17,7 @@ import { } from '../../src'; describe('hook', () => { - it('使用代码来创建hook', async () => { + it('could use code to create hook', async () => { const injector = new Injector(); @Injectable() class TestClass { @@ -74,14 +74,14 @@ describe('hook', () => { hook: () => undefined, method: 'add', target: TestClass, - type: 'other' as any, // 不会造成任何影响(为了提高覆盖率) + type: 'other' as any, // for coverage }); const testClass = injector.get(TestClass); expect(testClass.add(1, 2)).toBe(5); expect(testClass.add(3, 4)).toBe(9); - // 同步变成异步 // Async hook on sync target + // the sync method will be wrapped to async method injector.createHook({ awaitPromise: true, hook: async (joinPoint: IBeforeJoinPoint) => { @@ -172,7 +172,7 @@ describe('hook', () => { expect(await testClass5.add(1, 2)).toBe(5); }); - it('使用注解来创建hook', async () => { + it('could use decorator to create hook', async () => { const TestClassToken = Symbol(); const pendings: Array> = []; @@ -394,7 +394,103 @@ describe('hook', () => { await Promise.all(pendings); }); - it('子injector应该正确拦截', () => { + describe('hook priority', () => { + it("hook's priority should work case1", async () => { + @Injectable() + class TestClass { + async add(a: number, b: number): Promise { + return a + b; + } + } + const injector = new Injector(); + + const resultArray = [] as number[]; + + injector.createHook({ + hook: async (joinPoint) => { + resultArray.push(1); + joinPoint.proceed(); + resultArray.push(2); + return joinPoint.setResult(9); + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 1001, + }); + + injector.createHooks([ + { + hook: async (joinPoint) => { + resultArray.push(0); + joinPoint.proceed(); + resultArray.push(3); + const result = await joinPoint.getResult(); + if (result === 9) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 10011, + }, + ]); + + const testClass = injector.get(TestClass); + expect(await testClass.add(1, 2)).toBe(9); + expect(resultArray).toEqual([1, 0, 3, 2]); + }); + + it("hook's priority should work case2", async () => { + @Injectable() + class TestClass { + async add(a: number, b: number): Promise { + return a + b; + } + } + const injector = new Injector(); + + const resultArray = [] as number[]; + + injector.createHook({ + hook: async (joinPoint) => { + resultArray.push(1); + joinPoint.proceed(); + resultArray.push(2); + return joinPoint.setResult(9); + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 1001, + }); + + injector.createHooks([ + { + hook: async (joinPoint) => { + resultArray.push(0); + joinPoint.proceed(); + resultArray.push(3); + const result = await joinPoint.getResult(); + if (result === 9) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 100, + }, + ]); + + const testClass = injector.get(TestClass); + expect(await testClass.add(1, 2)).toBe(10); + expect(resultArray).toEqual([0, 1, 2, 3]); + }); + }); + + it('can work in child injector', () => { @Injectable() class TestClass { add(a: number, b: number): number { @@ -464,8 +560,8 @@ describe('hook', () => { const testClassChild = injector2.get(TestClass); const testClassChild2 = injector2.get(TestClass2); expect(testClass.add(1, 2)).toBe(30); - expect(testClassChild.add(1, 2)).toBe(30); // 仅仅命中parent中的Hook - expect(testClassChild2.add(1, 2)).toBe(600); // 两边的hook都会命中 + expect(testClassChild.add(1, 2)).toBe(30); // on hooked by parent + expect(testClassChild2.add(1, 2)).toBe(600); // will hooked by parent and child }); it('could dispose proxied instance', () => { @@ -527,7 +623,6 @@ describe('hook', () => { expect(constructorSpy).toBeCalledTimes(1); injector.disposeOne(TestClass); - // injector.disposeOne(TestAspect); expect(example.run()).toBe(30); expect(constructorSpy).toBeCalledTimes(2); diff --git a/test/helper/compose.test.ts b/tests/helper/compose.test.ts similarity index 99% rename from test/helper/compose.test.ts rename to tests/helper/compose.test.ts index 686f5ca..25fc69e 100644 --- a/test/helper/compose.test.ts +++ b/tests/helper/compose.test.ts @@ -1,4 +1,4 @@ -import compose, { Middleware } from '../../src/helper/compose'; +import compose, { Middleware } from '../../src/compose'; interface ExampleContext { getName(): string; diff --git a/test/helper/dep-helper.test.ts b/tests/helper/dep-helper.test.ts similarity index 82% rename from test/helper/dep-helper.test.ts rename to tests/helper/dep-helper.test.ts index c283ba0..bd0400e 100644 --- a/test/helper/dep-helper.test.ts +++ b/tests/helper/dep-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/dep-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('dep helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; @@ -24,7 +24,7 @@ describe(__filename, () => { Helper.addDeps(Constructor, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('在父级进行依赖定义', () => { @@ -32,7 +32,7 @@ describe(__filename, () => { Helper.addDeps(Parent, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('依赖取值出来应该是去重的结果', () => { @@ -40,7 +40,7 @@ describe(__filename, () => { Helper.addDeps(Parent, dep, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('在父级进行依赖定义,并且再新定义', () => { @@ -51,10 +51,10 @@ describe(__filename, () => { Helper.addDeps(Constructor, dep2); const depsFromParentConstructor = Helper.getAllDeps(Parent); - expect(depsFromParentConstructor).toEqual([ dep1 ]); + expect(depsFromParentConstructor).toEqual([dep1]); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep1, dep2 ]); + expect(depsFromConstructor).toEqual([dep1, dep2]); }); it('当前一个依赖包含了后面的所有依赖的时候,应该正确解析', () => { @@ -68,7 +68,7 @@ describe(__filename, () => { const deps = Helper.getAllDeps(Dep1, Dep2); const depsAgain = Helper.getAllDeps(Dep2); - expect(deps).toEqual([ Dep2, Dep3 ]); - expect(depsAgain).toEqual([ Dep3 ]); + expect(deps).toEqual([Dep2, Dep3]); + expect(depsAgain).toEqual([Dep3]); }); }); diff --git a/tests/helper/event.test.ts b/tests/helper/event.test.ts new file mode 100644 index 0000000..56ba2c8 --- /dev/null +++ b/tests/helper/event.test.ts @@ -0,0 +1,173 @@ +import { EventEmitter } from '../../src/helper/event'; + +describe('event emitter', () => { + it('basic usage', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('foo', spy2); + + expect(emitter.hasListener('test')).toBe(true); + const listeners = emitter.getListeners('test'); + expect(listeners.length).toBe(1); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + emitter.off('test', spy); + + const listeners2 = emitter.getListeners('test'); + expect(listeners2.length).toBe(0); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + + emitter.once('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + + emitter.off('bar', spy); + + emitter.dispose(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + }); + + it('many listeners listen to one event', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('test', spy2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + emitter.off('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + + emitter.dispose(); + }); + + it('can dispose event listener by using returned function', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + const spy3 = jest.fn(); + const disposeSpy = emitter.on('test', spy); + emitter.on('test', spy2); + + const disposeSpy3 = emitter.once('test', spy3); + disposeSpy3(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + disposeSpy(); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + expect(spy3).toBeCalledTimes(0); + emitter.dispose(); + }); +}); + +describe('event emitter types', () => { + it('basic usage', () => { + const emitter = new EventEmitter<{ + test: [string, string]; + foo: [string]; + }>(); + + const spy = jest.fn(); + const spy2 = jest.fn(); + + emitter.on('test', spy); + emitter.on('foo', spy2); + + expect(emitter.hasListener('test')).toBe(true); + const listeners = emitter.getListeners('test'); + expect(listeners.length).toBe(1); + + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledWith('hello', 'world'); + emitter.off('test', spy); + + const listeners2 = emitter.getListeners('test'); + expect(listeners2.length).toBe(0); + + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(1); + + emitter.once('test', spy); + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(2); + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(2); + + emitter.off('bar' as any, spy); + + emitter.dispose(); + + emitter.emit('test' as any, 'hello'); + expect(spy).toBeCalledTimes(2); + }); + + it('many listeners listen to one event', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('test', spy2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + emitter.off('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + + emitter.dispose(); + }); + + it('can dispose event listener by using returned function', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + const spy3 = jest.fn(); + const disposeSpy = emitter.on('test', spy); + emitter.on('test', spy2); + + const disposeSpy3 = emitter.once('test', spy3); + disposeSpy3(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + disposeSpy(); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + expect(spy3).toBeCalledTimes(0); + emitter.dispose(); + }); +}); diff --git a/test/helper/hook-helper.test.ts b/tests/helper/hook-helper.test.ts similarity index 92% rename from test/helper/hook-helper.test.ts rename to tests/helper/hook-helper.test.ts index f1ca8a1..2a81a3c 100644 --- a/test/helper/hook-helper.test.ts +++ b/tests/helper/hook-helper.test.ts @@ -2,7 +2,6 @@ import { HookStore, applyHooks, isHooked } from '../../src/helper'; import { HookType } from '../../src'; describe('hook store test', () => { - const token1 = Symbol(); it('可以创建和删除hook', () => { @@ -59,17 +58,18 @@ describe('hook store test', () => { expect(valueRet).toBe('1'); const hookStore = new HookStore(); - hookStore.createHooks([{ - hook: () => void 0, - method: 'a', - target: token1, - type: HookType.After, - }]); + hookStore.createHooks([ + { + hook: () => undefined, + method: 'a', + target: token1, + type: HookType.After, + }, + ]); const objectRet = applyHooks({}, token1, hookStore); expect(isHooked(objectRet)).toBeTruthy(); const objectRetNoHooks = applyHooks({}, token1, new HookStore()); expect(isHooked(objectRetNoHooks)).toBeFalsy(); - }); }); diff --git a/test/helper/injector-helper.test.ts b/tests/helper/injector-helper.test.ts similarity index 98% rename from test/helper/injector-helper.test.ts rename to tests/helper/injector-helper.test.ts index 7b8dfa1..997cf63 100644 --- a/test/helper/injector-helper.test.ts +++ b/tests/helper/injector-helper.test.ts @@ -4,7 +4,7 @@ import { ConstructorOf, InstanceOpts } from '../../src'; // eslint-disable-next-line const pkg = require('../../package.json'); -describe(__filename, () => { +describe('injector helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/test/helper/is-function.test.ts b/tests/helper/is-function.test.ts similarity index 97% rename from test/helper/is-function.test.ts rename to tests/helper/is-function.test.ts index aade253..216dec3 100644 --- a/test/helper/is-function.test.ts +++ b/tests/helper/is-function.test.ts @@ -9,7 +9,7 @@ import { CreatorStatus, } from '../../src'; -describe(__filename, () => { +describe('is function', () => { class A {} const clsToken: Token = A; const strToken: Token = 'strToken'; @@ -76,7 +76,7 @@ describe(__filename, () => { it('isValueCreator', () => { expect(Helper.isValueCreator(factoryProvider)).toBe(false); - expect(Helper.isValueCreator({ status: CreatorStatus.done, instance: A })).toBe(true); + expect(Helper.isValueCreator({ status: CreatorStatus.done, instances: new Set([A]) })).toBe(true); }); it('isFactoryCreator', () => { diff --git a/test/helper/parameter-helper.test.ts b/tests/helper/parameter-helper.test.ts similarity index 83% rename from test/helper/parameter-helper.test.ts rename to tests/helper/parameter-helper.test.ts index e0f52f9..857c8e1 100644 --- a/test/helper/parameter-helper.test.ts +++ b/tests/helper/parameter-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/parameter-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('parameter helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; @@ -54,28 +54,28 @@ describe(__filename, () => { }); it('从子类设置的依赖能够覆盖父类的依赖', () => { - Helper.setParameters(Parent, [ 'parent', 'parent' ]); + Helper.setParameters(Parent, ['parent', 'parent']); const parentDeps = Helper.getParameterDeps(Parent); - expect(parentDeps).toEqual([ 'parent', 'parent' ]); + expect(parentDeps).toEqual(['parent', 'parent']); - Helper.setParameters(Constructor, [ 'child', 'child' ]); + Helper.setParameters(Constructor, ['child', 'child']); const childDeps = Helper.getParameterDeps(Constructor); - expect(childDeps).toEqual([ 'child', 'child' ]); + expect(childDeps).toEqual(['child', 'child']); }); it('不同位置的 Token 描述能够合并', () => { Helper.setParameterIn(Parent, { token: 'parent' }, 0); Helper.setParameterIn(Constructor, { token: 'child' }, 1); const deps = Helper.getParameterDeps(Constructor); - expect(deps).toEqual([ 'parent', 'child' ]); + expect(deps).toEqual(['parent', 'child']); }); it('能够得到构造依赖和 Token 定义的结果产物', () => { - Helper.setParameters(Constructor, [ 'parameter1', 'parameter2' ]); + Helper.setParameters(Constructor, ['parameter1', 'parameter2']); Helper.setParameterIn(Constructor, { token: 'token' }, 1); const deps = Helper.getParameterDeps(Constructor); - expect(deps).toEqual([ 'parameter1', 'token' ]); + expect(deps).toEqual(['parameter1', 'token']); const opts = Helper.getParameterOpts(Constructor); expect(opts).toEqual([{ token: 'parameter1' }, { token: 'token' }]); diff --git a/test/helper/provider-helper.test.ts b/tests/helper/provider-helper.test.ts similarity index 88% rename from test/helper/provider-helper.test.ts rename to tests/helper/provider-helper.test.ts index 53f9022..5405b60 100644 --- a/test/helper/provider-helper.test.ts +++ b/tests/helper/provider-helper.test.ts @@ -9,7 +9,7 @@ import { Injectable, } from '../../src'; -describe(__filename, () => { +describe('provider helper', () => { @Injectable() class A {} const clsToken: Token = A; @@ -47,13 +47,15 @@ describe(__filename, () => { parameters: [], useClass: A, }); - expect(Helper.parseCreatorFromProvider(valueProvider)).toMatchObject({ - instance: expect.any(A), - status: CreatorStatus.done, - }); + + const creator = Helper.parseCreatorFromProvider(valueProvider); + expect(creator.instances?.has(A)).toBeTruthy; + expect(creator.status).toBe(CreatorStatus.done); + expect(Helper.parseCreatorFromProvider(factoryProvider)).toMatchObject({ useFactory: factoryProvider.useFactory, }); + expect(Helper.parseCreatorFromProvider(classProvider)).toMatchObject({ parameters: [], useClass: A, diff --git a/test/helper/reflect-helper.test.ts b/tests/helper/reflect-helper.test.ts similarity index 98% rename from test/helper/reflect-helper.test.ts rename to tests/helper/reflect-helper.test.ts index 1678cd4..569487a 100644 --- a/test/helper/reflect-helper.test.ts +++ b/tests/helper/reflect-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/reflect-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('reflect helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/test/helper/util-helper.test.ts b/tests/helper/utils-helper.test.ts similarity index 87% rename from test/helper/util-helper.test.ts rename to tests/helper/utils-helper.test.ts index cd1a06e..da1de4d 100644 --- a/test/helper/util-helper.test.ts +++ b/tests/helper/utils-helper.test.ts @@ -1,6 +1,6 @@ -import * as Helper from '../../src/helper/util'; +import * as Helper from '../../src/helper/utils'; -describe(__filename, () => { +describe('utils helper', () => { it('uniq', () => { const obj = {}; const arr = [1, 2, obj, obj, 3]; diff --git a/test/injector.test.ts b/tests/injector.test.ts similarity index 91% rename from test/injector.test.ts rename to tests/injector.test.ts index 59f4645..65f7e46 100644 --- a/test/injector.test.ts +++ b/tests/injector.test.ts @@ -1,24 +1,6 @@ -import { - Autowired, - Injectable, - Injector, - INJECTOR_TOKEN, - Inject, - Optional, - Aspect, - Before, - After, - Around, - HookType, - IBeforeJoinPoint, - IAfterJoinPoint, - IAroundJoinPoint, - AfterReturning, - IAfterReturningJoinPoint, - AfterThrowing, - IAfterThrowingJoinPoint, -} from '../src'; +import { Autowired, Injectable, Injector, INJECTOR_TOKEN, Inject, Optional } from '../src'; import * as InjectorError from '../src/error'; +import { getInjectorOfInstance } from '../src/helper'; describe('test injector work', () => { @Injectable() @@ -419,22 +401,15 @@ describe('test injector work', () => { expect(injector1.id).not.toBe(injector2.id); }); - it('实例对象有 ID 数据', () => { + it('could get id from the instance', () => { const injector = new Injector(); const instance1 = injector.get(A); const instance2 = injector.get(A); const instance3 = injector.get(B); - expect((instance1 as any).__injectorId).toBe(injector.id); - expect((instance2 as any).__injectorId).toBe(injector.id); - expect((instance3 as any).__injectorId).toBe(injector.id); - - expect((instance1 as any).__id.startsWith('Instance')).toBeTruthy(); - console.log(`file: injector.test.ts ~ it ~ (instance1 as any).__id:`, (instance1 as any).__id); - expect((instance1 as any).__id).toBe((instance2 as any).__id); - expect((instance1 as any).__id).toBe((injector as any).getOrSaveInstanceId(instance1)); - expect((instance2 as any).__id).toBe((injector as any).getOrSaveInstanceId(instance2)); - expect((instance1 as any).__id).not.toBe((instance3 as any).__id); + expect(getInjectorOfInstance(instance1)!.id).toBe(injector.id); + expect(getInjectorOfInstance(instance2)!.id).toBe(injector.id); + expect(getInjectorOfInstance(instance3)!.id).toBe(injector.id); }); }); diff --git a/test/injector/dispose.test.ts b/tests/injector/dispose.test.ts similarity index 78% rename from test/injector/dispose.test.ts rename to tests/injector/dispose.test.ts index a673c9c..dd60b95 100644 --- a/test/injector/dispose.test.ts +++ b/tests/injector/dispose.test.ts @@ -44,7 +44,7 @@ describe('dispose', () => { const creator = injector.creatorMap.get(A); expect(creator!.status).toBe(CreatorStatus.init); - expect(creator!.instance).toBeUndefined(); + expect(creator!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -62,11 +62,11 @@ describe('dispose', () => { const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); const creatorB = injector.creatorMap.get(B); expect(creatorB!.status).toBe(CreatorStatus.init); - expect(creatorB!.instance).toBeUndefined(); + expect(creatorB!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -137,7 +137,7 @@ describe('dispose', () => { injector.disposeOne(A); const creatorA = injector.creatorMap.get(A)!; expect(creatorA.status).toBe(CreatorStatus.init); - expect(creatorA.instance).toBeUndefined(); + expect(creatorA.instances).toBeUndefined(); expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2); @@ -181,7 +181,7 @@ describe('dispose asynchronous', () => { const creator = injector.creatorMap.get(A); expect(creator!.status).toBe(CreatorStatus.init); - expect(creator!.instance).toBeUndefined(); + expect(creator!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -199,11 +199,11 @@ describe('dispose asynchronous', () => { const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); const creatorB = injector.creatorMap.get(B); expect(creatorB!.status).toBe(CreatorStatus.init); - expect(creatorB!.instance).toBeUndefined(); + expect(creatorB!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -250,6 +250,33 @@ describe('dispose asynchronous', () => { expect(spy).toBeCalledTimes(1); }); + it("dispose creator with multiple instance will call instances's dispose method", async () => { + const spy = jest.fn(); + + @Injectable({ multiple: true }) + class DisposeCls { + dispose = async () => { + spy(); + }; + } + + const instance = injector.get(DisposeCls); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(DisposeCls); + + const instance2 = injector.get(DisposeCls); + expect(injector.hasInstance(instance2)).toBeTruthy(); + expect(instance2).toBeInstanceOf(DisposeCls); + + await injector.disposeOne(DisposeCls); + expect(injector.hasInstance(instance)).toBeFalsy(); + expect(injector.hasInstance(instance2)).toBeFalsy(); + expect(spy).toBeCalledTimes(2); + + await injector.disposeOne(DisposeCls); + expect(spy).toBeCalledTimes(2); + }); + it("dispose an instance will also dispose it's instance", async () => { const spy = jest.fn(); @@ -278,9 +305,39 @@ describe('dispose asynchronous', () => { await injector.disposeOne(A); const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2); }); + + it('dispose should dispose instance of useFactory', () => { + const injector = new Injector(); + let aValue = 1; + const token = Symbol.for('A'); + + injector.addProviders( + ...[ + { + token, + useFactory: () => aValue, + }, + ], + ); + + @Injectable() + class B { + @Autowired(token) + a!: number; + } + + const instance = injector.get(B); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(B); + expect(instance.a).toBe(1); + + injector.disposeOne(token); + aValue = 2; + expect(instance.a).toBe(2); + }); }); diff --git a/test/injector/domain.test.ts b/tests/injector/domain.test.ts similarity index 100% rename from test/injector/domain.test.ts rename to tests/injector/domain.test.ts diff --git a/test/injector/dynamicMultiple.test.ts b/tests/injector/dynamicMultiple.test.ts similarity index 100% rename from test/injector/dynamicMultiple.test.ts rename to tests/injector/dynamicMultiple.test.ts diff --git a/test/injector/hasInstance.test.ts b/tests/injector/hasInstance.test.ts similarity index 52% rename from test/injector/hasInstance.test.ts rename to tests/injector/hasInstance.test.ts index 2607387..cb4e9e7 100644 --- a/test/injector/hasInstance.test.ts +++ b/tests/injector/hasInstance.test.ts @@ -16,30 +16,22 @@ describe('hasInstance', () => { it('能够通过 hasInstance 查到单例对象的存在性', () => { const token = 'token'; const instance = {}; - const provider = { token, useValue: instance }; - const injector = new Injector([provider, B, C]); - - expect(injector.hasInstance(instance)).toBe(true); - - const b = injector.get(B); - expect(injector.hasInstance(b)).toBe(true); - const c = injector.get(C); - expect(injector.hasInstance(c)).toBe(false); - }); + const token2 = 'token2'; + const instance2 = true; - it('hasInstance 支持 primitive 的判断', () => { - const token = 'token'; - const instance = true; const provider = { token, useValue: instance }; - const injector = new Injector([provider, B, C]); + const injector = new Injector([provider, B, C, { token: token2, useValue: instance2 }]); expect(injector.hasInstance(instance)).toBe(true); + // 支持 primitive 的判断 + expect(injector.hasInstance(instance2)).toBe(true); + const b = injector.get(B); expect(injector.hasInstance(b)).toBe(true); const c = injector.get(C); - expect(injector.hasInstance(c)).toBe(false); + expect(injector.hasInstance(c)).toBe(true); }); }); diff --git a/test/injector/overrideProviders.test.ts b/tests/injector/overrideProviders.test.ts similarity index 100% rename from test/injector/overrideProviders.test.ts rename to tests/injector/overrideProviders.test.ts diff --git a/test/injector/strictMode.test.ts b/tests/injector/strictMode.test.ts similarity index 100% rename from test/injector/strictMode.test.ts rename to tests/injector/strictMode.test.ts diff --git a/test/injector/tag.test.ts b/tests/injector/tag.test.ts similarity index 100% rename from test/injector/tag.test.ts rename to tests/injector/tag.test.ts diff --git a/test/providers/useAlias.test.ts b/tests/providers/useAlias.test.ts similarity index 100% rename from test/providers/useAlias.test.ts rename to tests/providers/useAlias.test.ts diff --git a/test/use-case.test.ts b/tests/use-case.test.ts similarity index 99% rename from test/use-case.test.ts rename to tests/use-case.test.ts index 612b740..68e41a9 100644 --- a/test/use-case.test.ts +++ b/tests/use-case.test.ts @@ -2,7 +2,7 @@ import { asSingleton, Autowired, Inject, Injectable, Injector } from '../src'; import * as Error from '../src/error'; -describe(__filename, () => { +describe('use cases', () => { it('使用 Autowired 动态注入依赖', () => { const spy = jest.fn(); @Injectable() diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..0ee6f8f --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "esm", + "module": "es2022", + "target": "es2022", + "lib": ["es2022"], + "declaration": true, + "declarationDir": "types" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index 398993f..928cc64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,16 +4,15 @@ "module": "commonjs", "lib": ["es2020"], "strict": true, - "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "pretty": false, "noEmitOnError": false, "noFallthroughCasesInSwitch": true, - "downlevelIteration": true, + "downlevelIteration": false, "esModuleInterop": true, "skipLibCheck": true, - "outDir": "dist" + "moduleResolution": "node" }, - "include": ["src", "test"] + "include": ["src", "tests"] } diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 0000000..8acda50 --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "es2015", + "lib": ["es2015"], + "outDir": "lib", + "declaration": false + }, + "include": ["src"] +} diff --git a/tsconfig.prod.json b/tsconfig.prod.json deleted file mode 100644 index ee2af5e..0000000 --- a/tsconfig.prod.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "target": "es5", - "lib": ["es2017"] - }, - "include": ["src"] -}