alloy-di 1.2.3 → 1.4.0
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.
- package/README.md +1 -1
- package/dist/lib/container.d.ts +22 -5
- package/dist/lib/container.js +85 -25
- package/dist/lib/decorators.d.ts +1 -1
- package/dist/lib/env-detection.d.ts +24 -0
- package/dist/lib/env-detection.js +22 -9
- package/dist/lib/providers.d.ts +1 -1
- package/dist/lib/scope.d.ts +19 -2
- package/dist/plugins/core/codegen.js +118 -14
- package/dist/plugins/{vite-plugin → core}/container-loader.js +26 -13
- package/dist/plugins/core/decorators.js +12 -22
- package/dist/plugins/{vite-plugin → core}/discovery-runtime.js +4 -14
- package/dist/plugins/core/discovery-store.js +15 -0
- package/dist/plugins/{vite-plugin → core}/manifest-utils.js +14 -4
- package/dist/plugins/core/scanner.js +12 -0
- package/dist/plugins/core/scopes-validation.d.ts +19 -0
- package/dist/plugins/core/scopes-validation.js +225 -0
- package/dist/plugins/core/types.d.ts +9 -4
- package/dist/plugins/core/utils.js +45 -8
- package/dist/plugins/{vite-plugin → core}/visualization-utils.d.ts +1 -1
- package/dist/plugins/{vite-plugin → core}/visualization-utils.js +1 -1
- package/dist/plugins/{vite-plugin → core}/visualizer.d.ts +3 -3
- package/dist/plugins/{vite-plugin → core}/visualizer.js +92 -20
- package/dist/plugins/rollup-plugin/barrel-exports.js +51 -0
- package/dist/plugins/rollup-plugin/dependency-resolution.js +42 -0
- package/dist/plugins/rollup-plugin/index.d.ts +2 -23
- package/dist/plugins/rollup-plugin/index.js +15 -128
- package/dist/plugins/rollup-plugin/manifest-deps.js +48 -0
- package/dist/plugins/rollup-plugin/package-exports.js +20 -0
- package/dist/plugins/rollup-plugin/types.d.ts +36 -0
- package/dist/plugins/vite-plugin/index.d.ts +10 -2
- package/dist/plugins/vite-plugin/index.js +9 -4
- package/dist/plugins/vite-plugin/module-invalidation.js +13 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +3 -1
- package/dist/scopes.d.ts +66 -0
- package/dist/scopes.js +141 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# alloy-di
|
|
2
2
|
|
|
3
|
-
`alloy-di` is a
|
|
3
|
+
`alloy-di` is a build-time dependency injection toolkit for Vite. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
|
|
4
4
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
package/dist/lib/container.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Constructor, Newable, Token } from "./types.js";
|
|
2
|
+
import { ResolutionContext, ServiceScope } from "./scope.js";
|
|
2
3
|
import { ServiceIdentifier } from "./service-identifiers.js";
|
|
3
4
|
|
|
4
5
|
//#region src/lib/container.d.ts
|
|
@@ -8,13 +9,23 @@ import { ServiceIdentifier } from "./service-identifiers.js";
|
|
|
8
9
|
* It stores metadata discovered at build time, resolves constructor dependencies,
|
|
9
10
|
* performs singleton caching, and supports token-based value providers.
|
|
10
11
|
*/
|
|
11
|
-
declare class Container {
|
|
12
|
+
declare class Container implements ResolutionContext {
|
|
12
13
|
private readonly singletons;
|
|
13
14
|
private readonly pendingSingletons;
|
|
14
15
|
private readonly instanceOverrides;
|
|
15
16
|
private readonly metadataCache;
|
|
16
17
|
private readonly valueProviders;
|
|
17
18
|
private readonly factoryWarningCache;
|
|
19
|
+
private readonly scopeHierarchy;
|
|
20
|
+
readonly scopeName: ServiceScope;
|
|
21
|
+
readonly parent: ResolutionContext | null;
|
|
22
|
+
getCached(target: Constructor): unknown;
|
|
23
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
24
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
25
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
26
|
+
deletePending(target: Constructor): void;
|
|
27
|
+
getProvider(tokenId: symbol): unknown;
|
|
28
|
+
hasProvider(tokenId: symbol): boolean;
|
|
18
29
|
/**
|
|
19
30
|
* Resolve (and construct) the requested service.
|
|
20
31
|
*
|
|
@@ -23,6 +34,14 @@ declare class Container {
|
|
|
23
34
|
*/
|
|
24
35
|
get<T>(target: Newable<T>): Promise<T>;
|
|
25
36
|
get<T>(identifier: ServiceIdentifier<T>): Promise<T>;
|
|
37
|
+
/** @internal */
|
|
38
|
+
_registerScopeHierarchy(hierarchy: Record<string, string>): void;
|
|
39
|
+
/** @internal */
|
|
40
|
+
_validateScopeParent(childScope: ServiceScope, parentScope: ServiceScope): void;
|
|
41
|
+
/** @internal */
|
|
42
|
+
_resolveInContext<T>(target: Newable<T> | ServiceIdentifier<T>, context: ResolutionContext, options?: {
|
|
43
|
+
skipFactoryWarning?: boolean;
|
|
44
|
+
}): Promise<T>;
|
|
26
45
|
/**
|
|
27
46
|
* Provide a concrete instance override for a class constructor.
|
|
28
47
|
* Used by test utilities to inject mocks/stubs without altering global metadata.
|
|
@@ -62,10 +81,8 @@ declare class Container {
|
|
|
62
81
|
* @throws Error if a circular dependency is detected
|
|
63
82
|
*/
|
|
64
83
|
private resolve;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*/
|
|
68
|
-
private resolveSingleton;
|
|
84
|
+
private findContextForScope;
|
|
85
|
+
private resolveCached;
|
|
69
86
|
/**
|
|
70
87
|
* Instantiate a class by resolving and injecting all declared dependencies.
|
|
71
88
|
* Handles factory-lazy services by importing the real class before instantiation.
|
package/dist/lib/container.js
CHANGED
|
@@ -43,9 +43,51 @@ var Container = class {
|
|
|
43
43
|
metadataCache = /* @__PURE__ */ new Map();
|
|
44
44
|
valueProviders = /* @__PURE__ */ new Map();
|
|
45
45
|
factoryWarningCache = /* @__PURE__ */ new WeakSet();
|
|
46
|
+
scopeHierarchy = /* @__PURE__ */ new Map();
|
|
47
|
+
scopeName = ServiceScope.SINGLETON;
|
|
48
|
+
parent = null;
|
|
49
|
+
getCached(target) {
|
|
50
|
+
return this.singletons.get(target);
|
|
51
|
+
}
|
|
52
|
+
setCached(target, instance) {
|
|
53
|
+
this.singletons.set(target, instance);
|
|
54
|
+
}
|
|
55
|
+
getPending(target) {
|
|
56
|
+
return this.pendingSingletons.get(target);
|
|
57
|
+
}
|
|
58
|
+
setPending(target, promise) {
|
|
59
|
+
this.pendingSingletons.set(target, promise);
|
|
60
|
+
}
|
|
61
|
+
deletePending(target) {
|
|
62
|
+
this.pendingSingletons.delete(target);
|
|
63
|
+
}
|
|
64
|
+
getProvider(tokenId) {
|
|
65
|
+
return this.valueProviders.get(tokenId);
|
|
66
|
+
}
|
|
67
|
+
hasProvider(tokenId) {
|
|
68
|
+
return this.valueProviders.has(tokenId);
|
|
69
|
+
}
|
|
46
70
|
async get(targetOrIdentifier) {
|
|
47
71
|
if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
|
|
48
|
-
return this.getByConstructor(targetOrIdentifier);
|
|
72
|
+
return this.getByConstructor(targetOrIdentifier, this);
|
|
73
|
+
}
|
|
74
|
+
/** @internal */
|
|
75
|
+
_registerScopeHierarchy(hierarchy) {
|
|
76
|
+
for (const [child, parent] of Object.entries(hierarchy)) this.scopeHierarchy.set(child, parent);
|
|
77
|
+
}
|
|
78
|
+
/** @internal */
|
|
79
|
+
_validateScopeParent(childScope, parentScope) {
|
|
80
|
+
const declaredParent = this.scopeHierarchy.get(childScope);
|
|
81
|
+
if (declaredParent && declaredParent !== parentScope) throw new Error(`[alloy] Invalid scope hierarchy construction: scope '${String(childScope)}' is declared with parent '${String(declaredParent)}', but was constructed with parent scope '${String(parentScope)}'.`);
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
async _resolveInContext(target, context, options) {
|
|
85
|
+
if (typeof target === "symbol") {
|
|
86
|
+
const ctor = getConstructorByIdentifier(target);
|
|
87
|
+
if (!ctor) throw new Error(`No service registered for identifier ${target.description ?? target.toString()}`);
|
|
88
|
+
return this.getByConstructor(ctor, context, { skipFactoryWarning: true });
|
|
89
|
+
}
|
|
90
|
+
return this.getByConstructor(target, context, options);
|
|
49
91
|
}
|
|
50
92
|
/**
|
|
51
93
|
* Provide a concrete instance override for a class constructor.
|
|
@@ -69,7 +111,7 @@ var Container = class {
|
|
|
69
111
|
async getByIdentifier(identifier) {
|
|
70
112
|
const ctor = getConstructorByIdentifier(identifier);
|
|
71
113
|
if (!ctor) throw new Error(`No service registered for identifier ${identifier.description ?? identifier.toString()}`);
|
|
72
|
-
return this.getByConstructor(ctor, { skipFactoryWarning: true });
|
|
114
|
+
return this.getByConstructor(ctor, this, { skipFactoryWarning: true });
|
|
73
115
|
}
|
|
74
116
|
/**
|
|
75
117
|
* Register a concrete value for an injection token at runtime.
|
|
@@ -88,9 +130,9 @@ var Container = class {
|
|
|
88
130
|
if (!this.valueProviders.has(token.id)) throw new Error(`No provider registered for token ${token.description ?? String(token.id)}`);
|
|
89
131
|
return this.valueProviders.get(token.id);
|
|
90
132
|
}
|
|
91
|
-
async getByConstructor(target, options) {
|
|
133
|
+
async getByConstructor(target, context, options) {
|
|
92
134
|
if (!options?.skipFactoryWarning) this.maybeWarnFactoryLazyConstructorUsage(target);
|
|
93
|
-
return this.resolve(target, []);
|
|
135
|
+
return this.resolve(target, [], context);
|
|
94
136
|
}
|
|
95
137
|
maybeWarnFactoryLazyConstructorUsage(target) {
|
|
96
138
|
if (!isDevEnvironment()) return;
|
|
@@ -107,7 +149,7 @@ var Container = class {
|
|
|
107
149
|
* @returns Promise resolving to the service instance
|
|
108
150
|
* @throws Error if a circular dependency is detected
|
|
109
151
|
*/
|
|
110
|
-
async resolve(target, resolutionStack) {
|
|
152
|
+
async resolve(target, resolutionStack, context) {
|
|
111
153
|
const overridden = this.instanceOverrides.get(target);
|
|
112
154
|
if (overridden) return overridden;
|
|
113
155
|
if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
|
|
@@ -117,26 +159,40 @@ var Container = class {
|
|
|
117
159
|
});
|
|
118
160
|
const metadata = this.getServiceMetadata(target);
|
|
119
161
|
const nextStack = [...resolutionStack, target];
|
|
120
|
-
|
|
121
|
-
return this.
|
|
162
|
+
const targetCtx = this.findContextForScope(metadata.scope, context);
|
|
163
|
+
if (metadata.scope === ServiceScope.SINGLETON) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
164
|
+
if (metadata.scope === ServiceScope.TRANSIENT) return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
165
|
+
if (targetCtx.scopeName === metadata.scope) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
166
|
+
return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
122
167
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
findContextForScope(scope, startingContext) {
|
|
169
|
+
if (scope === ServiceScope.SINGLETON) {
|
|
170
|
+
let current = startingContext;
|
|
171
|
+
while (current.parent) current = current.parent;
|
|
172
|
+
return current;
|
|
173
|
+
}
|
|
174
|
+
if (scope === ServiceScope.TRANSIENT) return startingContext;
|
|
175
|
+
let current = startingContext;
|
|
176
|
+
while (current) {
|
|
177
|
+
if (current.scopeName === scope) return current;
|
|
178
|
+
current = current.parent;
|
|
179
|
+
}
|
|
180
|
+
return startingContext;
|
|
181
|
+
}
|
|
182
|
+
async resolveCached(target, metadata, resolutionStack, targetCtx) {
|
|
183
|
+
const cached = targetCtx.getCached(target);
|
|
128
184
|
if (cached) return cached;
|
|
129
|
-
const pending =
|
|
185
|
+
const pending = targetCtx.getPending(target);
|
|
130
186
|
if (pending) return await pending;
|
|
131
|
-
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory).then((instance) => {
|
|
132
|
-
|
|
187
|
+
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory, targetCtx).then((instance) => {
|
|
188
|
+
targetCtx.setCached(target, instance);
|
|
133
189
|
return instance;
|
|
134
190
|
});
|
|
135
|
-
|
|
191
|
+
targetCtx.setPending(target, creation);
|
|
136
192
|
try {
|
|
137
193
|
return await creation;
|
|
138
194
|
} finally {
|
|
139
|
-
|
|
195
|
+
targetCtx.deletePending(target);
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
198
|
/**
|
|
@@ -149,9 +205,9 @@ var Container = class {
|
|
|
149
205
|
* @param factory - Optional lazy factory to import the real class
|
|
150
206
|
* @returns Promise resolving to the instantiated service
|
|
151
207
|
*/
|
|
152
|
-
async createInstance(target, dependencies, resolutionStack, factory) {
|
|
208
|
+
async createInstance(target, dependencies, resolutionStack, factory, context) {
|
|
153
209
|
const ctor = factory ? await this.importWithRetry(factory, target, resolutionStack) : target;
|
|
154
|
-
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack))));
|
|
210
|
+
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack, context))));
|
|
155
211
|
}
|
|
156
212
|
/**
|
|
157
213
|
* Resolve a single dependency entry, handling lazies, tokens, and constructors.
|
|
@@ -163,7 +219,7 @@ var Container = class {
|
|
|
163
219
|
* @returns Promise resolving to the dependency instance
|
|
164
220
|
* @throws Error if dependency type is invalid
|
|
165
221
|
*/
|
|
166
|
-
async resolveParam(param, target, resolutionStack) {
|
|
222
|
+
async resolveParam(param, target, resolutionStack, context) {
|
|
167
223
|
const classification = classifyDependency(param);
|
|
168
224
|
if (!classification) {
|
|
169
225
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
@@ -176,10 +232,10 @@ var Container = class {
|
|
|
176
232
|
switch (classification.kind) {
|
|
177
233
|
case "lazy": {
|
|
178
234
|
const depClass = await this.importWithRetry(classification.lazy, target, resolutionStack);
|
|
179
|
-
return this.resolve(depClass, resolutionStack);
|
|
235
|
+
return this.resolve(depClass, resolutionStack, context);
|
|
180
236
|
}
|
|
181
|
-
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack);
|
|
182
|
-
case "constructor": return this.resolve(classification.ctor, resolutionStack);
|
|
237
|
+
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack, context);
|
|
238
|
+
case "constructor": return this.resolve(classification.ctor, resolutionStack, context);
|
|
183
239
|
}
|
|
184
240
|
return classification;
|
|
185
241
|
}
|
|
@@ -238,8 +294,12 @@ var Container = class {
|
|
|
238
294
|
/**
|
|
239
295
|
* Resolve a token dependency via registered value providers.
|
|
240
296
|
*/
|
|
241
|
-
resolveTokenLike(tok, target, resolutionStack) {
|
|
242
|
-
|
|
297
|
+
resolveTokenLike(tok, target, resolutionStack, context) {
|
|
298
|
+
let current = context;
|
|
299
|
+
while (current) {
|
|
300
|
+
if (current.hasProvider(tok.id)) return current.getProvider(tok.id);
|
|
301
|
+
current = current.parent;
|
|
302
|
+
}
|
|
243
303
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
244
304
|
throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
|
|
245
305
|
target,
|
package/dist/lib/decorators.d.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/lib/env-detection.d.ts
|
|
2
|
+
type ImportMetaEnvShape = {
|
|
3
|
+
MODE?: string;
|
|
4
|
+
PROD?: boolean;
|
|
5
|
+
NODE_ENV?: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
type EnvDetectionOverrides = {
|
|
9
|
+
/**
|
|
10
|
+
* Explicit import.meta.env replacement. Use `null` to force "no env" behavior.
|
|
11
|
+
*/
|
|
12
|
+
importMetaEnv?: ImportMetaEnvShape | null;
|
|
13
|
+
/**
|
|
14
|
+
* Explicit NODE_ENV replacement. Use `null` to ignore process.env.
|
|
15
|
+
*/
|
|
16
|
+
nodeEnv?: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Short-circuit the entire detection logic with a predetermined boolean.
|
|
19
|
+
*/
|
|
20
|
+
isDev?: boolean;
|
|
21
|
+
};
|
|
22
|
+
declare function setEnvDetectionOverrides(overrides: EnvDetectionOverrides | undefined): void;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { EnvDetectionOverrides, setEnvDetectionOverrides };
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return typeof value === "object" && value !== null;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Build-time-injected detection overrides.
|
|
7
|
+
*
|
|
8
|
+
* The Vite plugin emits a `setEnvDetectionOverrides({ isDev: ... })` call
|
|
9
|
+
* into the generated container module so detection in plugin-driven setups
|
|
10
|
+
* uses the bundler's authoritative mode instead of runtime sniffing.
|
|
11
|
+
*/
|
|
12
|
+
let injectedOverrides;
|
|
13
|
+
function setEnvDetectionOverrides(overrides) {
|
|
14
|
+
injectedOverrides = overrides;
|
|
15
|
+
}
|
|
5
16
|
function readImportMetaEnvFromRuntime() {
|
|
6
17
|
if (typeof import.meta === "undefined") return;
|
|
7
|
-
const
|
|
8
|
-
if (!isRecord(candidate) || !("env" in candidate)) return;
|
|
9
|
-
const envValue = candidate.env;
|
|
18
|
+
const envValue = import.meta.env;
|
|
10
19
|
if (!isRecord(envValue)) return;
|
|
11
20
|
const env = {};
|
|
12
21
|
if (typeof envValue.MODE === "string") env.MODE = envValue.MODE;
|
|
@@ -15,8 +24,11 @@ function readImportMetaEnvFromRuntime() {
|
|
|
15
24
|
return env;
|
|
16
25
|
}
|
|
17
26
|
function readProcessNodeEnv() {
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
try {
|
|
28
|
+
return "development";
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
function getImportMetaEnv(overrides) {
|
|
22
34
|
if (overrides?.importMetaEnv === null) return;
|
|
@@ -29,14 +41,15 @@ function getNodeEnv(overrides) {
|
|
|
29
41
|
return readProcessNodeEnv();
|
|
30
42
|
}
|
|
31
43
|
function isDevEnvironment(overrides) {
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const effective = overrides ?? injectedOverrides;
|
|
45
|
+
if (typeof effective?.isDev === "boolean") return effective.isDev;
|
|
46
|
+
const nodeEnv = getNodeEnv(effective);
|
|
34
47
|
if (typeof nodeEnv === "string") return nodeEnv !== "production";
|
|
35
|
-
const importMetaEnv = getImportMetaEnv(
|
|
48
|
+
const importMetaEnv = getImportMetaEnv(effective);
|
|
36
49
|
if (typeof importMetaEnv?.PROD === "boolean") return !importMetaEnv.PROD;
|
|
37
50
|
if (typeof importMetaEnv?.MODE === "string") return importMetaEnv.MODE !== "production";
|
|
38
51
|
if (typeof importMetaEnv?.NODE_ENV === "string") return importMetaEnv.NODE_ENV !== "production";
|
|
39
52
|
return true;
|
|
40
53
|
}
|
|
41
54
|
//#endregion
|
|
42
|
-
export { isDevEnvironment };
|
|
55
|
+
export { isDevEnvironment, setEnvDetectionOverrides };
|
package/dist/lib/providers.d.ts
CHANGED
package/dist/lib/scope.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import { Constructor } from "./types.js";
|
|
2
|
+
|
|
1
3
|
//#region src/lib/scope.d.ts
|
|
2
4
|
declare const ServiceScope: {
|
|
3
5
|
readonly SINGLETON: "singleton";
|
|
4
6
|
readonly TRANSIENT: "transient";
|
|
5
7
|
};
|
|
6
|
-
|
|
8
|
+
interface AlloyScopes {
|
|
9
|
+
singleton: true;
|
|
10
|
+
transient: true;
|
|
11
|
+
}
|
|
12
|
+
type ServiceScope = keyof AlloyScopes;
|
|
13
|
+
interface ResolutionContext {
|
|
14
|
+
readonly scopeName: ServiceScope;
|
|
15
|
+
getCached(target: Constructor): unknown;
|
|
16
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
17
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
18
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
19
|
+
deletePending(target: Constructor): void;
|
|
20
|
+
getProvider(tokenId: symbol): unknown;
|
|
21
|
+
hasProvider(tokenId: symbol): boolean;
|
|
22
|
+
readonly parent: ResolutionContext | null;
|
|
23
|
+
}
|
|
7
24
|
//#endregion
|
|
8
|
-
export { ServiceScope };
|
|
25
|
+
export { AlloyScopes, ResolutionContext, ServiceScope };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClassKey, createSymbolKey, hashString, normalizeImportPath } from "./utils.js";
|
|
2
2
|
import { IdentifierResolver } from "./identifier-resolver.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
4
5
|
//#region src/plugins/core/codegen.ts
|
|
5
6
|
function escapeSingleQuotes(value) {
|
|
6
7
|
return value.replaceAll("'", "\\'");
|
|
@@ -91,12 +92,72 @@ function resolveDependencyImports(metas, pool, serviceBindings, runtimeImports)
|
|
|
91
92
|
importMap
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* True when an identifier occupies a name position rather than referencing a
|
|
97
|
+
* binding: property-access names (`ns.Api`), object keys — including method
|
|
98
|
+
* and accessor keys (`{ Api() {} }`, `{ get Api() {} }`) — class member
|
|
99
|
+
* names, qualified names, and destructuring property names.
|
|
100
|
+
*/
|
|
101
|
+
function isNonReferencePosition(node) {
|
|
102
|
+
const parent = node.parent;
|
|
103
|
+
if (ts.isPropertyAccessExpression(parent)) return parent.name === node;
|
|
104
|
+
if (ts.isQualifiedName(parent)) return parent.right === node;
|
|
105
|
+
if (ts.isBindingElement(parent)) return parent.propertyName === node;
|
|
106
|
+
if (ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodSignature(parent) || ts.isPropertySignature(parent)) return parent.name === node;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Decide how to rewrite one identifier node, or skip it.
|
|
111
|
+
*
|
|
112
|
+
* Only binding references are rewritten: name positions and string/comment
|
|
113
|
+
* content keep their text. Shorthand properties expand
|
|
114
|
+
* (`{ Api }` -> `{ Api: Api_1 }`) so the key survives the rename.
|
|
115
|
+
*/
|
|
116
|
+
function createIdentifierEdit(node, source, replacement) {
|
|
117
|
+
const parent = node.parent;
|
|
118
|
+
if (isNonReferencePosition(node)) return;
|
|
119
|
+
const start = node.getStart(source);
|
|
120
|
+
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return {
|
|
121
|
+
start,
|
|
122
|
+
end: node.end,
|
|
123
|
+
text: `${node.text}: ${replacement}`
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
start,
|
|
127
|
+
end: node.end,
|
|
128
|
+
text: replacement
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Rewrite identifier references inside a reconstructed dependency expression.
|
|
133
|
+
*
|
|
134
|
+
* The expression is parsed and identifier nodes are replaced by position, so
|
|
135
|
+
* `$`-prefixed names rewrite correctly and occurrences inside string literals
|
|
136
|
+
* (e.g. lazy `import('/src/Api')` specifiers) and comments are untouched —
|
|
137
|
+
* the previous `\b`-regex text replacement got both wrong.
|
|
138
|
+
*/
|
|
139
|
+
function rewriteReferencedIdentifiers(expression, referenced, rewriter) {
|
|
140
|
+
const wrapped = `(${expression});`;
|
|
141
|
+
const source = ts.createSourceFile("alloy-dependency-expression.ts", wrapped, ts.ScriptTarget.ESNext, true);
|
|
142
|
+
const edits = [];
|
|
143
|
+
const visit = (node) => {
|
|
144
|
+
if (ts.isIdentifier(node) && referenced.has(node.text)) {
|
|
145
|
+
const replacement = rewriter(node.text);
|
|
146
|
+
if (replacement && replacement !== node.text) {
|
|
147
|
+
const edit = createIdentifierEdit(node, source, replacement);
|
|
148
|
+
if (edit) edits.push(edit);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
ts.forEachChild(node, visit);
|
|
152
|
+
};
|
|
153
|
+
visit(source);
|
|
154
|
+
let result = wrapped;
|
|
155
|
+
for (const edit of edits.toSorted((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
156
|
+
return result.slice(1, -2);
|
|
157
|
+
}
|
|
94
158
|
function reconstructDependencyExpression(dep, rewriter, contextDir) {
|
|
95
159
|
let expr = dep.expression;
|
|
96
|
-
|
|
97
|
-
const replacement = rewriter(ident);
|
|
98
|
-
if (replacement && replacement !== ident) expr = expr.replaceAll(new RegExp(`\\b${ident}\\b`, "g"), replacement);
|
|
99
|
-
}
|
|
160
|
+
if (dep.referencedIdentifiers.length > 0) expr = rewriteReferencedIdentifiers(expr, new Set(dep.referencedIdentifiers), rewriter);
|
|
100
161
|
if (dep.isLazy) expr = expr.replaceAll(/import\s*\(\s*(['"])(.+?)\1\s*\)/g, (match, quote, importPath) => {
|
|
101
162
|
if (importPath.startsWith(".")) return `import(${quote}${normalizeImportPath(path.resolve(contextDir, importPath))}${quote})`;
|
|
102
163
|
return match;
|
|
@@ -128,11 +189,11 @@ function reconstructOptionsText(meta, importMap, serviceRenames) {
|
|
|
128
189
|
if (parts.length === 0) return "{}";
|
|
129
190
|
return `{ ${parts.join(", ")} }`;
|
|
130
191
|
}
|
|
131
|
-
function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths) {
|
|
192
|
+
function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths, options) {
|
|
132
193
|
const hasProviderModules = providerModulePaths.length > 0;
|
|
133
194
|
const activeMetas = filterActiveMetas(metas, lazyReferencedClassKeys);
|
|
134
195
|
const resolver = new IdentifierResolver(activeMetas);
|
|
135
|
-
const runtimeImports = computeRuntimeImports(activeMetas, hasProviderModules);
|
|
196
|
+
const runtimeImports = computeRuntimeImports(activeMetas, hasProviderModules, options?.isDev !== void 0);
|
|
136
197
|
const pool = createNamePool([
|
|
137
198
|
...runtimeImports,
|
|
138
199
|
"registrations",
|
|
@@ -189,8 +250,9 @@ function enrichRegistrations(activeMetas, naming) {
|
|
|
189
250
|
};
|
|
190
251
|
});
|
|
191
252
|
}
|
|
192
|
-
function computeRuntimeImports(activeMetas, hasProviderModules) {
|
|
253
|
+
function computeRuntimeImports(activeMetas, hasProviderModules, hasEnvOverrides = false) {
|
|
193
254
|
const imports = new Set(["Container", "dependenciesRegistry"]);
|
|
255
|
+
if (hasEnvOverrides) imports.add("setEnvDetectionOverrides");
|
|
194
256
|
const needsLazyImport = activeMetas.some((m) => m.metadata.dependencies.some((d) => d.isLazy) || !!m.metadata.factory);
|
|
195
257
|
if (hasProviderModules) imports.add("applyProviders");
|
|
196
258
|
if (needsLazyImport) imports.add("Lazy");
|
|
@@ -244,6 +306,18 @@ function createIdentifierExports(registrations) {
|
|
|
244
306
|
return `${registrations.map((meta) => `const ${meta.identifierConst} = registerServiceIdentifier(${meta.importName}, Symbol.for('${escapeSingleQuotes(meta.symbolDescription)}'));`).join("\n")}\n\nexport const serviceIdentifiers = {\n${registrations.map((meta) => ` '${meta.exportKey}': ${meta.identifierConst}`).join(",\n")}\n};\n`;
|
|
245
307
|
}
|
|
246
308
|
/**
|
|
309
|
+
* Builds the runtime scope-hierarchy registration statement. The generated
|
|
310
|
+
* container records the declared parent of each custom scope so child scopes
|
|
311
|
+
* constructed via `alloy-di/scopes` can be validated against the build-time
|
|
312
|
+
* hierarchy. Returns an empty string when no custom scopes are configured.
|
|
313
|
+
*/
|
|
314
|
+
function createScopeHierarchyBlock(scopes) {
|
|
315
|
+
if (!scopes) return "";
|
|
316
|
+
const names = Object.keys(scopes);
|
|
317
|
+
if (!names.length) return "";
|
|
318
|
+
return `\ncontainer._registerScopeHierarchy({\n${names.map((name) => ` ${JSON.stringify(name)}: ${JSON.stringify(scopes[name].parent ?? "singleton")}`).join(",\n")}\n});\n`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
247
321
|
* Generates the virtual container module code.
|
|
248
322
|
* This module:
|
|
249
323
|
* 1. Imports the runtime container and necessary helpers.
|
|
@@ -257,9 +331,11 @@ function createIdentifierExports(registrations) {
|
|
|
257
331
|
* @param lazyReferencedClassKeys - Set of service keys that are referenced ONLY lazily (and thus should not be imported/registered eagerly in this bundle).
|
|
258
332
|
* @param providerModulePaths - List of provider modules to import and apply.
|
|
259
333
|
*/
|
|
260
|
-
function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths) {
|
|
334
|
+
function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths, options) {
|
|
261
335
|
const hasProviderModules = providerModulePaths.length > 0;
|
|
262
|
-
const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths);
|
|
336
|
+
const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths, options);
|
|
337
|
+
const envOverridesBlock = options?.isDev === void 0 ? "" : `\nsetEnvDetectionOverrides({ isDev: ${options.isDev} });\n`;
|
|
338
|
+
const scopeHierarchyBlock = createScopeHierarchyBlock(options?.scopes);
|
|
263
339
|
let providerImportBlock = "";
|
|
264
340
|
let providerInvocationBlock = "";
|
|
265
341
|
if (hasProviderModules) {
|
|
@@ -268,12 +344,12 @@ function generateContainerModule(metas, lazyReferencedClassKeys, providerModuleP
|
|
|
268
344
|
providerInvocationBlock = `\nconst providerDefinitions = [${aliasNames.join(", ")}];\nfor (const definition of providerDefinitions) {\n applyProviders(container, definition);\n}\n`;
|
|
269
345
|
}
|
|
270
346
|
return `
|
|
271
|
-
${runtimeImportStatement}${stubsBlock}
|
|
347
|
+
${runtimeImportStatement}${envOverridesBlock}${stubsBlock}
|
|
272
348
|
${providerImportBlock}
|
|
273
349
|
${registrationsBlock}
|
|
274
350
|
|
|
275
351
|
const container = new Container();
|
|
276
|
-
|
|
352
|
+
${scopeHierarchyBlock}
|
|
277
353
|
for (const entry of registrations) {
|
|
278
354
|
dependenciesRegistry.set(entry.ctor, entry.meta);
|
|
279
355
|
}
|
|
@@ -281,6 +357,11 @@ ${providerInvocationBlock}${identifierExportBlock}
|
|
|
281
357
|
export default container;
|
|
282
358
|
`;
|
|
283
359
|
}
|
|
360
|
+
const GENERATED_FILE_NOTICE = "This file was auto-generated by Alloy. Manual changes will be overwritten.";
|
|
361
|
+
const GENERATED_FILE_HEADER = `/**
|
|
362
|
+
* ${GENERATED_FILE_NOTICE}
|
|
363
|
+
*/
|
|
364
|
+
`;
|
|
284
365
|
/**
|
|
285
366
|
* Generates the TypeScript declaration definition (`.d.ts`) for the virtual container module.
|
|
286
367
|
* It exports the `ServiceIdentifiers` interface matching the runtime exports.
|
|
@@ -305,7 +386,7 @@ function generateContainerTypeDefinition(metas, pathResolver) {
|
|
|
305
386
|
const exportKey = createIdentifierExportKey(meta, resolver);
|
|
306
387
|
interfaceMembers.push(`${exportKey}: ServiceIdentifier<${importName}>;`);
|
|
307
388
|
}
|
|
308
|
-
return
|
|
389
|
+
return `${GENERATED_FILE_HEADER}
|
|
309
390
|
declare module "virtual:alloy-container" {
|
|
310
391
|
import { Container, ServiceIdentifier } from "alloy-di/runtime";
|
|
311
392
|
${imports.length ? imports.join("\n") + "\n" : ""}
|
|
@@ -321,6 +402,29 @@ declare module "virtual:alloy-container" {
|
|
|
321
402
|
`;
|
|
322
403
|
}
|
|
323
404
|
/**
|
|
405
|
+
* Generates a standalone declaration file that augments `AlloyScopes` with the
|
|
406
|
+
* custom scope names, opening the `ServiceScope` union so `@Injectable('<scope>')`
|
|
407
|
+
* type-checks. Returns `undefined` when no custom scopes are configured.
|
|
408
|
+
*
|
|
409
|
+
* This MUST live in its own file: module augmentation requires the file to be a
|
|
410
|
+
* module (hence the trailing `export {}`), whereas the container declaration
|
|
411
|
+
* must stay a global script so `virtual:alloy-container` resolves everywhere.
|
|
412
|
+
* Combining the two in one file turns the container declaration into a module
|
|
413
|
+
* and breaks resolution of the virtual module.
|
|
414
|
+
*/
|
|
415
|
+
function generateScopeAugmentationDefinition(scopeNames) {
|
|
416
|
+
if (!scopeNames.length) return;
|
|
417
|
+
return `${GENERATED_FILE_HEADER}
|
|
418
|
+
declare module "alloy-di/runtime" {
|
|
419
|
+
interface AlloyScopes {
|
|
420
|
+
${scopeNames.map((name) => ` ${JSON.stringify(name)}: true;`).join("\n")}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export {};
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
324
428
|
* Generates ambient type declarations for external Alloy manifests consumed by the project.
|
|
325
429
|
* Creates:
|
|
326
430
|
* 1. `declare module "PKG/manifest"` typed as `LibraryManifest`.
|
|
@@ -329,7 +433,7 @@ declare module "virtual:alloy-container" {
|
|
|
329
433
|
* @param manifests - List of loaded manifest info (packageName and services).
|
|
330
434
|
*/
|
|
331
435
|
function generateManifestTypeDefinition(manifests) {
|
|
332
|
-
return manifests.map((m) => {
|
|
436
|
+
return GENERATED_FILE_HEADER + manifests.map((m) => {
|
|
333
437
|
const serviceIdentifiers = m.services.map((s) => ` export const ${s.exportName}Identifier: ServiceIdentifier;`).join("\n");
|
|
334
438
|
return `
|
|
335
439
|
declare module "${m.packageName}/manifest" {
|
|
@@ -420,4 +524,4 @@ ${serviceIdentifiers}
|
|
|
420
524
|
}).join("\n");
|
|
421
525
|
}
|
|
422
526
|
//#endregion
|
|
423
|
-
export { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition };
|
|
527
|
+
export { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition, generateScopeAugmentationDefinition };
|