@avijs/di 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import { isToken, tokenName, type Constructor, type Token } from './token.js';
2
+
3
+ /**
4
+ * `singleton`: one instance per app. `transient`: a new one per injection. `request`: one per
5
+ * request, created on first use and disposed after the handler.
6
+ */
7
+ export type Scope = 'singleton' | 'transient' | 'request';
8
+
9
+ export interface ClassProvider<T = unknown> {
10
+ provide: Token<T>;
11
+ useClass: Constructor<T>;
12
+ scope?: Scope;
13
+ }
14
+
15
+ export interface ValueProvider<T = unknown> {
16
+ provide: Token<T>;
17
+ useValue: T;
18
+ }
19
+
20
+ export interface FactoryProvider<T = unknown> {
21
+ provide: Token<T>;
22
+ /** Receives `inject` in order. Only singleton factories may return a promise. */
23
+ useFactory: (...deps: never[]) => T | Promise<T>;
24
+ inject?: readonly Token[];
25
+ scope?: Scope;
26
+ /**
27
+ * Called with the created value on shutdown, or after the request for request scope. A method,
28
+ * so typed callbacks fit `Provider[]`.
29
+ */
30
+ dispose?(value: T): unknown;
31
+ }
32
+
33
+ export type Provider<T = unknown> =
34
+ Constructor<T> | ClassProvider<T> | ValueProvider<T> | FactoryProvider<T>;
35
+
36
+ export interface ProviderSpec {
37
+ readonly token: Token;
38
+ readonly kind: 'class' | 'value' | 'factory';
39
+ readonly scope: Scope;
40
+ readonly deps: readonly Token[];
41
+ readonly create: (deps: unknown[]) => unknown;
42
+ readonly dispose: ((value: unknown) => unknown) | undefined;
43
+ }
44
+
45
+ /** Validates a provider and converts it to one internal shape. `where` labels errors. */
46
+ export function normalizeProvider(provider: unknown, where: string): ProviderSpec {
47
+ if (typeof provider === 'function') {
48
+ return classSpec(provider as Constructor, provider as Constructor, 'singleton', where);
49
+ }
50
+ if (typeof provider !== 'object' || provider === null) {
51
+ throw new TypeError(`Invalid provider at ${where}: ${String(provider)}${hint(provider)}`);
52
+ }
53
+
54
+ const p = provider as Record<string, unknown>;
55
+ if (!isToken(p['provide'])) {
56
+ throw new TypeError(
57
+ `Invalid "provide" token at ${where}: ${String(p['provide'])}${hint(p['provide'])}`,
58
+ );
59
+ }
60
+ const token = p['provide'];
61
+ const label = `${where} (${tokenName(token)})`;
62
+ const kinds = ['useClass', 'useValue', 'useFactory'].filter((key) => key in p);
63
+ if (kinds.length !== 1) {
64
+ throw new TypeError(
65
+ `Provider at ${label} needs exactly one of useClass, useValue or useFactory`,
66
+ );
67
+ }
68
+
69
+ if ('useValue' in p) {
70
+ const value = p['useValue'];
71
+ return {
72
+ token,
73
+ kind: 'value',
74
+ scope: 'singleton',
75
+ deps: [],
76
+ create: () => value,
77
+ dispose: undefined,
78
+ };
79
+ }
80
+
81
+ const scope = p['scope'] ?? 'singleton';
82
+ if (scope !== 'singleton' && scope !== 'transient' && scope !== 'request') {
83
+ throw new TypeError(
84
+ `Invalid scope ${typeof scope === 'string' ? `"${scope}"` : typeof scope} at ${label}`,
85
+ );
86
+ }
87
+
88
+ if ('useClass' in p) {
89
+ if (typeof p['useClass'] !== 'function') {
90
+ throw new TypeError(`useClass at ${label} must be a class`);
91
+ }
92
+ return classSpec(token, p['useClass'] as Constructor, scope, label);
93
+ }
94
+
95
+ const factory = p['useFactory'];
96
+ if (typeof factory !== 'function')
97
+ throw new TypeError(`useFactory at ${label} must be a function`);
98
+ const deps = checkInject(p['inject'], `${label} inject`);
99
+ if (factory.length > deps.length) {
100
+ throw new TypeError(
101
+ `Factory at ${label} takes ${String(factory.length)} arguments but inject lists ${String(deps.length)}`,
102
+ );
103
+ }
104
+ const dispose = p['dispose'];
105
+ if (dispose !== undefined && typeof dispose !== 'function') {
106
+ throw new TypeError(`dispose at ${label} must be a function`);
107
+ }
108
+ return {
109
+ token,
110
+ kind: 'factory',
111
+ scope,
112
+ deps,
113
+ create: (values) => (factory as (...args: unknown[]) => unknown)(...values),
114
+ dispose: dispose as ((value: unknown) => unknown) | undefined,
115
+ };
116
+ }
117
+
118
+ function classSpec(token: Token, cls: Constructor, scope: Scope, where: string): ProviderSpec {
119
+ const deps = checkInject(cls.inject, `${where} ${cls.name}.inject`);
120
+ if (cls.length > deps.length) {
121
+ throw new TypeError(
122
+ `${cls.name || 'Class'} takes ${String(cls.length)} constructor arguments but static inject lists ${String(deps.length)} (at ${where})`,
123
+ );
124
+ }
125
+ const Ctor = cls as unknown as new (...args: unknown[]) => unknown;
126
+ return {
127
+ token,
128
+ kind: 'class',
129
+ scope,
130
+ deps,
131
+ create: (values) => new Ctor(...values),
132
+ dispose: undefined,
133
+ };
134
+ }
135
+
136
+ function checkInject(inject: unknown, where: string): readonly Token[] {
137
+ if (inject === undefined) return [];
138
+ if (!Array.isArray(inject)) throw new TypeError(`${where} must be an array of tokens`);
139
+ inject.forEach((token: unknown, i) => {
140
+ if (!isToken(token)) {
141
+ throw new TypeError(
142
+ `Invalid token at ${where}[${String(i)}]: ${String(token)}${hint(token)}`,
143
+ );
144
+ }
145
+ });
146
+ return inject as readonly Token[];
147
+ }
148
+
149
+ function hint(value: unknown): string {
150
+ return value === undefined ? ' (is there a circular import?)' : '';
151
+ }
package/src/token.ts ADDED
@@ -0,0 +1,44 @@
1
+ export type Class<T = unknown> = abstract new (...args: never[]) => T;
2
+
3
+ /** A class Avi can construct. Dependencies are listed in `static inject`. */
4
+ export type Constructor<T = unknown> = (new (...args: never[]) => T) & {
5
+ readonly inject?: readonly Token[];
6
+ };
7
+
8
+ declare const tokenType: unique symbol;
9
+
10
+ /** A typed, non-class token. Created with `createToken<T>()`. */
11
+ export interface InjectionToken<T> {
12
+ readonly description: string;
13
+ readonly [tokenType]?: T;
14
+ }
15
+
16
+ export type Token<T = unknown> = Class<T> | InjectionToken<T>;
17
+
18
+ export type TokenType<K> =
19
+ K extends Class<infer T> ? T : K extends InjectionToken<infer T> ? T : never;
20
+
21
+ /** Maps an `inject` tuple to the tuple of values it resolves to. */
22
+ export type ResolveTokens<K extends readonly Token[]> = {
23
+ -readonly [I in keyof K]: TokenType<K[I]>;
24
+ };
25
+
26
+ export function createToken<T>(description: string): InjectionToken<T> {
27
+ if (typeof description !== 'string' || description === '') {
28
+ throw new TypeError('createToken needs a non-empty description');
29
+ }
30
+ return Object.freeze({ description });
31
+ }
32
+
33
+ export function isToken(value: unknown): value is Token {
34
+ return (
35
+ typeof value === 'function' ||
36
+ (typeof value === 'object' &&
37
+ value !== null &&
38
+ typeof (value as { description?: unknown }).description === 'string')
39
+ );
40
+ }
41
+
42
+ export function tokenName(token: Token): string {
43
+ return typeof token === 'function' ? token.name || '(anonymous class)' : token.description;
44
+ }