@husky-di/module 1.0.0 → 1.1.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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @overview Module error code enum.
3
+ * @author AEPKILL
4
+ * @created 2026-05-09 00:00:00
5
+ */
6
+ export declare enum ModuleErrorCodeEnum {
7
+ /**
8
+ * Module contains multiple declarations with the same ServiceIdentifier.
9
+ * @see SPECIFICATION.md Section 4.1 D1
10
+ */
11
+ E_DUPLICATE_DECLARATION = "E_DUPLICATE_DECLARATION",
12
+ /**
13
+ * Declaration lacks valid registration strategy (useClass, useFactory, useValue, or useAlias).
14
+ * @see SPECIFICATION.md Section 4.1 D2
15
+ */
16
+ E_INVALID_REGISTRATION = "E_INVALID_REGISTRATION",
17
+ /**
18
+ * Same module instance imported multiple times in a single import list.
19
+ * @see SPECIFICATION.md Section 4.2 I1
20
+ */
21
+ E_DUPLICATE_IMPORT_MODULE = "E_DUPLICATE_IMPORT_MODULE",
22
+ /**
23
+ * Circular dependency detected in module import graph.
24
+ * @see SPECIFICATION.md Section 4.2 I2
25
+ */
26
+ E_CIRCULAR_DEPENDENCY = "E_CIRCULAR_DEPENDENCY",
27
+ /**
28
+ * Multiple imported modules export the same ServiceIdentifier without alias resolution.
29
+ * @see SPECIFICATION.md Section 4.2 I3
30
+ */
31
+ E_IMPORT_COLLISION = "E_IMPORT_COLLISION",
32
+ /**
33
+ * Imported ServiceIdentifier conflicts with a local declaration in the importing module.
34
+ * @see SPECIFICATION.md Section 4.2 I4
35
+ */
36
+ E_IMPORT_CONFLICT_LOCAL = "E_IMPORT_CONFLICT_LOCAL",
37
+ /**
38
+ * Alias source ServiceIdentifier is not exported by the source module.
39
+ * @see SPECIFICATION.md Section 5.1 Resolution Logic
40
+ */
41
+ E_ALIAS_SOURCE_NOT_EXPORTED = "E_ALIAS_SOURCE_NOT_EXPORTED",
42
+ /**
43
+ * Same source ServiceIdentifier mapped multiple times within a single alias list.
44
+ * @see SPECIFICATION.md Section 5.1 Resolution Logic
45
+ */
46
+ E_DUPLICATE_ALIAS_MAP = "E_DUPLICATE_ALIAS_MAP",
47
+ /**
48
+ * Export references a ServiceIdentifier that is neither declared locally nor imported.
49
+ * @see SPECIFICATION.md Section 4.3 E1
50
+ */
51
+ E_EXPORT_NOT_FOUND = "E_EXPORT_NOT_FOUND",
52
+ /**
53
+ * Same ServiceIdentifier exported multiple times from a single module.
54
+ * @see SPECIFICATION.md Section 4.3 E2
55
+ */
56
+ E_DUPLICATE_EXPORT = "E_DUPLICATE_EXPORT"
57
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @overview Module package exception.
3
+ * @author AEPKILL
4
+ * @created 2026-05-09 00:00:00
5
+ */
6
+ import { CodedException } from "@husky-di/core";
7
+ import type { ModuleErrorCode } from "../types/module-error-code.type";
8
+ export declare class ModuleException extends CodedException<ModuleErrorCode> {
9
+ /** Internal marker to identify ModuleException instances across frames. */
10
+ private __isModuleException__;
11
+ static isModuleException(error: unknown): error is ModuleException;
12
+ }
@@ -1,7 +1,38 @@
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
- export declare function createExportedGuardMiddlewareFactory(exports: ServiceIdentifier<unknown>[]): ResolveMiddleware<any, any>;
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
+ * @important Do not remove this middleware from the container once registered.
35
+ * Removing this middleware will disable export protection and allow external access
36
+ * to non-exported services.
37
+ */
38
+ export declare function createExportedGuardMiddleware(exports: ReadonlyArray<ServiceIdentifier<unknown>>): ResolveMiddleware<any, any>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Import scope factory.
3
+ *
4
+ * @overview
5
+ * Creates the normalized import visibility model for module imports.
6
+ *
7
+ * @author AEPKILL
8
+ * @created 2026-05-09 00:00:00
9
+ */
10
+ import type { IModule, ModuleWithAliases } from "../interfaces/module.interface";
11
+ import type { ImportScope } from "../types/import-scope.type";
12
+ export declare function createImportScope(imports?: ReadonlyArray<IModule | ModuleWithAliases>): ImportScope;
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @overview
2
+ * @overview Module implementation.
3
3
  * @author AEPKILL
4
4
  * @created 2025-08-09 14:51:09
5
5
  */
6
- import type { IContainer, IsRegisteredOptions, ResolveInstance, ResolveMiddleware, ResolveOptions, ServiceIdentifier } from "@husky-di/core";
6
+ import { type Cleanup, 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
- export declare class Module implements IModule {
8
+ export declare class ModuleImpl implements IModule {
9
9
  get id(): string;
10
10
  get name(): string;
11
11
  get declarations(): Declaration<unknown>[] | undefined;
@@ -13,16 +13,23 @@ export declare class Module implements IModule {
13
13
  get imports(): (IModule | ModuleWithAliases)[] | undefined;
14
14
  get displayName(): string;
15
15
  readonly container: IContainer;
16
- private _id;
17
- private _name;
18
- private _declarations?;
19
- private _imports?;
20
- private _exports?;
16
+ private readonly _id;
17
+ private readonly _name;
18
+ private readonly _declarations?;
19
+ private readonly _imports?;
20
+ private readonly _exports?;
21
+ private readonly _importScope;
21
22
  constructor(options: CreateModuleOptions);
22
23
  resolve<T, O extends ResolveOptions<T>>(serviceIdentifier: ServiceIdentifier<T>, options?: O): ResolveInstance<T, O>;
23
24
  isRegistered<T>(serviceIdentifier: ServiceIdentifier<T>, options?: IsRegisteredOptions): boolean;
24
25
  getServiceIdentifiers(): ServiceIdentifier<unknown>[];
25
- use(middleware: ResolveMiddleware<any, any>): void;
26
- unused(middleware: ResolveMiddleware<any, any>): void;
26
+ use(...middleware: ResolveMiddleware<any, any>[]): Cleanup;
27
+ unused(...middleware: ResolveMiddleware<any, any>[]): void;
27
28
  withAliases(aliases: Alias[]): ModuleWithAliases;
29
+ private buildContainer;
30
+ private validateConfiguration;
31
+ private validateImports;
32
+ private registerDeclarations;
33
+ private registerImports;
34
+ private validateExports;
28
35
  }
package/dist/index.cjs CHANGED
@@ -13,7 +13,7 @@ var __webpack_require__ = {};
13
13
  })();
14
14
  (()=>{
15
15
  __webpack_require__.r = (exports1)=>{
16
- if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
17
  value: 'Module'
18
18
  });
19
19
  Object.defineProperty(exports1, '__esModule', {
@@ -24,10 +24,31 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ ModuleErrorCodeEnum: ()=>module_error_code_enum_ModuleErrorCodeEnum,
28
+ ModuleException: ()=>ModuleException,
27
29
  createModule: ()=>createModule
28
30
  });
31
+ var module_error_code_enum_ModuleErrorCodeEnum = /*#__PURE__*/ function(ModuleErrorCodeEnum) {
32
+ ModuleErrorCodeEnum["E_DUPLICATE_DECLARATION"] = "E_DUPLICATE_DECLARATION";
33
+ ModuleErrorCodeEnum["E_INVALID_REGISTRATION"] = "E_INVALID_REGISTRATION";
34
+ ModuleErrorCodeEnum["E_DUPLICATE_IMPORT_MODULE"] = "E_DUPLICATE_IMPORT_MODULE";
35
+ ModuleErrorCodeEnum["E_CIRCULAR_DEPENDENCY"] = "E_CIRCULAR_DEPENDENCY";
36
+ ModuleErrorCodeEnum["E_IMPORT_COLLISION"] = "E_IMPORT_COLLISION";
37
+ ModuleErrorCodeEnum["E_IMPORT_CONFLICT_LOCAL"] = "E_IMPORT_CONFLICT_LOCAL";
38
+ ModuleErrorCodeEnum["E_ALIAS_SOURCE_NOT_EXPORTED"] = "E_ALIAS_SOURCE_NOT_EXPORTED";
39
+ ModuleErrorCodeEnum["E_DUPLICATE_ALIAS_MAP"] = "E_DUPLICATE_ALIAS_MAP";
40
+ ModuleErrorCodeEnum["E_EXPORT_NOT_FOUND"] = "E_EXPORT_NOT_FOUND";
41
+ ModuleErrorCodeEnum["E_DUPLICATE_EXPORT"] = "E_DUPLICATE_EXPORT";
42
+ return ModuleErrorCodeEnum;
43
+ }({});
29
44
  const core_namespaceObject = require("@husky-di/core");
30
- function createExportedGuardMiddlewareFactory(exports1) {
45
+ class ModuleException extends core_namespaceObject.CodedException {
46
+ __isModuleException__ = true;
47
+ static isModuleException(error) {
48
+ return error?.__isModuleException__ === true;
49
+ }
50
+ }
51
+ function createExportedGuardMiddleware(exports1) {
31
52
  const exportedSet = new Set(exports1);
32
53
  return {
33
54
  name: "ExportGuard",
@@ -38,7 +59,7 @@ function createExportedGuardMiddlewareFactory(exports1) {
38
59
  if (!exportedSet.has(serviceIdentifier)) {
39
60
  if (container.isRegistered(serviceIdentifier, {
40
61
  recursive: true
41
- })) throw new core_namespaceObject.ResolveException(`Service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" is not exported from ${container.displayName}.`, resolveRecord);
62
+ })) throw new core_namespaceObject.ResolveException(module_error_code_enum_ModuleErrorCodeEnum.E_EXPORT_NOT_FOUND, `Service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" is not exported from ${container.displayName}.`, resolveRecord);
42
63
  }
43
64
  return next(params);
44
65
  }
@@ -51,170 +72,131 @@ function findPreviousContainer(paths) {
51
72
  lastContainer = path.value.container;
52
73
  }
53
74
  }
54
- function isModuleWithAliases(moduleImport) {
55
- return moduleImport.module instanceof Module;
75
+ function isModuleWithAliases(item) {
76
+ return "module" in item;
56
77
  }
57
- function getModuleByImport(moduleImport) {
58
- return isModuleWithAliases(moduleImport) ? moduleImport.module : moduleImport;
78
+ function createImportScope(imports) {
79
+ const bindings = (imports ?? []).flatMap((item)=>{
80
+ const sourceModule = isModuleWithAliases(item) ? item.module : item;
81
+ const aliases = isModuleWithAliases(item) ? item.aliases ?? [] : [];
82
+ const aliasMap = createAliasMap(aliases);
83
+ return (sourceModule.exports ?? []).map((sourceServiceIdentifier)=>{
84
+ const localServiceIdentifier = aliasMap.get(sourceServiceIdentifier) ?? sourceServiceIdentifier;
85
+ return {
86
+ sourceModule,
87
+ sourceServiceIdentifier,
88
+ localServiceIdentifier,
89
+ isAliased: aliasMap.has(sourceServiceIdentifier)
90
+ };
91
+ });
92
+ });
93
+ return {
94
+ bindings,
95
+ visibleServiceIdentifiers: new Set(bindings.map((binding)=>binding.localServiceIdentifier))
96
+ };
59
97
  }
60
- function build(module) {
61
- const builder = new ModuleBuilder(module);
62
- return builder.build();
98
+ function createAliasMap(aliases) {
99
+ return new Map(aliases.map((alias)=>[
100
+ alias.serviceIdentifier,
101
+ alias.as
102
+ ]));
63
103
  }
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
- }
104
+ function validateDeclarations(moduleName, declarations) {
105
+ if (!declarations || 0 === declarations.length) return;
106
+ const seen = new Set();
107
+ for (const decl of declarations){
108
+ const { serviceIdentifier } = decl;
109
+ if (seen.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_DECLARATION, `Duplicate declaration of service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${moduleName}".`);
110
+ seen.add(serviceIdentifier);
111
+ const hasValidOption = "useClass" in decl || "useFactory" in decl || "useValue" in decl || "useAlias" in decl;
112
+ if (!hasValidOption) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_INVALID_REGISTRATION, `Invalid registration options for service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${moduleName}": must specify useClass, useFactory, useValue, or useAlias.`);
99
113
  }
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
- }
114
+ }
115
+ function validateImportUniqueness(moduleName, imports) {
116
+ const seenModules = new Set();
117
+ for (const item of imports){
118
+ const importedModule = isModuleWithAliases(item) ? item.module : item;
119
+ const moduleId = importedModule.id;
120
+ if (seenModules.has(moduleId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_IMPORT_MODULE, `Duplicate import module: "${importedModule.displayName}" in "${moduleName}".`);
121
+ seenModules.add(moduleId);
171
122
  }
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.`);
123
+ }
124
+ function validateImportAliases(imports) {
125
+ for (const item of imports)if (isModuleWithAliases(item)) validateAliases(item.module.displayName, item.aliases ?? [], item.module.exports);
126
+ }
127
+ function detectCircularDependencies(module, visitedModules = new Set(), visitStack = []) {
128
+ if (visitStack.includes(module)) {
129
+ const cycle = [
130
+ ...visitStack.slice(visitStack.indexOf(module)),
131
+ module
132
+ ];
133
+ const cyclePath = cycle.map((m)=>m.displayName).join(" → ");
134
+ throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_CIRCULAR_DEPENDENCY, `Circular dependency detected: ${cyclePath}`);
176
135
  }
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;
136
+ if (visitedModules.has(module.id)) return;
137
+ visitStack.push(module);
138
+ const imports = module.imports ?? [];
139
+ for (const item of imports){
140
+ const importedModule = isModuleWithAliases(item) ? item.module : item;
141
+ detectCircularDependencies(importedModule, visitedModules, visitStack);
186
142
  }
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
- }
143
+ visitedModules.add(module.id);
144
+ visitStack.pop();
145
+ }
146
+ function validateAliases(moduleName, aliases, exports1) {
147
+ if (!aliases || 0 === aliases.length) return;
148
+ const exportedSet = new Set(exports1 ?? []);
149
+ const mappedServices = new Set();
150
+ for (const alias of aliases){
151
+ const { serviceIdentifier } = alias;
152
+ if (!exportedSet.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_ALIAS_SOURCE_NOT_EXPORTED, `Cannot alias service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" from module "${moduleName}": it is not exported from that module.`);
153
+ if (mappedServices.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_ALIAS_MAP, `Duplicate alias mapping for service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceIdentifier)}" in module "${moduleName}".`);
154
+ mappedServices.add(serviceIdentifier);
194
155
  }
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
- }
156
+ }
157
+ function validateImportConflictsWithDeclarations(moduleName, importScope, declarations) {
158
+ if (!declarations) return;
159
+ const localDeclarations = new Set((declarations ?? []).map((decl)=>decl.serviceIdentifier));
160
+ const conflictingImport = importScope.bindings.find((binding)=>localDeclarations.has(binding.localServiceIdentifier));
161
+ if (conflictingImport) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_IMPORT_CONFLICT_LOCAL, `Imported service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(conflictingImport.localServiceIdentifier)}" conflicts with local declaration in module "${moduleName}". Use an alias to resolve the conflict.`);
162
+ }
163
+ function validateImportNamingConflicts(_moduleName, importScope) {
164
+ if (0 === importScope.bindings.length) return;
165
+ const serviceToModules = importScope.bindings.reduce((acc, binding)=>{
166
+ const modules = acc.get(binding.localServiceIdentifier) ?? [];
167
+ modules.push(binding.sourceModule);
168
+ acc.set(binding.localServiceIdentifier, modules);
169
+ return acc;
170
+ }, new Map());
171
+ const conflicts = Array.from(serviceToModules.entries()).filter(([, modules])=>modules.length > 1);
172
+ if (conflicts.length > 0) {
173
+ const [serviceId, modules] = conflicts[0];
174
+ const moduleNames = modules.map((m)=>`"${m.displayName}"`).join(", ");
175
+ throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_IMPORT_COLLISION, `Service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(serviceId)}" is exported by multiple imported modules: ${moduleNames}. Consider using aliases to resolve the conflict.`);
208
176
  }
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}".`;
177
+ }
178
+ function validateExportUniqueness(moduleName, exports1) {
179
+ if (!exports1 || 0 === exports1.length) return;
180
+ const seen = new Set();
181
+ for (const exportId of exports1){
182
+ if (seen.has(exportId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_EXPORT, `Duplicate export of service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(exportId)}" in module "${moduleName}".`);
183
+ seen.add(exportId);
214
184
  }
215
185
  }
186
+ function collectAvailableServices(declarations, importScope) {
187
+ const localServices = (declarations ?? []).map((decl)=>decl.serviceIdentifier);
188
+ return new Set([
189
+ ...localServices,
190
+ ...importScope?.visibleServiceIdentifiers ?? []
191
+ ]);
192
+ }
193
+ function validateExportAvailability(moduleName, exports1, declarations, importScope) {
194
+ if (!exports1 || 0 === exports1.length) return;
195
+ const availableServices = collectAvailableServices(declarations, importScope);
196
+ for (const exportId of exports1)if (!availableServices.has(exportId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_EXPORT_NOT_FOUND, `Cannot export service identifier "${(0, core_namespaceObject.getServiceIdentifierName)(exportId)}" from "${moduleName}": it is not declared in this module or imported from any imported module.`);
197
+ }
216
198
  const createModuleId = (0, core_namespaceObject.incrementalIdFactory)("MODULE");
217
- class Module {
199
+ class ModuleImpl {
218
200
  get id() {
219
201
  return this._id;
220
202
  }
@@ -231,7 +213,7 @@ class Module {
231
213
  return this._imports;
232
214
  }
233
215
  get displayName() {
234
- return `${String(this._name)}#${this._id}`;
216
+ return `${String(this._name)}/${this._id}`;
235
217
  }
236
218
  container;
237
219
  _id;
@@ -239,13 +221,17 @@ class Module {
239
221
  _declarations;
240
222
  _imports;
241
223
  _exports;
224
+ _importScope;
242
225
  constructor(options){
243
226
  this._id = createModuleId();
244
227
  this._name = options.name;
245
228
  this._declarations = options.declarations;
246
229
  this._imports = options.imports;
247
230
  this._exports = options.exports;
248
- this.container = build(this);
231
+ this._importScope = createImportScope(this._imports);
232
+ this.validateConfiguration();
233
+ this.container = this.buildContainer();
234
+ this.container.use(createExportedGuardMiddleware(this.exports ?? []));
249
235
  }
250
236
  resolve(serviceIdentifier, options) {
251
237
  return this.container.resolve(serviceIdentifier, options);
@@ -256,26 +242,69 @@ class Module {
256
242
  getServiceIdentifiers() {
257
243
  return this.container.getServiceIdentifiers();
258
244
  }
259
- use(middleware) {
260
- this.container.use(middleware);
245
+ use(...middleware) {
246
+ return this.container.use(...middleware);
261
247
  }
262
- unused(middleware) {
263
- this.container.unused(middleware);
248
+ unused(...middleware) {
249
+ this.container.unused(...middleware);
264
250
  }
265
251
  withAliases(aliases) {
252
+ validateAliases(this.displayName, aliases, this._exports);
266
253
  return {
267
254
  module: this,
268
255
  aliases
269
256
  };
270
257
  }
258
+ buildContainer() {
259
+ const container = (0, core_namespaceObject.createContainer)(this._name);
260
+ this.registerDeclarations(container);
261
+ this.registerImports(container);
262
+ return container;
263
+ }
264
+ validateConfiguration() {
265
+ validateDeclarations(this.displayName, this._declarations);
266
+ this.validateImports();
267
+ this.validateExports();
268
+ }
269
+ validateImports() {
270
+ if (!this._imports || 0 === this._imports.length) return;
271
+ validateImportUniqueness(this.displayName, this._imports);
272
+ validateImportAliases(this._imports);
273
+ detectCircularDependencies(this);
274
+ validateImportConflictsWithDeclarations(this.displayName, this._importScope, this._declarations);
275
+ validateImportNamingConflicts(this.displayName, this._importScope);
276
+ }
277
+ registerDeclarations(container) {
278
+ if (!this._declarations || 0 === this._declarations.length) return;
279
+ for (const decl of this._declarations){
280
+ const { serviceIdentifier, ...options } = decl;
281
+ container.register(serviceIdentifier, options);
282
+ }
283
+ }
284
+ registerImports(container) {
285
+ if (0 === this._importScope.bindings.length) return;
286
+ for (const binding of this._importScope.bindings)container.register(binding.localServiceIdentifier, {
287
+ useAlias: binding.sourceServiceIdentifier,
288
+ getContainer: ()=>binding.sourceModule.container
289
+ });
290
+ }
291
+ validateExports() {
292
+ if (!this._exports || 0 === this._exports.length) return;
293
+ validateExportUniqueness(this.displayName, this._exports);
294
+ validateExportAvailability(this.displayName, this._exports, this._declarations, this._importScope);
295
+ }
271
296
  }
272
297
  function createModule(options) {
273
- return new Module(options);
298
+ return new ModuleImpl(options);
274
299
  }
300
+ exports.ModuleErrorCodeEnum = __webpack_exports__.ModuleErrorCodeEnum;
301
+ exports.ModuleException = __webpack_exports__.ModuleException;
275
302
  exports.createModule = __webpack_exports__.createModule;
276
- for(var __webpack_i__ in __webpack_exports__)if (-1 === [
303
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
304
+ "ModuleErrorCodeEnum",
305
+ "ModuleException",
277
306
  "createModule"
278
- ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
307
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
279
308
  Object.defineProperty(exports, '__esModule', {
280
309
  value: true
281
310
  });
package/dist/index.d.ts CHANGED
@@ -3,5 +3,8 @@
3
3
  * @author AEPKILL
4
4
  * @created 2025-08-12 22:53:27
5
5
  */
6
+ export { ModuleErrorCodeEnum } from "./enums/module-error-code.enum";
7
+ export { ModuleException } from "./exceptions/module.exception";
6
8
  export { createModule } from "./factories/module.factory";
7
9
  export type { CreateModuleOptions, IModule, } from "./interfaces/module.interface";
10
+ export type { ModuleErrorCode } from "./types/module-error-code.type";
package/dist/index.js CHANGED
@@ -1,5 +1,24 @@
1
- import { ResolveException, createContainer, getServiceIdentifierName, incrementalIdFactory, isResolveServiceIdentifierRecord } from "@husky-di/core";
2
- function createExportedGuardMiddlewareFactory(exports) {
1
+ import { CodedException, ResolveException, createContainer, getServiceIdentifierName, incrementalIdFactory, isResolveServiceIdentifierRecord } from "@husky-di/core";
2
+ var module_error_code_enum_ModuleErrorCodeEnum = /*#__PURE__*/ function(ModuleErrorCodeEnum) {
3
+ ModuleErrorCodeEnum["E_DUPLICATE_DECLARATION"] = "E_DUPLICATE_DECLARATION";
4
+ ModuleErrorCodeEnum["E_INVALID_REGISTRATION"] = "E_INVALID_REGISTRATION";
5
+ ModuleErrorCodeEnum["E_DUPLICATE_IMPORT_MODULE"] = "E_DUPLICATE_IMPORT_MODULE";
6
+ ModuleErrorCodeEnum["E_CIRCULAR_DEPENDENCY"] = "E_CIRCULAR_DEPENDENCY";
7
+ ModuleErrorCodeEnum["E_IMPORT_COLLISION"] = "E_IMPORT_COLLISION";
8
+ ModuleErrorCodeEnum["E_IMPORT_CONFLICT_LOCAL"] = "E_IMPORT_CONFLICT_LOCAL";
9
+ ModuleErrorCodeEnum["E_ALIAS_SOURCE_NOT_EXPORTED"] = "E_ALIAS_SOURCE_NOT_EXPORTED";
10
+ ModuleErrorCodeEnum["E_DUPLICATE_ALIAS_MAP"] = "E_DUPLICATE_ALIAS_MAP";
11
+ ModuleErrorCodeEnum["E_EXPORT_NOT_FOUND"] = "E_EXPORT_NOT_FOUND";
12
+ ModuleErrorCodeEnum["E_DUPLICATE_EXPORT"] = "E_DUPLICATE_EXPORT";
13
+ return ModuleErrorCodeEnum;
14
+ }({});
15
+ class ModuleException extends CodedException {
16
+ __isModuleException__ = true;
17
+ static isModuleException(error) {
18
+ return error?.__isModuleException__ === true;
19
+ }
20
+ }
21
+ function createExportedGuardMiddleware(exports) {
3
22
  const exportedSet = new Set(exports);
4
23
  return {
5
24
  name: "ExportGuard",
@@ -10,7 +29,7 @@ function createExportedGuardMiddlewareFactory(exports) {
10
29
  if (!exportedSet.has(serviceIdentifier)) {
11
30
  if (container.isRegistered(serviceIdentifier, {
12
31
  recursive: true
13
- })) throw new ResolveException(`Service identifier "${getServiceIdentifierName(serviceIdentifier)}" is not exported from ${container.displayName}.`, resolveRecord);
32
+ })) throw new ResolveException(module_error_code_enum_ModuleErrorCodeEnum.E_EXPORT_NOT_FOUND, `Service identifier "${getServiceIdentifierName(serviceIdentifier)}" is not exported from ${container.displayName}.`, resolveRecord);
14
33
  }
15
34
  return next(params);
16
35
  }
@@ -23,170 +42,131 @@ function findPreviousContainer(paths) {
23
42
  lastContainer = path.value.container;
24
43
  }
25
44
  }
26
- function isModuleWithAliases(moduleImport) {
27
- return moduleImport.module instanceof Module;
45
+ function isModuleWithAliases(item) {
46
+ return "module" in item;
28
47
  }
29
- function getModuleByImport(moduleImport) {
30
- return isModuleWithAliases(moduleImport) ? moduleImport.module : moduleImport;
48
+ function createImportScope(imports) {
49
+ const bindings = (imports ?? []).flatMap((item)=>{
50
+ const sourceModule = isModuleWithAliases(item) ? item.module : item;
51
+ const aliases = isModuleWithAliases(item) ? item.aliases ?? [] : [];
52
+ const aliasMap = createAliasMap(aliases);
53
+ return (sourceModule.exports ?? []).map((sourceServiceIdentifier)=>{
54
+ const localServiceIdentifier = aliasMap.get(sourceServiceIdentifier) ?? sourceServiceIdentifier;
55
+ return {
56
+ sourceModule,
57
+ sourceServiceIdentifier,
58
+ localServiceIdentifier,
59
+ isAliased: aliasMap.has(sourceServiceIdentifier)
60
+ };
61
+ });
62
+ });
63
+ return {
64
+ bindings,
65
+ visibleServiceIdentifiers: new Set(bindings.map((binding)=>binding.localServiceIdentifier))
66
+ };
31
67
  }
32
- function build(module) {
33
- const builder = new ModuleBuilder(module);
34
- return builder.build();
68
+ function createAliasMap(aliases) {
69
+ return new Map(aliases.map((alias)=>[
70
+ alias.serviceIdentifier,
71
+ alias.as
72
+ ]));
35
73
  }
36
- class ModuleBuilder {
37
- module;
38
- serviceIdentifierMap = new Map();
39
- availableServiceIdentifiers = new Set();
40
- importAliasesCache = new Map();
41
- constructor(module){
42
- this.module = module;
74
+ function validateDeclarations(moduleName, declarations) {
75
+ if (!declarations || 0 === declarations.length) return;
76
+ const seen = new Set();
77
+ for (const decl of declarations){
78
+ const { serviceIdentifier } = decl;
79
+ if (seen.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_DECLARATION, `Duplicate declaration of service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${moduleName}".`);
80
+ seen.add(serviceIdentifier);
81
+ const hasValidOption = "useClass" in decl || "useFactory" in decl || "useValue" in decl || "useAlias" in decl;
82
+ if (!hasValidOption) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_INVALID_REGISTRATION, `Invalid registration options for service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${moduleName}": must specify useClass, useFactory, useValue, or useAlias.`);
43
83
  }
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
- }
84
+ }
85
+ function validateImportUniqueness(moduleName, imports) {
86
+ const seenModules = new Set();
87
+ for (const item of imports){
88
+ const importedModule = isModuleWithAliases(item) ? item.module : item;
89
+ const moduleId = importedModule.id;
90
+ if (seenModules.has(moduleId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_IMPORT_MODULE, `Duplicate import module: "${importedModule.displayName}" in "${moduleName}".`);
91
+ seenModules.add(moduleId);
80
92
  }
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
- }
93
+ }
94
+ function validateImportAliases(imports) {
95
+ for (const item of imports)if (isModuleWithAliases(item)) validateAliases(item.module.displayName, item.aliases ?? [], item.module.exports);
96
+ }
97
+ function detectCircularDependencies(module, visitedModules = new Set(), visitStack = []) {
98
+ if (visitStack.includes(module)) {
99
+ const cycle = [
100
+ ...visitStack.slice(visitStack.indexOf(module)),
101
+ module
102
+ ];
103
+ const cyclePath = cycle.map((m)=>m.displayName).join(" ");
104
+ throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_CIRCULAR_DEPENDENCY, `Circular dependency detected: ${cyclePath}`);
143
105
  }
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;
106
+ if (visitedModules.has(module.id)) return;
107
+ visitStack.push(module);
108
+ const imports = module.imports ?? [];
109
+ for (const item of imports){
110
+ const importedModule = isModuleWithAliases(item) ? item.module : item;
111
+ detectCircularDependencies(importedModule, visitedModules, visitStack);
158
112
  }
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
- }
113
+ visitedModules.add(module.id);
114
+ visitStack.pop();
115
+ }
116
+ function validateAliases(moduleName, aliases, exports) {
117
+ if (!aliases || 0 === aliases.length) return;
118
+ const exportedSet = new Set(exports ?? []);
119
+ const mappedServices = new Set();
120
+ for (const alias of aliases){
121
+ const { serviceIdentifier } = alias;
122
+ if (!exportedSet.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_ALIAS_SOURCE_NOT_EXPORTED, `Cannot alias service identifier "${getServiceIdentifierName(serviceIdentifier)}" from module "${moduleName}": it is not exported from that module.`);
123
+ if (mappedServices.has(serviceIdentifier)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_ALIAS_MAP, `Duplicate alias mapping for service identifier "${getServiceIdentifierName(serviceIdentifier)}" in module "${moduleName}".`);
124
+ mappedServices.add(serviceIdentifier);
166
125
  }
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
- }
126
+ }
127
+ function validateImportConflictsWithDeclarations(moduleName, importScope, declarations) {
128
+ if (!declarations) return;
129
+ const localDeclarations = new Set((declarations ?? []).map((decl)=>decl.serviceIdentifier));
130
+ const conflictingImport = importScope.bindings.find((binding)=>localDeclarations.has(binding.localServiceIdentifier));
131
+ if (conflictingImport) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_IMPORT_CONFLICT_LOCAL, `Imported service identifier "${getServiceIdentifierName(conflictingImport.localServiceIdentifier)}" conflicts with local declaration in module "${moduleName}". Use an alias to resolve the conflict.`);
132
+ }
133
+ function validateImportNamingConflicts(_moduleName, importScope) {
134
+ if (0 === importScope.bindings.length) return;
135
+ const serviceToModules = importScope.bindings.reduce((acc, binding)=>{
136
+ const modules = acc.get(binding.localServiceIdentifier) ?? [];
137
+ modules.push(binding.sourceModule);
138
+ acc.set(binding.localServiceIdentifier, modules);
139
+ return acc;
140
+ }, new Map());
141
+ const conflicts = Array.from(serviceToModules.entries()).filter(([, modules])=>modules.length > 1);
142
+ if (conflicts.length > 0) {
143
+ const [serviceId, modules] = conflicts[0];
144
+ const moduleNames = modules.map((m)=>`"${m.displayName}"`).join(", ");
145
+ throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_IMPORT_COLLISION, `Service identifier "${getServiceIdentifierName(serviceId)}" is exported by multiple imported modules: ${moduleNames}. Consider using aliases to resolve the conflict.`);
180
146
  }
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}".`;
147
+ }
148
+ function validateExportUniqueness(moduleName, exports) {
149
+ if (!exports || 0 === exports.length) return;
150
+ const seen = new Set();
151
+ for (const exportId of exports){
152
+ if (seen.has(exportId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_DUPLICATE_EXPORT, `Duplicate export of service identifier "${getServiceIdentifierName(exportId)}" in module "${moduleName}".`);
153
+ seen.add(exportId);
186
154
  }
187
155
  }
156
+ function collectAvailableServices(declarations, importScope) {
157
+ const localServices = (declarations ?? []).map((decl)=>decl.serviceIdentifier);
158
+ return new Set([
159
+ ...localServices,
160
+ ...importScope?.visibleServiceIdentifiers ?? []
161
+ ]);
162
+ }
163
+ function validateExportAvailability(moduleName, exports, declarations, importScope) {
164
+ if (!exports || 0 === exports.length) return;
165
+ const availableServices = collectAvailableServices(declarations, importScope);
166
+ for (const exportId of exports)if (!availableServices.has(exportId)) throw new ModuleException(module_error_code_enum_ModuleErrorCodeEnum.E_EXPORT_NOT_FOUND, `Cannot export service identifier "${getServiceIdentifierName(exportId)}" from "${moduleName}": it is not declared in this module or imported from any imported module.`);
167
+ }
188
168
  const createModuleId = incrementalIdFactory("MODULE");
189
- class Module {
169
+ class ModuleImpl {
190
170
  get id() {
191
171
  return this._id;
192
172
  }
@@ -203,7 +183,7 @@ class Module {
203
183
  return this._imports;
204
184
  }
205
185
  get displayName() {
206
- return `${String(this._name)}#${this._id}`;
186
+ return `${String(this._name)}/${this._id}`;
207
187
  }
208
188
  container;
209
189
  _id;
@@ -211,13 +191,17 @@ class Module {
211
191
  _declarations;
212
192
  _imports;
213
193
  _exports;
194
+ _importScope;
214
195
  constructor(options){
215
196
  this._id = createModuleId();
216
197
  this._name = options.name;
217
198
  this._declarations = options.declarations;
218
199
  this._imports = options.imports;
219
200
  this._exports = options.exports;
220
- this.container = build(this);
201
+ this._importScope = createImportScope(this._imports);
202
+ this.validateConfiguration();
203
+ this.container = this.buildContainer();
204
+ this.container.use(createExportedGuardMiddleware(this.exports ?? []));
221
205
  }
222
206
  resolve(serviceIdentifier, options) {
223
207
  return this.container.resolve(serviceIdentifier, options);
@@ -228,20 +212,59 @@ class Module {
228
212
  getServiceIdentifiers() {
229
213
  return this.container.getServiceIdentifiers();
230
214
  }
231
- use(middleware) {
232
- this.container.use(middleware);
215
+ use(...middleware) {
216
+ return this.container.use(...middleware);
233
217
  }
234
- unused(middleware) {
235
- this.container.unused(middleware);
218
+ unused(...middleware) {
219
+ this.container.unused(...middleware);
236
220
  }
237
221
  withAliases(aliases) {
222
+ validateAliases(this.displayName, aliases, this._exports);
238
223
  return {
239
224
  module: this,
240
225
  aliases
241
226
  };
242
227
  }
228
+ buildContainer() {
229
+ const container = createContainer(this._name);
230
+ this.registerDeclarations(container);
231
+ this.registerImports(container);
232
+ return container;
233
+ }
234
+ validateConfiguration() {
235
+ validateDeclarations(this.displayName, this._declarations);
236
+ this.validateImports();
237
+ this.validateExports();
238
+ }
239
+ validateImports() {
240
+ if (!this._imports || 0 === this._imports.length) return;
241
+ validateImportUniqueness(this.displayName, this._imports);
242
+ validateImportAliases(this._imports);
243
+ detectCircularDependencies(this);
244
+ validateImportConflictsWithDeclarations(this.displayName, this._importScope, this._declarations);
245
+ validateImportNamingConflicts(this.displayName, this._importScope);
246
+ }
247
+ registerDeclarations(container) {
248
+ if (!this._declarations || 0 === this._declarations.length) return;
249
+ for (const decl of this._declarations){
250
+ const { serviceIdentifier, ...options } = decl;
251
+ container.register(serviceIdentifier, options);
252
+ }
253
+ }
254
+ registerImports(container) {
255
+ if (0 === this._importScope.bindings.length) return;
256
+ for (const binding of this._importScope.bindings)container.register(binding.localServiceIdentifier, {
257
+ useAlias: binding.sourceServiceIdentifier,
258
+ getContainer: ()=>binding.sourceModule.container
259
+ });
260
+ }
261
+ validateExports() {
262
+ if (!this._exports || 0 === this._exports.length) return;
263
+ validateExportUniqueness(this.displayName, this._exports);
264
+ validateExportAvailability(this.displayName, this._exports, this._declarations, this._importScope);
265
+ }
243
266
  }
244
267
  function createModule(options) {
245
- return new Module(options);
268
+ return new ModuleImpl(options);
246
269
  }
247
- export { createModule };
270
+ export { ModuleException, createModule, module_error_code_enum_ModuleErrorCodeEnum as ModuleErrorCodeEnum };
@@ -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> | string;
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?: Array<IModule | ModuleWithAliases>;
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
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Import scope type definitions.
3
+ *
4
+ * @overview
5
+ * Defines the normalized import visibility model used by module validation and
6
+ * container assembly.
7
+ *
8
+ * @author AEPKILL
9
+ * @created 2026-05-09 00:00:00
10
+ */
11
+ import type { ServiceIdentifier } from "@husky-di/core";
12
+ import type { IModule } from "../interfaces/module.interface";
13
+ export type ImportBinding = {
14
+ readonly sourceModule: IModule;
15
+ readonly sourceServiceIdentifier: ServiceIdentifier<unknown>;
16
+ readonly localServiceIdentifier: ServiceIdentifier<unknown>;
17
+ readonly isAliased: boolean;
18
+ };
19
+ export type ImportScope = {
20
+ readonly bindings: ReadonlyArray<ImportBinding>;
21
+ readonly visibleServiceIdentifiers: ReadonlySet<ServiceIdentifier<unknown>>;
22
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @overview Module error code type.
3
+ * @author AEPKILL
4
+ * @created 2026-05-09 00:00:00
5
+ */
6
+ import type { ModuleErrorCodeEnum } from "../enums/module-error-code.enum";
7
+ export type ModuleErrorCode = ModuleErrorCodeEnum;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Module import utilities.
3
+ *
4
+ * @overview
5
+ * Provides helpers for working with module import descriptors.
6
+ *
7
+ * @author AEPKILL
8
+ * @created 2026-05-09 00:00:00
9
+ */
10
+ import type { IModule, ModuleWithAliases } from "../interfaces/module.interface";
11
+ export declare function isModuleWithAliases(item: IModule | ModuleWithAliases): item is ModuleWithAliases;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @overview Module validation utilities.
3
+ * @author AEPKILL
4
+ * @created 2025-03-30
5
+ */
6
+ import { type ServiceIdentifier } from "@husky-di/core";
7
+ import type { Alias, Declaration, IModule, ModuleWithAliases } from "../interfaces/module.interface";
8
+ import type { ImportScope } from "../types/import-scope.type";
9
+ export declare function validateDeclarations(moduleName: string, declarations?: Declaration<unknown>[]): void;
10
+ export declare function validateImportUniqueness(moduleName: string, imports: ReadonlyArray<IModule | ModuleWithAliases>): void;
11
+ export declare function validateImportAliases(imports: ReadonlyArray<IModule | ModuleWithAliases>): void;
12
+ export declare function detectCircularDependencies(module: IModule, visitedModules?: Set<string | symbol>, visitStack?: IModule[]): void;
13
+ export declare function validateAliases(moduleName: string, aliases: ReadonlyArray<Alias>, exports?: ReadonlyArray<ServiceIdentifier<unknown>>): void;
14
+ export declare function validateImportConflictsWithDeclarations(moduleName: string, importScope: ImportScope, declarations?: Declaration<unknown>[]): void;
15
+ export declare function validateImportNamingConflicts(_moduleName: string, importScope: ImportScope): void;
16
+ export declare function validateExportUniqueness(moduleName: string, exports?: ServiceIdentifier<unknown>[]): void;
17
+ export declare function collectAvailableServices(declarations?: Declaration<unknown>[], importScope?: ImportScope): Set<ServiceIdentifier<unknown>>;
18
+ export declare function validateExportAvailability(moduleName: string, exports?: ServiceIdentifier<unknown>[], declarations?: Declaration<unknown>[], importScope?: ImportScope): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@husky-di/module",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -15,13 +15,16 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@husky-di/core": "1.0.0"
18
+ "@husky-di/core": "1.1.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@rslib/core": "^0.11.2",
22
- "@types/node": "^22.17.0",
23
- "typescript": "^5.9.2",
24
- "vitest": "^3.2.4"
21
+ "@rslib/core": "^0.20.1",
22
+ "@types/node": "^25.5.0",
23
+ "typescript": "^6.0.3",
24
+ "vitest": "^4.1.2"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
25
28
  },
26
29
  "scripts": {
27
30
  "build": "rslib build",
@@ -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
- }