@husky-di/module 1.0.0 → 1.0.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.
- package/dist/factories/exported-guard-middleware.factory.d.ts +28 -1
- package/dist/impls/module.d.ts +167 -1
- package/dist/index.cjs +166 -163
- package/dist/index.js +166 -163
- package/dist/interfaces/module.interface.d.ts +8 -8
- package/package.json +5 -2
- package/dist/utils/module.utils.d.ts +0 -126
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Factory for creating export guard middleware.
|
|
3
|
+
*
|
|
2
4
|
* @overview
|
|
5
|
+
* This middleware enforces export restrictions for module containers. It ensures
|
|
6
|
+
* that only explicitly exported service identifiers can be accessed from outside
|
|
7
|
+
* the container. Internal access (within the same container) is always allowed.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The middleware distinguishes between internal and external access by checking
|
|
11
|
+
* the resolution path. If the previous resolution step occurred in the same
|
|
12
|
+
* container, it's considered internal access. Otherwise, it's external access
|
|
13
|
+
* and must be in the exports list.
|
|
14
|
+
*
|
|
3
15
|
* @author AEPKILL
|
|
4
16
|
* @created 2025-08-18 22:01:34
|
|
5
17
|
*/
|
|
6
18
|
import { type ResolveMiddleware, type ServiceIdentifier } from "@husky-di/core";
|
|
7
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Creates a middleware factory that guards against accessing non-exported services.
|
|
21
|
+
*
|
|
22
|
+
* @param exports - Array of service identifiers that are allowed to be accessed from outside the container
|
|
23
|
+
* @returns A resolve middleware that enforces export restrictions
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* The middleware works by:
|
|
27
|
+
* 1. Checking if the current resolution is from within the same container (internal access)
|
|
28
|
+
* 2. If external access is detected, verifying that the service identifier is in the exports list
|
|
29
|
+
* 3. Throwing a ResolveException if a non-exported service is accessed externally
|
|
30
|
+
*
|
|
31
|
+
* Note: Services not registered in the container are not considered external access,
|
|
32
|
+
* as they will be resolved from parent containers or fail with a different error.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createExportedGuardMiddlewareFactory(exports: ReadonlyArray<ServiceIdentifier<unknown>>): ResolveMiddleware<any, any>;
|
package/dist/impls/module.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @author AEPKILL
|
|
4
4
|
* @created 2025-08-09 14:51:09
|
|
5
5
|
*/
|
|
6
|
-
import type
|
|
6
|
+
import { type IContainer, type IsRegisteredOptions, type ResolveInstance, type ResolveMiddleware, type ResolveOptions, type ServiceIdentifier } from "@husky-di/core";
|
|
7
7
|
import type { Alias, CreateModuleOptions, Declaration, IModule, ModuleWithAliases } from "../interfaces/module.interface";
|
|
8
8
|
export declare class Module implements IModule {
|
|
9
9
|
get id(): string;
|
|
@@ -18,6 +18,8 @@ export declare class Module implements IModule {
|
|
|
18
18
|
private _declarations?;
|
|
19
19
|
private _imports?;
|
|
20
20
|
private _exports?;
|
|
21
|
+
private readonly _visitedModules;
|
|
22
|
+
private readonly _visitStack;
|
|
21
23
|
constructor(options: CreateModuleOptions);
|
|
22
24
|
resolve<T, O extends ResolveOptions<T>>(serviceIdentifier: ServiceIdentifier<T>, options?: O): ResolveInstance<T, O>;
|
|
23
25
|
isRegistered<T>(serviceIdentifier: ServiceIdentifier<T>, options?: IsRegisteredOptions): boolean;
|
|
@@ -25,4 +27,168 @@ export declare class Module implements IModule {
|
|
|
25
27
|
use(middleware: ResolveMiddleware<any, any>): void;
|
|
26
28
|
unused(middleware: ResolveMiddleware<any, any>): void;
|
|
27
29
|
withAliases(aliases: Alias[]): ModuleWithAliases;
|
|
30
|
+
/**
|
|
31
|
+
* Builds the container for this module.
|
|
32
|
+
*
|
|
33
|
+
* @returns The built container
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* This method is called after all validations have been performed.
|
|
37
|
+
* It creates the container and registers all declarations and imports.
|
|
38
|
+
*/
|
|
39
|
+
private buildContainer;
|
|
40
|
+
/**
|
|
41
|
+
* Validates the module's imports.
|
|
42
|
+
*
|
|
43
|
+
* @throws Error if validation fails
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Validates the following rules:
|
|
47
|
+
* - Rule I1: Module uniqueness - no module imported multiple times
|
|
48
|
+
* - Rule I2: Circular dependencies - no circular import chains
|
|
49
|
+
* - Rule I3: Alias validation - aliases reference exported services and don't conflict
|
|
50
|
+
* - Rule I4: Import naming conflicts - no service name conflicts across imports
|
|
51
|
+
*/
|
|
52
|
+
private validateImports;
|
|
53
|
+
/**
|
|
54
|
+
* Validates that no module is imported multiple times (Rule I1).
|
|
55
|
+
*
|
|
56
|
+
* @param imports - Array of imports to validate
|
|
57
|
+
* @throws Error if a module is imported more than once
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* This prevents accidentally importing the same module multiple times,
|
|
61
|
+
* which would be redundant and potentially confusing.
|
|
62
|
+
*/
|
|
63
|
+
private validateImportUniqueness;
|
|
64
|
+
/**
|
|
65
|
+
* Detects circular dependencies in the module import graph.
|
|
66
|
+
*
|
|
67
|
+
* @throws Error if a circular dependency is detected
|
|
68
|
+
*/
|
|
69
|
+
private detectCircularDependencies;
|
|
70
|
+
/**
|
|
71
|
+
* Recursively visits a module to detect circular dependencies (Rule I2).
|
|
72
|
+
*
|
|
73
|
+
* @param module - The module to visit
|
|
74
|
+
* @throws Error if a circular dependency is detected
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Uses depth-first search with a visit stack to detect cycles.
|
|
78
|
+
* The visit stack tracks the current path being explored, while
|
|
79
|
+
* visitedModules tracks fully processed modules to avoid redundant work.
|
|
80
|
+
*/
|
|
81
|
+
private visitModule;
|
|
82
|
+
/**
|
|
83
|
+
* Registers the module's local declarations in the container.
|
|
84
|
+
*
|
|
85
|
+
* @param container - The container to register declarations in
|
|
86
|
+
*/
|
|
87
|
+
private registerDeclarations;
|
|
88
|
+
/**
|
|
89
|
+
* Registers the module's imports in the container.
|
|
90
|
+
*
|
|
91
|
+
* @param container - The container to register imports in
|
|
92
|
+
*/
|
|
93
|
+
private registerImports;
|
|
94
|
+
/**
|
|
95
|
+
* Normalizes imports into a flat list of service identifiers with their aliases.
|
|
96
|
+
*
|
|
97
|
+
* @param imports - Array of imports to normalize
|
|
98
|
+
* @returns Normalized array of imports with alias mappings
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* This method transforms the import declarations into a format suitable
|
|
102
|
+
* for container registration, applying any alias mappings in the process.
|
|
103
|
+
*/
|
|
104
|
+
private normalizeImports;
|
|
105
|
+
/**
|
|
106
|
+
* Builds a map of service identifier aliases from an alias array.
|
|
107
|
+
*
|
|
108
|
+
* @param aliases - Array of alias mappings
|
|
109
|
+
* @returns Map from source service identifier to target service identifier
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* This method is used throughout the validation and registration process
|
|
113
|
+
* to consistently apply alias transformations.
|
|
114
|
+
*/
|
|
115
|
+
private buildAliasMap;
|
|
116
|
+
/**
|
|
117
|
+
* Validates declarations for this module.
|
|
118
|
+
*
|
|
119
|
+
* @throws Error if validation fails
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* Validates that:
|
|
123
|
+
* 1. No serviceIdentifier is declared multiple times (Rule D1)
|
|
124
|
+
* 2. Each declaration has valid registration options (Rule D2)
|
|
125
|
+
*/
|
|
126
|
+
private validateDeclarations;
|
|
127
|
+
/**
|
|
128
|
+
* Validates exports for this module.
|
|
129
|
+
*
|
|
130
|
+
* @throws Error if validation fails
|
|
131
|
+
*
|
|
132
|
+
* @remarks
|
|
133
|
+
* Validates that:
|
|
134
|
+
* 1. No serviceIdentifier is exported multiple times
|
|
135
|
+
* 2. Each exported serviceIdentifier is either declared locally or imported
|
|
136
|
+
* 3. Aliased imports must be exported using their alias name, not original name
|
|
137
|
+
*/
|
|
138
|
+
private validateExports;
|
|
139
|
+
/**
|
|
140
|
+
* Collects all service identifiers that are available in this module.
|
|
141
|
+
*
|
|
142
|
+
* @returns Set of available service identifiers
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* Available services include:
|
|
146
|
+
* 1. Locally declared services
|
|
147
|
+
* 2. Imported services (considering aliases)
|
|
148
|
+
*
|
|
149
|
+
* This is used by validateExports to ensure only available services are exported.
|
|
150
|
+
*/
|
|
151
|
+
private collectAvailableServices;
|
|
152
|
+
/**
|
|
153
|
+
* Validates aliases for this module (called from withAliases).
|
|
154
|
+
*
|
|
155
|
+
* @param aliases - Array of alias mappings to validate
|
|
156
|
+
* @throws Error if validation fails
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Validates that:
|
|
160
|
+
* 1. Each aliased serviceIdentifier is exported by this module (Rule I3.1)
|
|
161
|
+
* 2. No serviceIdentifier is mapped multiple times (Rule I3.2)
|
|
162
|
+
*
|
|
163
|
+
* Note: Rule I3.3 (alias conflicts with declarations) is validated during
|
|
164
|
+
* module construction in validateAliasConflictsWithDeclarations()
|
|
165
|
+
*/
|
|
166
|
+
private validateAliases;
|
|
167
|
+
/**
|
|
168
|
+
* Validates that aliases don't conflict with local declarations (Rule I3.3).
|
|
169
|
+
*
|
|
170
|
+
* @throws Error if an alias name conflicts with a local declaration
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* This ensures that aliased imports don't shadow or conflict with
|
|
174
|
+
* services declared in the current module.
|
|
175
|
+
*/
|
|
176
|
+
private validateAliasConflictsWithDeclarations;
|
|
177
|
+
/**
|
|
178
|
+
* Validates that imported services don't have naming conflicts (Rule I4).
|
|
179
|
+
*
|
|
180
|
+
* @throws Error if the same service identifier is exported by multiple modules
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* This prevents ambiguity when multiple imported modules export services
|
|
184
|
+
* with the same name. Users should use aliases to resolve such conflicts.
|
|
185
|
+
*/
|
|
186
|
+
private validateImportNamingConflicts;
|
|
187
|
+
/**
|
|
188
|
+
* Helper method to check if an import item is a ModuleWithAliases.
|
|
189
|
+
*
|
|
190
|
+
* @param item - The import item to check
|
|
191
|
+
* @returns True if the item is a ModuleWithAliases, false otherwise
|
|
192
|
+
*/
|
|
193
|
+
private isModuleWithAliases;
|
|
28
194
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -51,168 +51,6 @@ function findPreviousContainer(paths) {
|
|
|
51
51
|
lastContainer = path.value.container;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
function isModuleWithAliases(moduleImport) {
|
|
55
|
-
return moduleImport.module instanceof Module;
|
|
56
|
-
}
|
|
57
|
-
function getModuleByImport(moduleImport) {
|
|
58
|
-
return isModuleWithAliases(moduleImport) ? moduleImport.module : moduleImport;
|
|
59
|
-
}
|
|
60
|
-
function build(module) {
|
|
61
|
-
const builder = new ModuleBuilder(module);
|
|
62
|
-
return builder.build();
|
|
63
|
-
}
|
|
64
|
-
class ModuleBuilder {
|
|
65
|
-
module;
|
|
66
|
-
serviceIdentifierMap = new Map();
|
|
67
|
-
availableServiceIdentifiers = new Set();
|
|
68
|
-
importAliasesCache = new Map();
|
|
69
|
-
constructor(module){
|
|
70
|
-
this.module = module;
|
|
71
|
-
}
|
|
72
|
-
build() {
|
|
73
|
-
this.validateAndCollectInfo();
|
|
74
|
-
if (this.module.container) return this.module.container;
|
|
75
|
-
const container = (0, core_namespaceObject.createContainer)(this.module.name);
|
|
76
|
-
if (this.module.exports?.length) container.use(createExportedGuardMiddlewareFactory(this.module.exports));
|
|
77
|
-
this.registerDeclarations(container);
|
|
78
|
-
this.registerImports(container);
|
|
79
|
-
return container;
|
|
80
|
-
}
|
|
81
|
-
validateAndCollectInfo() {
|
|
82
|
-
this.validateImportUniqueness();
|
|
83
|
-
this.validateExportUniqueness();
|
|
84
|
-
this.validateCircularDependencies();
|
|
85
|
-
this.collectServiceInfoAndValidateConflicts();
|
|
86
|
-
this.validateExportValidity();
|
|
87
|
-
}
|
|
88
|
-
validateImportUniqueness() {
|
|
89
|
-
const { imports } = this.module;
|
|
90
|
-
if (!imports?.length) return;
|
|
91
|
-
const importModules = new Set();
|
|
92
|
-
for (const importModule of imports)try {
|
|
93
|
-
const importedModule = getModuleByImport(importModule);
|
|
94
|
-
if (importModules.has(importedModule)) throw new Error(`Duplicate import module: "${importedModule.displayName}" in "${this.module.displayName}".`);
|
|
95
|
-
importModules.add(importedModule);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
throw new Error(`Invalid module import in "${this.module.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
validateExportUniqueness() {
|
|
101
|
-
const { exports: exports1 } = this.module;
|
|
102
|
-
if (!exports1?.length) return;
|
|
103
|
-
const existingExportServiceIdentifiers = new Set();
|
|
104
|
-
for (const exported of exports1){
|
|
105
|
-
if (existingExportServiceIdentifiers.has(exported)) throw new Error(`Duplicate export service identifier: "${(0, core_namespaceObject.getServiceIdentifierName)(exported)}" in "${this.module.displayName}".`);
|
|
106
|
-
existingExportServiceIdentifiers.add(exported);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
validateCircularDependencies() {
|
|
110
|
-
const visited = new Set();
|
|
111
|
-
const visiting = new Set();
|
|
112
|
-
const dependencyPath = [];
|
|
113
|
-
this.detectCircularDependency(this.module, visited, visiting, dependencyPath);
|
|
114
|
-
}
|
|
115
|
-
detectCircularDependency(currentModule, visited, visiting, dependencyPath) {
|
|
116
|
-
if (visited.has(currentModule)) return;
|
|
117
|
-
if (visiting.has(currentModule)) {
|
|
118
|
-
const cycleStartIndex = dependencyPath.findIndex((module)=>module === currentModule);
|
|
119
|
-
const cyclePath = dependencyPath.slice(cycleStartIndex).concat(currentModule).map((module)=>module.displayName).join(" -> ");
|
|
120
|
-
throw new Error(`Circular dependency detected: ${cyclePath}. Modules cannot have circular import relationships.`);
|
|
121
|
-
}
|
|
122
|
-
visiting.add(currentModule);
|
|
123
|
-
dependencyPath.push(currentModule);
|
|
124
|
-
const imports = currentModule.imports ?? [];
|
|
125
|
-
for (const importModule of imports)try {
|
|
126
|
-
const importedModule = getModuleByImport(importModule);
|
|
127
|
-
this.detectCircularDependency(importedModule, visited, visiting, dependencyPath);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
if (error instanceof Error && error.message.includes("Circular dependency detected")) throw error;
|
|
130
|
-
throw new Error(`Failed to validate circular dependencies for "${currentModule.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
131
|
-
}
|
|
132
|
-
visiting.delete(currentModule);
|
|
133
|
-
dependencyPath.pop();
|
|
134
|
-
visited.add(currentModule);
|
|
135
|
-
}
|
|
136
|
-
collectServiceInfoAndValidateConflicts() {
|
|
137
|
-
const { imports, declarations } = this.module;
|
|
138
|
-
if (declarations?.length) for (const declaration of declarations){
|
|
139
|
-
this.serviceIdentifierMap.set(declaration.serviceIdentifier, {
|
|
140
|
-
type: "declaration",
|
|
141
|
-
source: "declarations"
|
|
142
|
-
});
|
|
143
|
-
this.availableServiceIdentifiers.add(declaration.serviceIdentifier);
|
|
144
|
-
}
|
|
145
|
-
if (imports?.length) for (const importModule of imports)try {
|
|
146
|
-
const importedModule = getModuleByImport(importModule);
|
|
147
|
-
const exportedServices = importedModule.exports ?? [];
|
|
148
|
-
const aliasesMap = this.buildAndCacheAliasesMap(importModule, importedModule);
|
|
149
|
-
for (const exported of exportedServices){
|
|
150
|
-
const existing = this.serviceIdentifierMap.get(exported);
|
|
151
|
-
if (existing) {
|
|
152
|
-
const conflictInfo = {
|
|
153
|
-
serviceName: (0, core_namespaceObject.getServiceIdentifierName)(exported),
|
|
154
|
-
currentModule: importedModule.displayName,
|
|
155
|
-
existing,
|
|
156
|
-
targetModule: this.module.displayName
|
|
157
|
-
};
|
|
158
|
-
throw new Error(this.buildConflictMessage(conflictInfo));
|
|
159
|
-
}
|
|
160
|
-
this.serviceIdentifierMap.set(exported, {
|
|
161
|
-
type: "import",
|
|
162
|
-
source: importedModule.displayName
|
|
163
|
-
});
|
|
164
|
-
this.availableServiceIdentifiers.add(exported);
|
|
165
|
-
const alias = aliasesMap.get(exported);
|
|
166
|
-
if (alias) this.availableServiceIdentifiers.add(alias);
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
throw new Error(`Failed to validate imports in "${this.module.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
validateExportValidity() {
|
|
173
|
-
const { exports: exports1 } = this.module;
|
|
174
|
-
if (!exports1?.length) return;
|
|
175
|
-
for (const exported of exports1)if (!this.availableServiceIdentifiers.has(exported)) throw new Error(`Cannot export service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(exported)}" from "${this.module.displayName}": it is not declared in this module or imported from any imported module.`);
|
|
176
|
-
}
|
|
177
|
-
buildAndCacheAliasesMap(importModule, importedModule) {
|
|
178
|
-
const cached = this.importAliasesCache.get(importedModule);
|
|
179
|
-
if (cached) return cached;
|
|
180
|
-
const aliasesMap = new Map();
|
|
181
|
-
const moduleWithAliases = importModule;
|
|
182
|
-
const aliases = moduleWithAliases.aliases ?? [];
|
|
183
|
-
for (const alias of aliases)aliasesMap.set(alias.serviceIdentifier, alias.as);
|
|
184
|
-
this.importAliasesCache.set(importedModule, aliasesMap);
|
|
185
|
-
return aliasesMap;
|
|
186
|
-
}
|
|
187
|
-
registerDeclarations(container) {
|
|
188
|
-
const { declarations } = this.module;
|
|
189
|
-
if (!declarations?.length) return;
|
|
190
|
-
for (const declaration of declarations){
|
|
191
|
-
const { serviceIdentifier, ...rest } = declaration;
|
|
192
|
-
container.register(serviceIdentifier, rest);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
registerImports(container) {
|
|
196
|
-
const { imports } = this.module;
|
|
197
|
-
if (!imports?.length) return;
|
|
198
|
-
for (const importModule of imports){
|
|
199
|
-
const importedModule = getModuleByImport(importModule);
|
|
200
|
-
const aliasesMap = this.importAliasesCache.get(importedModule) ?? new Map();
|
|
201
|
-
for (const exported of importedModule.exports ?? [])container.register(aliasesMap.get(exported) ?? exported, {
|
|
202
|
-
useAlias: exported,
|
|
203
|
-
getContainer () {
|
|
204
|
-
return importedModule.container;
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
buildConflictMessage(conflictInfo) {
|
|
210
|
-
const { serviceName, currentModule, existing, targetModule } = conflictInfo;
|
|
211
|
-
const conflictType = "declaration" === existing.type ? "declared in" : "exported by";
|
|
212
|
-
const conflictSource = existing.source;
|
|
213
|
-
return `Service identifier conflict: "${serviceName}" is exported by "${currentModule}" and ${conflictType} "${conflictSource}" in "${targetModule}".`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
54
|
const createModuleId = (0, core_namespaceObject.incrementalIdFactory)("MODULE");
|
|
217
55
|
class Module {
|
|
218
56
|
get id() {
|
|
@@ -239,13 +77,19 @@ class Module {
|
|
|
239
77
|
_declarations;
|
|
240
78
|
_imports;
|
|
241
79
|
_exports;
|
|
80
|
+
_visitedModules = new Set();
|
|
81
|
+
_visitStack = [];
|
|
242
82
|
constructor(options){
|
|
243
83
|
this._id = createModuleId();
|
|
244
84
|
this._name = options.name;
|
|
245
85
|
this._declarations = options.declarations;
|
|
246
86
|
this._imports = options.imports;
|
|
247
87
|
this._exports = options.exports;
|
|
248
|
-
this.
|
|
88
|
+
this.validateDeclarations();
|
|
89
|
+
this.validateImports();
|
|
90
|
+
this.validateExports();
|
|
91
|
+
this.container = this.buildContainer();
|
|
92
|
+
this.container.use(createExportedGuardMiddlewareFactory(this.exports ?? []));
|
|
249
93
|
}
|
|
250
94
|
resolve(serviceIdentifier, options) {
|
|
251
95
|
return this.container.resolve(serviceIdentifier, options);
|
|
@@ -263,11 +107,170 @@ class Module {
|
|
|
263
107
|
this.container.unused(middleware);
|
|
264
108
|
}
|
|
265
109
|
withAliases(aliases) {
|
|
110
|
+
this.validateAliases(aliases);
|
|
266
111
|
return {
|
|
267
112
|
module: this,
|
|
268
113
|
aliases
|
|
269
114
|
};
|
|
270
115
|
}
|
|
116
|
+
buildContainer() {
|
|
117
|
+
const container = (0, core_namespaceObject.createContainer)(this._name);
|
|
118
|
+
this.registerDeclarations(container);
|
|
119
|
+
this.registerImports(container);
|
|
120
|
+
return container;
|
|
121
|
+
}
|
|
122
|
+
validateImports() {
|
|
123
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
124
|
+
this.validateImportUniqueness(this._imports);
|
|
125
|
+
this.detectCircularDependencies();
|
|
126
|
+
this.validateAliasConflictsWithDeclarations();
|
|
127
|
+
this.validateImportNamingConflicts();
|
|
128
|
+
}
|
|
129
|
+
validateImportUniqueness(imports) {
|
|
130
|
+
const seenModules = new Set();
|
|
131
|
+
for (const item of imports){
|
|
132
|
+
const importedModule = this.isModuleWithAliases(item) ? item.module : item;
|
|
133
|
+
const moduleId = importedModule.id;
|
|
134
|
+
if (seenModules.has(moduleId)) throw new Error(`Duplicate import module: "${importedModule.displayName}" in "${this.displayName}".`);
|
|
135
|
+
seenModules.add(moduleId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
detectCircularDependencies() {
|
|
139
|
+
this._visitedModules.clear();
|
|
140
|
+
this._visitStack.length = 0;
|
|
141
|
+
this.visitModule(this);
|
|
142
|
+
}
|
|
143
|
+
visitModule(module) {
|
|
144
|
+
if (this._visitStack.includes(module)) {
|
|
145
|
+
const cycle = [
|
|
146
|
+
...this._visitStack.slice(this._visitStack.indexOf(module)),
|
|
147
|
+
module
|
|
148
|
+
];
|
|
149
|
+
const cyclePath = cycle.map((m)=>m.displayName).join(" \u2192 ");
|
|
150
|
+
throw new Error(`Circular dependency detected: ${cyclePath}`);
|
|
151
|
+
}
|
|
152
|
+
if (this._visitedModules.has(module.id)) return;
|
|
153
|
+
this._visitStack.push(module);
|
|
154
|
+
const imports = module.imports ?? [];
|
|
155
|
+
for (const item of imports){
|
|
156
|
+
const importedModule = this.isModuleWithAliases(item) ? item.module : item;
|
|
157
|
+
this.visitModule(importedModule);
|
|
158
|
+
}
|
|
159
|
+
this._visitedModules.add(module.id);
|
|
160
|
+
this._visitStack.pop();
|
|
161
|
+
}
|
|
162
|
+
registerDeclarations(container) {
|
|
163
|
+
if (!this._declarations || 0 === this._declarations.length) return;
|
|
164
|
+
for (const decl of this._declarations){
|
|
165
|
+
const { serviceIdentifier, ...options } = decl;
|
|
166
|
+
container.register(serviceIdentifier, options);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
registerImports(container) {
|
|
170
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
171
|
+
const normalizedImports = this.normalizeImports(this._imports);
|
|
172
|
+
for (const { module: sourceModule, serviceIdentifier, as } of normalizedImports)if (serviceIdentifier !== as) container.register(as, {
|
|
173
|
+
useAlias: serviceIdentifier,
|
|
174
|
+
getContainer: ()=>sourceModule.container
|
|
175
|
+
});
|
|
176
|
+
else container.register(serviceIdentifier, {
|
|
177
|
+
useAlias: serviceIdentifier,
|
|
178
|
+
getContainer: ()=>sourceModule.container
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
normalizeImports(imports) {
|
|
182
|
+
return imports.flatMap((item)=>{
|
|
183
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
184
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
185
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
186
|
+
return (module.exports ?? []).map((serviceIdentifier)=>({
|
|
187
|
+
module,
|
|
188
|
+
serviceIdentifier,
|
|
189
|
+
as: aliasMap.get(serviceIdentifier) ?? serviceIdentifier
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
buildAliasMap(aliases) {
|
|
194
|
+
return new Map((aliases ?? []).map((alias)=>[
|
|
195
|
+
alias.serviceIdentifier,
|
|
196
|
+
alias.as
|
|
197
|
+
]));
|
|
198
|
+
}
|
|
199
|
+
validateDeclarations() {
|
|
200
|
+
if (!this._declarations || 0 === this._declarations.length) return;
|
|
201
|
+
const seen = new Set();
|
|
202
|
+
for (const decl of this._declarations){
|
|
203
|
+
const { serviceIdentifier } = decl;
|
|
204
|
+
if (seen.has(serviceIdentifier)) throw new Error(`Duplicate declaration of service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${this.displayName}".`);
|
|
205
|
+
seen.add(serviceIdentifier);
|
|
206
|
+
const hasValidOption = "useClass" in decl || "useFactory" in decl || "useValue" in decl || "useAlias" in decl;
|
|
207
|
+
if (!hasValidOption) throw new Error(`Invalid registration options for service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${this.displayName}": must specify useClass, useFactory, useValue, or useAlias.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
validateExports() {
|
|
211
|
+
if (!this._exports || 0 === this._exports.length) return;
|
|
212
|
+
const seen = new Set();
|
|
213
|
+
for (const exportId of this._exports){
|
|
214
|
+
if (seen.has(exportId)) throw new Error(`Duplicate export of service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(exportId)}" in module "${this.displayName}".`);
|
|
215
|
+
seen.add(exportId);
|
|
216
|
+
}
|
|
217
|
+
const availableServices = this.collectAvailableServices();
|
|
218
|
+
for (const exportId of this._exports)if (!availableServices.has(exportId)) throw new Error(`Cannot export service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(exportId)}" from "${this.displayName}": it is not declared in this module or imported from any imported module.`);
|
|
219
|
+
}
|
|
220
|
+
collectAvailableServices() {
|
|
221
|
+
const localServices = (this._declarations ?? []).map((decl)=>decl.serviceIdentifier);
|
|
222
|
+
const importedServices = (this._imports ?? []).flatMap((item)=>{
|
|
223
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
224
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
225
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
226
|
+
return (module.exports ?? []).map((serviceId)=>aliasMap.get(serviceId) ?? serviceId);
|
|
227
|
+
});
|
|
228
|
+
return new Set([
|
|
229
|
+
...localServices,
|
|
230
|
+
...importedServices
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
validateAliases(aliases) {
|
|
234
|
+
if (!aliases || 0 === aliases.length) return;
|
|
235
|
+
const exportedSet = new Set(this._exports ?? []);
|
|
236
|
+
const mappedServices = new Set();
|
|
237
|
+
for (const alias of aliases){
|
|
238
|
+
const { serviceIdentifier } = alias;
|
|
239
|
+
if (!exportedSet.has(serviceIdentifier)) throw new Error(`Cannot alias service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" from module "${this.displayName}": it is not exported from that module.`);
|
|
240
|
+
if (mappedServices.has(serviceIdentifier)) throw new Error(`Duplicate alias mapping for service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${this.displayName}".`);
|
|
241
|
+
mappedServices.add(serviceIdentifier);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
validateAliasConflictsWithDeclarations() {
|
|
245
|
+
if (!this._imports || !this._declarations) return;
|
|
246
|
+
const localDeclarations = new Set((this._declarations ?? []).map((decl)=>decl.serviceIdentifier));
|
|
247
|
+
const conflictingAlias = (this._imports ?? []).filter((item)=>this.isModuleWithAliases(item)).flatMap((item)=>item.aliases ?? []).find((alias)=>localDeclarations.has(alias.as));
|
|
248
|
+
if (conflictingAlias) throw new Error(`Alias "${(0, core_namespaceObject.getServiceIdentifierName)(conflictingAlias.as)}" conflicts with local declaration in module "${this.displayName}".`);
|
|
249
|
+
}
|
|
250
|
+
validateImportNamingConflicts() {
|
|
251
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
252
|
+
const serviceToModules = (this._imports ?? []).reduce((acc, item)=>{
|
|
253
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
254
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
255
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
256
|
+
for (const serviceId of module.exports ?? []){
|
|
257
|
+
const effectiveName = aliasMap.get(serviceId) ?? serviceId;
|
|
258
|
+
const modules = acc.get(effectiveName) ?? [];
|
|
259
|
+
modules.push(module);
|
|
260
|
+
acc.set(effectiveName, modules);
|
|
261
|
+
}
|
|
262
|
+
return acc;
|
|
263
|
+
}, new Map());
|
|
264
|
+
const conflicts = Array.from(serviceToModules.entries()).filter(([, modules])=>modules.length > 1);
|
|
265
|
+
if (conflicts.length > 0) {
|
|
266
|
+
const [serviceId, modules] = conflicts[0];
|
|
267
|
+
const moduleNames = modules.map((m)=>`"${m.displayName}"`).join(", ");
|
|
268
|
+
throw new Error(`Service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceId)}" is exported by multiple imported modules: ${moduleNames}. Consider using aliases to resolve the conflict.`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
isModuleWithAliases(item) {
|
|
272
|
+
return "module" in item;
|
|
273
|
+
}
|
|
271
274
|
}
|
|
272
275
|
function createModule(options) {
|
|
273
276
|
return new Module(options);
|
package/dist/index.js
CHANGED
|
@@ -23,168 +23,6 @@ function findPreviousContainer(paths) {
|
|
|
23
23
|
lastContainer = path.value.container;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
function isModuleWithAliases(moduleImport) {
|
|
27
|
-
return moduleImport.module instanceof Module;
|
|
28
|
-
}
|
|
29
|
-
function getModuleByImport(moduleImport) {
|
|
30
|
-
return isModuleWithAliases(moduleImport) ? moduleImport.module : moduleImport;
|
|
31
|
-
}
|
|
32
|
-
function build(module) {
|
|
33
|
-
const builder = new ModuleBuilder(module);
|
|
34
|
-
return builder.build();
|
|
35
|
-
}
|
|
36
|
-
class ModuleBuilder {
|
|
37
|
-
module;
|
|
38
|
-
serviceIdentifierMap = new Map();
|
|
39
|
-
availableServiceIdentifiers = new Set();
|
|
40
|
-
importAliasesCache = new Map();
|
|
41
|
-
constructor(module){
|
|
42
|
-
this.module = module;
|
|
43
|
-
}
|
|
44
|
-
build() {
|
|
45
|
-
this.validateAndCollectInfo();
|
|
46
|
-
if (this.module.container) return this.module.container;
|
|
47
|
-
const container = createContainer(this.module.name);
|
|
48
|
-
if (this.module.exports?.length) container.use(createExportedGuardMiddlewareFactory(this.module.exports));
|
|
49
|
-
this.registerDeclarations(container);
|
|
50
|
-
this.registerImports(container);
|
|
51
|
-
return container;
|
|
52
|
-
}
|
|
53
|
-
validateAndCollectInfo() {
|
|
54
|
-
this.validateImportUniqueness();
|
|
55
|
-
this.validateExportUniqueness();
|
|
56
|
-
this.validateCircularDependencies();
|
|
57
|
-
this.collectServiceInfoAndValidateConflicts();
|
|
58
|
-
this.validateExportValidity();
|
|
59
|
-
}
|
|
60
|
-
validateImportUniqueness() {
|
|
61
|
-
const { imports } = this.module;
|
|
62
|
-
if (!imports?.length) return;
|
|
63
|
-
const importModules = new Set();
|
|
64
|
-
for (const importModule of imports)try {
|
|
65
|
-
const importedModule = getModuleByImport(importModule);
|
|
66
|
-
if (importModules.has(importedModule)) throw new Error(`Duplicate import module: "${importedModule.displayName}" in "${this.module.displayName}".`);
|
|
67
|
-
importModules.add(importedModule);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
throw new Error(`Invalid module import in "${this.module.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
validateExportUniqueness() {
|
|
73
|
-
const { exports } = this.module;
|
|
74
|
-
if (!exports?.length) return;
|
|
75
|
-
const existingExportServiceIdentifiers = new Set();
|
|
76
|
-
for (const exported of exports){
|
|
77
|
-
if (existingExportServiceIdentifiers.has(exported)) throw new Error(`Duplicate export service identifier: "${getServiceIdentifierName(exported)}" in "${this.module.displayName}".`);
|
|
78
|
-
existingExportServiceIdentifiers.add(exported);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
validateCircularDependencies() {
|
|
82
|
-
const visited = new Set();
|
|
83
|
-
const visiting = new Set();
|
|
84
|
-
const dependencyPath = [];
|
|
85
|
-
this.detectCircularDependency(this.module, visited, visiting, dependencyPath);
|
|
86
|
-
}
|
|
87
|
-
detectCircularDependency(currentModule, visited, visiting, dependencyPath) {
|
|
88
|
-
if (visited.has(currentModule)) return;
|
|
89
|
-
if (visiting.has(currentModule)) {
|
|
90
|
-
const cycleStartIndex = dependencyPath.findIndex((module)=>module === currentModule);
|
|
91
|
-
const cyclePath = dependencyPath.slice(cycleStartIndex).concat(currentModule).map((module)=>module.displayName).join(" -> ");
|
|
92
|
-
throw new Error(`Circular dependency detected: ${cyclePath}. Modules cannot have circular import relationships.`);
|
|
93
|
-
}
|
|
94
|
-
visiting.add(currentModule);
|
|
95
|
-
dependencyPath.push(currentModule);
|
|
96
|
-
const imports = currentModule.imports ?? [];
|
|
97
|
-
for (const importModule of imports)try {
|
|
98
|
-
const importedModule = getModuleByImport(importModule);
|
|
99
|
-
this.detectCircularDependency(importedModule, visited, visiting, dependencyPath);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error instanceof Error && error.message.includes("Circular dependency detected")) throw error;
|
|
102
|
-
throw new Error(`Failed to validate circular dependencies for "${currentModule.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
103
|
-
}
|
|
104
|
-
visiting.delete(currentModule);
|
|
105
|
-
dependencyPath.pop();
|
|
106
|
-
visited.add(currentModule);
|
|
107
|
-
}
|
|
108
|
-
collectServiceInfoAndValidateConflicts() {
|
|
109
|
-
const { imports, declarations } = this.module;
|
|
110
|
-
if (declarations?.length) for (const declaration of declarations){
|
|
111
|
-
this.serviceIdentifierMap.set(declaration.serviceIdentifier, {
|
|
112
|
-
type: "declaration",
|
|
113
|
-
source: "declarations"
|
|
114
|
-
});
|
|
115
|
-
this.availableServiceIdentifiers.add(declaration.serviceIdentifier);
|
|
116
|
-
}
|
|
117
|
-
if (imports?.length) for (const importModule of imports)try {
|
|
118
|
-
const importedModule = getModuleByImport(importModule);
|
|
119
|
-
const exportedServices = importedModule.exports ?? [];
|
|
120
|
-
const aliasesMap = this.buildAndCacheAliasesMap(importModule, importedModule);
|
|
121
|
-
for (const exported of exportedServices){
|
|
122
|
-
const existing = this.serviceIdentifierMap.get(exported);
|
|
123
|
-
if (existing) {
|
|
124
|
-
const conflictInfo = {
|
|
125
|
-
serviceName: getServiceIdentifierName(exported),
|
|
126
|
-
currentModule: importedModule.displayName,
|
|
127
|
-
existing,
|
|
128
|
-
targetModule: this.module.displayName
|
|
129
|
-
};
|
|
130
|
-
throw new Error(this.buildConflictMessage(conflictInfo));
|
|
131
|
-
}
|
|
132
|
-
this.serviceIdentifierMap.set(exported, {
|
|
133
|
-
type: "import",
|
|
134
|
-
source: importedModule.displayName
|
|
135
|
-
});
|
|
136
|
-
this.availableServiceIdentifiers.add(exported);
|
|
137
|
-
const alias = aliasesMap.get(exported);
|
|
138
|
-
if (alias) this.availableServiceIdentifiers.add(alias);
|
|
139
|
-
}
|
|
140
|
-
} catch (error) {
|
|
141
|
-
throw new Error(`Failed to validate imports in "${this.module.displayName}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
validateExportValidity() {
|
|
145
|
-
const { exports } = this.module;
|
|
146
|
-
if (!exports?.length) return;
|
|
147
|
-
for (const exported of exports)if (!this.availableServiceIdentifiers.has(exported)) throw new Error(`Cannot export service identifier "${getServiceIdentifierName(exported)}" from "${this.module.displayName}": it is not declared in this module or imported from any imported module.`);
|
|
148
|
-
}
|
|
149
|
-
buildAndCacheAliasesMap(importModule, importedModule) {
|
|
150
|
-
const cached = this.importAliasesCache.get(importedModule);
|
|
151
|
-
if (cached) return cached;
|
|
152
|
-
const aliasesMap = new Map();
|
|
153
|
-
const moduleWithAliases = importModule;
|
|
154
|
-
const aliases = moduleWithAliases.aliases ?? [];
|
|
155
|
-
for (const alias of aliases)aliasesMap.set(alias.serviceIdentifier, alias.as);
|
|
156
|
-
this.importAliasesCache.set(importedModule, aliasesMap);
|
|
157
|
-
return aliasesMap;
|
|
158
|
-
}
|
|
159
|
-
registerDeclarations(container) {
|
|
160
|
-
const { declarations } = this.module;
|
|
161
|
-
if (!declarations?.length) return;
|
|
162
|
-
for (const declaration of declarations){
|
|
163
|
-
const { serviceIdentifier, ...rest } = declaration;
|
|
164
|
-
container.register(serviceIdentifier, rest);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
registerImports(container) {
|
|
168
|
-
const { imports } = this.module;
|
|
169
|
-
if (!imports?.length) return;
|
|
170
|
-
for (const importModule of imports){
|
|
171
|
-
const importedModule = getModuleByImport(importModule);
|
|
172
|
-
const aliasesMap = this.importAliasesCache.get(importedModule) ?? new Map();
|
|
173
|
-
for (const exported of importedModule.exports ?? [])container.register(aliasesMap.get(exported) ?? exported, {
|
|
174
|
-
useAlias: exported,
|
|
175
|
-
getContainer () {
|
|
176
|
-
return importedModule.container;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
buildConflictMessage(conflictInfo) {
|
|
182
|
-
const { serviceName, currentModule, existing, targetModule } = conflictInfo;
|
|
183
|
-
const conflictType = "declaration" === existing.type ? "declared in" : "exported by";
|
|
184
|
-
const conflictSource = existing.source;
|
|
185
|
-
return `Service identifier conflict: "${serviceName}" is exported by "${currentModule}" and ${conflictType} "${conflictSource}" in "${targetModule}".`;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
26
|
const createModuleId = incrementalIdFactory("MODULE");
|
|
189
27
|
class Module {
|
|
190
28
|
get id() {
|
|
@@ -211,13 +49,19 @@ class Module {
|
|
|
211
49
|
_declarations;
|
|
212
50
|
_imports;
|
|
213
51
|
_exports;
|
|
52
|
+
_visitedModules = new Set();
|
|
53
|
+
_visitStack = [];
|
|
214
54
|
constructor(options){
|
|
215
55
|
this._id = createModuleId();
|
|
216
56
|
this._name = options.name;
|
|
217
57
|
this._declarations = options.declarations;
|
|
218
58
|
this._imports = options.imports;
|
|
219
59
|
this._exports = options.exports;
|
|
220
|
-
this.
|
|
60
|
+
this.validateDeclarations();
|
|
61
|
+
this.validateImports();
|
|
62
|
+
this.validateExports();
|
|
63
|
+
this.container = this.buildContainer();
|
|
64
|
+
this.container.use(createExportedGuardMiddlewareFactory(this.exports ?? []));
|
|
221
65
|
}
|
|
222
66
|
resolve(serviceIdentifier, options) {
|
|
223
67
|
return this.container.resolve(serviceIdentifier, options);
|
|
@@ -235,11 +79,170 @@ class Module {
|
|
|
235
79
|
this.container.unused(middleware);
|
|
236
80
|
}
|
|
237
81
|
withAliases(aliases) {
|
|
82
|
+
this.validateAliases(aliases);
|
|
238
83
|
return {
|
|
239
84
|
module: this,
|
|
240
85
|
aliases
|
|
241
86
|
};
|
|
242
87
|
}
|
|
88
|
+
buildContainer() {
|
|
89
|
+
const container = createContainer(this._name);
|
|
90
|
+
this.registerDeclarations(container);
|
|
91
|
+
this.registerImports(container);
|
|
92
|
+
return container;
|
|
93
|
+
}
|
|
94
|
+
validateImports() {
|
|
95
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
96
|
+
this.validateImportUniqueness(this._imports);
|
|
97
|
+
this.detectCircularDependencies();
|
|
98
|
+
this.validateAliasConflictsWithDeclarations();
|
|
99
|
+
this.validateImportNamingConflicts();
|
|
100
|
+
}
|
|
101
|
+
validateImportUniqueness(imports) {
|
|
102
|
+
const seenModules = new Set();
|
|
103
|
+
for (const item of imports){
|
|
104
|
+
const importedModule = this.isModuleWithAliases(item) ? item.module : item;
|
|
105
|
+
const moduleId = importedModule.id;
|
|
106
|
+
if (seenModules.has(moduleId)) throw new Error(`Duplicate import module: "${importedModule.displayName}" in "${this.displayName}".`);
|
|
107
|
+
seenModules.add(moduleId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
detectCircularDependencies() {
|
|
111
|
+
this._visitedModules.clear();
|
|
112
|
+
this._visitStack.length = 0;
|
|
113
|
+
this.visitModule(this);
|
|
114
|
+
}
|
|
115
|
+
visitModule(module) {
|
|
116
|
+
if (this._visitStack.includes(module)) {
|
|
117
|
+
const cycle = [
|
|
118
|
+
...this._visitStack.slice(this._visitStack.indexOf(module)),
|
|
119
|
+
module
|
|
120
|
+
];
|
|
121
|
+
const cyclePath = cycle.map((m)=>m.displayName).join(" \u2192 ");
|
|
122
|
+
throw new Error(`Circular dependency detected: ${cyclePath}`);
|
|
123
|
+
}
|
|
124
|
+
if (this._visitedModules.has(module.id)) return;
|
|
125
|
+
this._visitStack.push(module);
|
|
126
|
+
const imports = module.imports ?? [];
|
|
127
|
+
for (const item of imports){
|
|
128
|
+
const importedModule = this.isModuleWithAliases(item) ? item.module : item;
|
|
129
|
+
this.visitModule(importedModule);
|
|
130
|
+
}
|
|
131
|
+
this._visitedModules.add(module.id);
|
|
132
|
+
this._visitStack.pop();
|
|
133
|
+
}
|
|
134
|
+
registerDeclarations(container) {
|
|
135
|
+
if (!this._declarations || 0 === this._declarations.length) return;
|
|
136
|
+
for (const decl of this._declarations){
|
|
137
|
+
const { serviceIdentifier, ...options } = decl;
|
|
138
|
+
container.register(serviceIdentifier, options);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
registerImports(container) {
|
|
142
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
143
|
+
const normalizedImports = this.normalizeImports(this._imports);
|
|
144
|
+
for (const { module: sourceModule, serviceIdentifier, as } of normalizedImports)if (serviceIdentifier !== as) container.register(as, {
|
|
145
|
+
useAlias: serviceIdentifier,
|
|
146
|
+
getContainer: ()=>sourceModule.container
|
|
147
|
+
});
|
|
148
|
+
else container.register(serviceIdentifier, {
|
|
149
|
+
useAlias: serviceIdentifier,
|
|
150
|
+
getContainer: ()=>sourceModule.container
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
normalizeImports(imports) {
|
|
154
|
+
return imports.flatMap((item)=>{
|
|
155
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
156
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
157
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
158
|
+
return (module.exports ?? []).map((serviceIdentifier)=>({
|
|
159
|
+
module,
|
|
160
|
+
serviceIdentifier,
|
|
161
|
+
as: aliasMap.get(serviceIdentifier) ?? serviceIdentifier
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
buildAliasMap(aliases) {
|
|
166
|
+
return new Map((aliases ?? []).map((alias)=>[
|
|
167
|
+
alias.serviceIdentifier,
|
|
168
|
+
alias.as
|
|
169
|
+
]));
|
|
170
|
+
}
|
|
171
|
+
validateDeclarations() {
|
|
172
|
+
if (!this._declarations || 0 === this._declarations.length) return;
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
for (const decl of this._declarations){
|
|
175
|
+
const { serviceIdentifier } = decl;
|
|
176
|
+
if (seen.has(serviceIdentifier)) throw new Error(`Duplicate declaration of service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${this.displayName}".`);
|
|
177
|
+
seen.add(serviceIdentifier);
|
|
178
|
+
const hasValidOption = "useClass" in decl || "useFactory" in decl || "useValue" in decl || "useAlias" in decl;
|
|
179
|
+
if (!hasValidOption) throw new Error(`Invalid registration options for service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${this.displayName}": must specify useClass, useFactory, useValue, or useAlias.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
validateExports() {
|
|
183
|
+
if (!this._exports || 0 === this._exports.length) return;
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
for (const exportId of this._exports){
|
|
186
|
+
if (seen.has(exportId)) throw new Error(`Duplicate export of service identifier "${getServiceIdentifierName(exportId)}" in module "${this.displayName}".`);
|
|
187
|
+
seen.add(exportId);
|
|
188
|
+
}
|
|
189
|
+
const availableServices = this.collectAvailableServices();
|
|
190
|
+
for (const exportId of this._exports)if (!availableServices.has(exportId)) throw new Error(`Cannot export service identifier "${getServiceIdentifierName(exportId)}" from "${this.displayName}": it is not declared in this module or imported from any imported module.`);
|
|
191
|
+
}
|
|
192
|
+
collectAvailableServices() {
|
|
193
|
+
const localServices = (this._declarations ?? []).map((decl)=>decl.serviceIdentifier);
|
|
194
|
+
const importedServices = (this._imports ?? []).flatMap((item)=>{
|
|
195
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
196
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
197
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
198
|
+
return (module.exports ?? []).map((serviceId)=>aliasMap.get(serviceId) ?? serviceId);
|
|
199
|
+
});
|
|
200
|
+
return new Set([
|
|
201
|
+
...localServices,
|
|
202
|
+
...importedServices
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
validateAliases(aliases) {
|
|
206
|
+
if (!aliases || 0 === aliases.length) return;
|
|
207
|
+
const exportedSet = new Set(this._exports ?? []);
|
|
208
|
+
const mappedServices = new Set();
|
|
209
|
+
for (const alias of aliases){
|
|
210
|
+
const { serviceIdentifier } = alias;
|
|
211
|
+
if (!exportedSet.has(serviceIdentifier)) throw new Error(`Cannot alias service identifier "${getServiceIdentifierName(serviceIdentifier)}" from module "${this.displayName}": it is not exported from that module.`);
|
|
212
|
+
if (mappedServices.has(serviceIdentifier)) throw new Error(`Duplicate alias mapping for service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${this.displayName}".`);
|
|
213
|
+
mappedServices.add(serviceIdentifier);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
validateAliasConflictsWithDeclarations() {
|
|
217
|
+
if (!this._imports || !this._declarations) return;
|
|
218
|
+
const localDeclarations = new Set((this._declarations ?? []).map((decl)=>decl.serviceIdentifier));
|
|
219
|
+
const conflictingAlias = (this._imports ?? []).filter((item)=>this.isModuleWithAliases(item)).flatMap((item)=>item.aliases ?? []).find((alias)=>localDeclarations.has(alias.as));
|
|
220
|
+
if (conflictingAlias) throw new Error(`Alias "${getServiceIdentifierName(conflictingAlias.as)}" conflicts with local declaration in module "${this.displayName}".`);
|
|
221
|
+
}
|
|
222
|
+
validateImportNamingConflicts() {
|
|
223
|
+
if (!this._imports || 0 === this._imports.length) return;
|
|
224
|
+
const serviceToModules = (this._imports ?? []).reduce((acc, item)=>{
|
|
225
|
+
const module = this.isModuleWithAliases(item) ? item.module : item;
|
|
226
|
+
const aliases = this.isModuleWithAliases(item) ? item.aliases : void 0;
|
|
227
|
+
const aliasMap = this.buildAliasMap(aliases);
|
|
228
|
+
for (const serviceId of module.exports ?? []){
|
|
229
|
+
const effectiveName = aliasMap.get(serviceId) ?? serviceId;
|
|
230
|
+
const modules = acc.get(effectiveName) ?? [];
|
|
231
|
+
modules.push(module);
|
|
232
|
+
acc.set(effectiveName, modules);
|
|
233
|
+
}
|
|
234
|
+
return acc;
|
|
235
|
+
}, new Map());
|
|
236
|
+
const conflicts = Array.from(serviceToModules.entries()).filter(([, modules])=>modules.length > 1);
|
|
237
|
+
if (conflicts.length > 0) {
|
|
238
|
+
const [serviceId, modules] = conflicts[0];
|
|
239
|
+
const moduleNames = modules.map((m)=>`"${m.displayName}"`).join(", ");
|
|
240
|
+
throw new Error(`Service identifier "${getServiceIdentifierName(serviceId)}" is exported by multiple imported modules: ${moduleNames}. Consider using aliases to resolve the conflict.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
isModuleWithAliases(item) {
|
|
244
|
+
return "module" in item;
|
|
245
|
+
}
|
|
243
246
|
}
|
|
244
247
|
function createModule(options) {
|
|
245
248
|
return new Module(options);
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { CreateRegistrationOptions, IContainer, IDisplayName, IUnique, ServiceIdentifier } from "@husky-di/core";
|
|
7
7
|
export type Declaration<T> = CreateRegistrationOptions<T> & {
|
|
8
|
-
serviceIdentifier: ServiceIdentifier<T
|
|
8
|
+
readonly serviceIdentifier: ServiceIdentifier<T>;
|
|
9
9
|
};
|
|
10
10
|
export type Alias = {
|
|
11
|
-
serviceIdentifier: ServiceIdentifier<unknown>;
|
|
12
|
-
as: ServiceIdentifier<unknown>;
|
|
11
|
+
readonly serviceIdentifier: ServiceIdentifier<unknown>;
|
|
12
|
+
readonly as: ServiceIdentifier<unknown>;
|
|
13
13
|
};
|
|
14
14
|
export type CreateModuleOptions = {
|
|
15
15
|
readonly name: string;
|
|
@@ -18,14 +18,14 @@ export type CreateModuleOptions = {
|
|
|
18
18
|
readonly exports?: ServiceIdentifier<unknown>[];
|
|
19
19
|
};
|
|
20
20
|
export type ModuleWithAliases = {
|
|
21
|
-
module: IModule;
|
|
22
|
-
aliases?: Alias[];
|
|
21
|
+
readonly module: IModule;
|
|
22
|
+
readonly aliases?: Alias[];
|
|
23
23
|
};
|
|
24
24
|
export interface IModule extends IUnique, IDisplayName, Pick<IContainer, "resolve" | "isRegistered" | "getServiceIdentifiers" | "use" | "unused"> {
|
|
25
25
|
readonly name: string;
|
|
26
|
-
readonly declarations?: Declaration<unknown
|
|
27
|
-
readonly imports?:
|
|
28
|
-
readonly exports?: ServiceIdentifier<unknown
|
|
26
|
+
readonly declarations?: ReadonlyArray<Declaration<unknown>>;
|
|
27
|
+
readonly imports?: ReadonlyArray<IModule | ModuleWithAliases>;
|
|
28
|
+
readonly exports?: ReadonlyArray<ServiceIdentifier<unknown>>;
|
|
29
29
|
readonly container: IContainer;
|
|
30
30
|
withAliases(aliases: Alias[]): ModuleWithAliases;
|
|
31
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@husky-di/module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@husky-di/core": "1.0.
|
|
18
|
+
"@husky-di/core": "1.0.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@rslib/core": "^0.11.2",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"typescript": "^5.9.2",
|
|
24
24
|
"vitest": "^3.2.4"
|
|
25
25
|
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build": "rslib build",
|
|
28
31
|
"dev": "rslib build --watch",
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @overview 模块工具函数,包含模块构建和验证逻辑
|
|
3
|
-
* @author AEPKILL
|
|
4
|
-
* @created 2025-08-12 19:57:50
|
|
5
|
-
*/
|
|
6
|
-
import { type IContainer } from "@husky-di/core";
|
|
7
|
-
import type { CreateModuleOptions, IModule, ModuleWithAliases } from "../interfaces/module.interface";
|
|
8
|
-
/**
|
|
9
|
-
* 类型守卫:检查模块导入是否包含别名映射
|
|
10
|
-
*
|
|
11
|
-
* @param moduleImport 模块导入对象
|
|
12
|
-
* @returns 如果包含别名映射则返回 true,否则返回 false
|
|
13
|
-
*/
|
|
14
|
-
export declare function isModuleWithAliases(moduleImport: NonNullable<CreateModuleOptions["imports"]>[number]): moduleImport is ModuleWithAliases;
|
|
15
|
-
/**
|
|
16
|
-
* 从模块导入中获取实际的模块对象
|
|
17
|
-
*
|
|
18
|
-
* @param moduleImport 模块导入(可能包含别名)
|
|
19
|
-
* @returns 实际的模块对象
|
|
20
|
-
*/
|
|
21
|
-
export declare function getModuleByImport(moduleImport: NonNullable<CreateModuleOptions["imports"]>[number]): IModule;
|
|
22
|
-
/**
|
|
23
|
-
* 构建模块容器的公共函数
|
|
24
|
-
*
|
|
25
|
-
* @param module 要构建的模块
|
|
26
|
-
* @returns 构建好的容器
|
|
27
|
-
*/
|
|
28
|
-
export declare function build(module: IModule): IContainer;
|
|
29
|
-
/**
|
|
30
|
-
* 模块构建器类,整合模块验证和构建逻辑
|
|
31
|
-
*
|
|
32
|
-
* 在验证过程中收集服务标识符信息,并在构建过程中复用这些信息
|
|
33
|
-
*/
|
|
34
|
-
export declare class ModuleBuilder {
|
|
35
|
-
/** 要构建的模块 */
|
|
36
|
-
private readonly module;
|
|
37
|
-
/** 服务标识符映射表(验证时构建,构建时复用) */
|
|
38
|
-
private readonly serviceIdentifierMap;
|
|
39
|
-
/** 可用服务标识符集合(验证时构建,构建时复用) */
|
|
40
|
-
private readonly availableServiceIdentifiers;
|
|
41
|
-
/** 导入模块的别名映射缓存 */
|
|
42
|
-
private readonly importAliasesCache;
|
|
43
|
-
constructor(module: IModule);
|
|
44
|
-
/**
|
|
45
|
-
* 构建模块容器
|
|
46
|
-
*
|
|
47
|
-
* @returns 构建好的容器
|
|
48
|
-
* @throws {Error} 当模块配置无效时抛出错误
|
|
49
|
-
*/
|
|
50
|
-
build(): IContainer;
|
|
51
|
-
/**
|
|
52
|
-
* 验证模块并收集信息
|
|
53
|
-
*
|
|
54
|
-
* @throws {Error} 当模块配置无效时抛出错误
|
|
55
|
-
*/
|
|
56
|
-
validateAndCollectInfo(): void;
|
|
57
|
-
/**
|
|
58
|
-
* 验证导入模块的唯一性
|
|
59
|
-
*
|
|
60
|
-
* @throws {Error} 当存在重复导入时抛出错误
|
|
61
|
-
*/
|
|
62
|
-
private validateImportUniqueness;
|
|
63
|
-
/**
|
|
64
|
-
* 验证导出服务标识符的唯一性
|
|
65
|
-
*
|
|
66
|
-
* @throws {Error} 当存在重复导出时抛出错误
|
|
67
|
-
*/
|
|
68
|
-
private validateExportUniqueness;
|
|
69
|
-
/**
|
|
70
|
-
* 验证循环依赖
|
|
71
|
-
*
|
|
72
|
-
* 使用深度优先搜索(DFS)算法检测模块之间的循环依赖
|
|
73
|
-
*
|
|
74
|
-
* @throws {Error} 当检测到循环依赖时抛出错误
|
|
75
|
-
*/
|
|
76
|
-
private validateCircularDependencies;
|
|
77
|
-
/**
|
|
78
|
-
* 递归检测循环依赖的核心方法
|
|
79
|
-
*
|
|
80
|
-
* @param currentModule 当前检查的模块
|
|
81
|
-
* @param visited 已完全访问过的模块集合(白色节点)
|
|
82
|
-
* @param visiting 正在访问中的模块集合(灰色节点)
|
|
83
|
-
* @param dependencyPath 当前依赖路径,用于构建错误信息
|
|
84
|
-
* @throws {Error} 当检测到循环依赖时抛出错误
|
|
85
|
-
*/
|
|
86
|
-
private detectCircularDependency;
|
|
87
|
-
/**
|
|
88
|
-
* 收集服务标识符信息并验证冲突
|
|
89
|
-
*
|
|
90
|
-
* @throws {Error} 当存在服务标识符冲突时抛出错误
|
|
91
|
-
*/
|
|
92
|
-
private collectServiceInfoAndValidateConflicts;
|
|
93
|
-
/**
|
|
94
|
-
* 验证导出服务标识符的有效性
|
|
95
|
-
*
|
|
96
|
-
* @throws {Error} 当导出的服务标识符不可用时抛出错误
|
|
97
|
-
*/
|
|
98
|
-
private validateExportValidity;
|
|
99
|
-
/**
|
|
100
|
-
* 构建并缓存别名映射
|
|
101
|
-
*
|
|
102
|
-
* @param importModule 导入模块配置
|
|
103
|
-
* @param importedModule 实际导入的模块
|
|
104
|
-
* @returns 别名映射
|
|
105
|
-
*/
|
|
106
|
-
private buildAndCacheAliasesMap;
|
|
107
|
-
/**
|
|
108
|
-
* 注册声明的服务
|
|
109
|
-
*
|
|
110
|
-
* @param container 目标容器
|
|
111
|
-
*/
|
|
112
|
-
private registerDeclarations;
|
|
113
|
-
/**
|
|
114
|
-
* 注册导入的服务
|
|
115
|
-
*
|
|
116
|
-
* @param container 目标容器
|
|
117
|
-
*/
|
|
118
|
-
private registerImports;
|
|
119
|
-
/**
|
|
120
|
-
* 构建服务标识符冲突的详细错误消息
|
|
121
|
-
*
|
|
122
|
-
* @param conflictInfo 包含冲突详细信息的对象
|
|
123
|
-
* @returns 格式化的错误消息字符串
|
|
124
|
-
*/
|
|
125
|
-
private buildConflictMessage;
|
|
126
|
-
}
|