Skip to content

Commit 094d30e

Browse files
authored
feat: add @service decorator (#72)
* feat: add @service decorator * fix: eslint
1 parent 377e6bb commit 094d30e

File tree

5 files changed

+24
-25
lines changed

5 files changed

+24
-25
lines changed

spec/inject.test.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-shadow */
2-
import {
3-
popInjectionContext,
4-
pushInjectionContext,
5-
registerValue,
6-
registerScopedValue,
7-
registerService,
8-
setupScope,
9-
use,
10-
registerServiceWithFactory,
11-
} from "../src";
2+
import { popInjectionContext, pushInjectionContext, registerValue, registerScopedValue, registerService, setupScope, use } from "../src";
3+
import { Service } from "../src/service";
124

135
describe("inject", () => {
146
beforeEach(pushInjectionContext);
@@ -52,6 +44,7 @@ describe("inject", () => {
5244
it("registers a service as transient", () => {
5345
let constructorCalledTimes = 0;
5446

47+
@Service("transient")
5548
class TestService {
5649
public id = Math.random();
5750

@@ -60,8 +53,6 @@ describe("inject", () => {
6053
}
6154
}
6255

63-
registerService("transient", TestService);
64-
6556
expect(constructorCalledTimes).toBe(0);
6657

6758
const instance1 = use(TestService);
@@ -429,19 +420,19 @@ describe("inject", () => {
429420
});
430421

431422
it("allow registering a service with a custom factory function", () => {
432-
class A {
433-
id = 0;
434-
}
435-
436423
const factory = jest.fn(() => {
424+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
437425
const instance = new A();
438426

439427
instance.id = 1;
440428

441429
return instance;
442430
});
443431

444-
registerServiceWithFactory("singleton", A, factory);
432+
@Service("singleton", factory)
433+
class A {
434+
id = 0;
435+
}
445436

446437
expect(use(A)).toBeInstanceOf(A);
447438
expect(use(A).id).toBe(1);

src/global-context.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { randomBytes } from "crypto";
22

3-
import type { Service } from "./service";
3+
import type { ServiceSpec } from "./service";
44

55
export class GlobalContext {
66
id = randomBytes(32).toString("hex");
77

88
constructor(public readonly parent?: GlobalContext) {}
99

10-
private readonly services = new Map<string, Service>();
10+
private readonly services = new Map<string, ServiceSpec>();
1111

1212
private readonly singletonInstances = new Map<string, unknown>();
1313

1414
private readonly values = new Map<string, unknown>();
1515

16-
getService(name: string): Service | undefined {
16+
getService(name: string): ServiceSpec | undefined {
1717
let service = this.services.get(name);
1818

1919
if (service) {
@@ -33,7 +33,7 @@ export class GlobalContext {
3333
return this.services.has(name);
3434
}
3535

36-
setService(name: string, value: Service) {
36+
setService(name: string, value: ServiceSpec) {
3737
this.services.set(name, value);
3838
}
3939

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { pushInjectionContext, popInjectionContext } from "./global-context";
22
export { setupScope } from "./scope-context";
3-
export { registerService, registerServiceWithFactory } from "./service";
3+
export { registerService, registerServiceWithFactory, Service } from "./service";
44
export { registerScopedValue, registerValue } from "./value";
55
export { use } from "./use";
66

src/service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getCurrentScope } from "./scope-context";
66
export type ServiceType = new (...args: any) => any;
77
export type ServiceLifetime = "singleton" | "scoped" | "transient";
88

9-
export interface Service {
9+
export interface ServiceSpec {
1010
type: ServiceType;
1111
factory(): unknown;
1212
lifetime: ServiceLifetime;
@@ -15,7 +15,7 @@ export interface Service {
1515
const serviceConstructionStack: Function[] = [];
1616
const serviceConstructionLookup = new Set<Function>();
1717

18-
function createServiceInstance(service: Service) {
18+
function createServiceInstance(service: ServiceSpec) {
1919
serviceConstructionStack.push(service.type);
2020

2121
if (serviceConstructionLookup.has(service.type)) {
@@ -48,6 +48,13 @@ export function registerService<T extends ServiceType>(lifetime: ServiceLifetime
4848
registerServiceWithFactory(lifetime, type, () => new type(...(ctorArgs as unknown[])));
4949
}
5050

51+
export function Service(lifetime: ServiceLifetime, factory?: () => unknown) {
52+
return <T extends ServiceType>(type: T) => {
53+
registerServiceWithFactory(lifetime, type, factory ? (factory as () => InstanceType<T>) : () => new type());
54+
return type;
55+
};
56+
}
57+
5158
export function useService<T extends ServiceType>(type: T): InstanceType<T> {
5259
const service = getGlobalContext().getService(type.name);
5360

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"declaration": true,
66
"outDir": "./dist",
77
"strict": true,
8-
"esModuleInterop": true
8+
"esModuleInterop": true,
9+
"experimentalDecorators": true
910
},
1011
"files": [
1112
"src/index.ts",

0 commit comments

Comments
 (0)