@husky-di/module 1.0.1 → 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
+ }
@@ -30,5 +30,9 @@ import { type ResolveMiddleware, type ServiceIdentifier } from "@husky-di/core";
30
30
  *
31
31
  * Note: Services not registered in the container are not considered external access,
32
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.
33
37
  */
34
- export declare function createExportedGuardMiddlewareFactory(exports: ReadonlyArray<ServiceIdentifier<unknown>>): ResolveMiddleware<any, any>;
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;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @overview Module implementation.
3
+ * @author AEPKILL
4
+ * @created 2025-08-09 14:51:09
5
+ */
6
+ import { type Cleanup, type IContainer, type IsRegisteredOptions, type ResolveInstance, type ResolveMiddleware, type ResolveOptions, type ServiceIdentifier } from "@husky-di/core";
7
+ import type { Alias, CreateModuleOptions, Declaration, IModule, ModuleWithAliases } from "../interfaces/module.interface";
8
+ export declare class ModuleImpl implements IModule {
9
+ get id(): string;
10
+ get name(): string;
11
+ get declarations(): Declaration<unknown>[] | undefined;
12
+ get exports(): ServiceIdentifier<unknown>[] | undefined;
13
+ get imports(): (IModule | ModuleWithAliases)[] | undefined;
14
+ get displayName(): string;
15
+ readonly container: IContainer;
16
+ private readonly _id;
17
+ private readonly _name;
18
+ private readonly _declarations?;
19
+ private readonly _imports?;
20
+ private readonly _exports?;
21
+ private readonly _importScope;
22
+ constructor(options: CreateModuleOptions);
23
+ resolve<T, O extends ResolveOptions<T>>(serviceIdentifier: ServiceIdentifier<T>, options?: O): ResolveInstance<T, O>;
24
+ isRegistered<T>(serviceIdentifier: ServiceIdentifier<T>, options?: IsRegisteredOptions): boolean;
25
+ getServiceIdentifiers(): ServiceIdentifier<unknown>[];
26
+ use(...middleware: ResolveMiddleware<any, any>[]): Cleanup;
27
+ unused(...middleware: ResolveMiddleware<any, any>[]): void;
28
+ withAliases(aliases: Alias[]): ModuleWithAliases;
29
+ private buildContainer;
30
+ private validateConfiguration;
31
+ private validateImports;
32
+ private registerDeclarations;
33
+ private registerImports;
34
+ private validateExports;
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,8 +72,131 @@ function findPreviousContainer(paths) {
51
72
  lastContainer = path.value.container;
52
73
  }
53
74
  }
75
+ function isModuleWithAliases(item) {
76
+ return "module" in item;
77
+ }
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
+ };
97
+ }
98
+ function createAliasMap(aliases) {
99
+ return new Map(aliases.map((alias)=>[
100
+ alias.serviceIdentifier,
101
+ alias.as
102
+ ]));
103
+ }
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.`);
113
+ }
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);
122
+ }
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}`);
135
+ }
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);
142
+ }
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);
155
+ }
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.`);
176
+ }
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);
184
+ }
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
+ }
54
198
  const createModuleId = (0, core_namespaceObject.incrementalIdFactory)("MODULE");
55
- class Module {
199
+ class ModuleImpl {
56
200
  get id() {
57
201
  return this._id;
58
202
  }
@@ -69,7 +213,7 @@ class Module {
69
213
  return this._imports;
70
214
  }
71
215
  get displayName() {
72
- return `${String(this._name)}#${this._id}`;
216
+ return `${String(this._name)}/${this._id}`;
73
217
  }
74
218
  container;
75
219
  _id;
@@ -77,19 +221,17 @@ class Module {
77
221
  _declarations;
78
222
  _imports;
79
223
  _exports;
80
- _visitedModules = new Set();
81
- _visitStack = [];
224
+ _importScope;
82
225
  constructor(options){
83
226
  this._id = createModuleId();
84
227
  this._name = options.name;
85
228
  this._declarations = options.declarations;
86
229
  this._imports = options.imports;
87
230
  this._exports = options.exports;
88
- this.validateDeclarations();
89
- this.validateImports();
90
- this.validateExports();
231
+ this._importScope = createImportScope(this._imports);
232
+ this.validateConfiguration();
91
233
  this.container = this.buildContainer();
92
- this.container.use(createExportedGuardMiddlewareFactory(this.exports ?? []));
234
+ this.container.use(createExportedGuardMiddleware(this.exports ?? []));
93
235
  }
94
236
  resolve(serviceIdentifier, options) {
95
237
  return this.container.resolve(serviceIdentifier, options);
@@ -100,14 +242,14 @@ class Module {
100
242
  getServiceIdentifiers() {
101
243
  return this.container.getServiceIdentifiers();
102
244
  }
103
- use(middleware) {
104
- this.container.use(middleware);
245
+ use(...middleware) {
246
+ return this.container.use(...middleware);
105
247
  }
106
- unused(middleware) {
107
- this.container.unused(middleware);
248
+ unused(...middleware) {
249
+ this.container.unused(...middleware);
108
250
  }
109
251
  withAliases(aliases) {
110
- this.validateAliases(aliases);
252
+ validateAliases(this.displayName, aliases, this._exports);
111
253
  return {
112
254
  module: this,
113
255
  aliases
@@ -119,45 +261,18 @@ class Module {
119
261
  this.registerImports(container);
120
262
  return container;
121
263
  }
264
+ validateConfiguration() {
265
+ validateDeclarations(this.displayName, this._declarations);
266
+ this.validateImports();
267
+ this.validateExports();
268
+ }
122
269
  validateImports() {
123
270
  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();
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);
161
276
  }
162
277
  registerDeclarations(container) {
163
278
  if (!this._declarations || 0 === this._declarations.length) return;
@@ -167,118 +282,29 @@ class Module {
167
282
  }
168
283
  }
169
284
  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
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
175
289
  });
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
290
  }
210
291
  validateExports() {
211
292
  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;
293
+ validateExportUniqueness(this.displayName, this._exports);
294
+ validateExportAvailability(this.displayName, this._exports, this._declarations, this._importScope);
273
295
  }
274
296
  }
275
297
  function createModule(options) {
276
- return new Module(options);
298
+ return new ModuleImpl(options);
277
299
  }
300
+ exports.ModuleErrorCodeEnum = __webpack_exports__.ModuleErrorCodeEnum;
301
+ exports.ModuleException = __webpack_exports__.ModuleException;
278
302
  exports.createModule = __webpack_exports__.createModule;
279
- for(var __webpack_i__ in __webpack_exports__)if (-1 === [
303
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
304
+ "ModuleErrorCodeEnum",
305
+ "ModuleException",
280
306
  "createModule"
281
- ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
307
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
282
308
  Object.defineProperty(exports, '__esModule', {
283
309
  value: true
284
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,8 +42,131 @@ function findPreviousContainer(paths) {
23
42
  lastContainer = path.value.container;
24
43
  }
25
44
  }
45
+ function isModuleWithAliases(item) {
46
+ return "module" in item;
47
+ }
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
+ };
67
+ }
68
+ function createAliasMap(aliases) {
69
+ return new Map(aliases.map((alias)=>[
70
+ alias.serviceIdentifier,
71
+ alias.as
72
+ ]));
73
+ }
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.`);
83
+ }
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);
92
+ }
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}`);
105
+ }
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);
112
+ }
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);
125
+ }
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.`);
146
+ }
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);
154
+ }
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
+ }
26
168
  const createModuleId = incrementalIdFactory("MODULE");
27
- class Module {
169
+ class ModuleImpl {
28
170
  get id() {
29
171
  return this._id;
30
172
  }
@@ -41,7 +183,7 @@ class Module {
41
183
  return this._imports;
42
184
  }
43
185
  get displayName() {
44
- return `${String(this._name)}#${this._id}`;
186
+ return `${String(this._name)}/${this._id}`;
45
187
  }
46
188
  container;
47
189
  _id;
@@ -49,19 +191,17 @@ class Module {
49
191
  _declarations;
50
192
  _imports;
51
193
  _exports;
52
- _visitedModules = new Set();
53
- _visitStack = [];
194
+ _importScope;
54
195
  constructor(options){
55
196
  this._id = createModuleId();
56
197
  this._name = options.name;
57
198
  this._declarations = options.declarations;
58
199
  this._imports = options.imports;
59
200
  this._exports = options.exports;
60
- this.validateDeclarations();
61
- this.validateImports();
62
- this.validateExports();
201
+ this._importScope = createImportScope(this._imports);
202
+ this.validateConfiguration();
63
203
  this.container = this.buildContainer();
64
- this.container.use(createExportedGuardMiddlewareFactory(this.exports ?? []));
204
+ this.container.use(createExportedGuardMiddleware(this.exports ?? []));
65
205
  }
66
206
  resolve(serviceIdentifier, options) {
67
207
  return this.container.resolve(serviceIdentifier, options);
@@ -72,14 +212,14 @@ class Module {
72
212
  getServiceIdentifiers() {
73
213
  return this.container.getServiceIdentifiers();
74
214
  }
75
- use(middleware) {
76
- this.container.use(middleware);
215
+ use(...middleware) {
216
+ return this.container.use(...middleware);
77
217
  }
78
- unused(middleware) {
79
- this.container.unused(middleware);
218
+ unused(...middleware) {
219
+ this.container.unused(...middleware);
80
220
  }
81
221
  withAliases(aliases) {
82
- this.validateAliases(aliases);
222
+ validateAliases(this.displayName, aliases, this._exports);
83
223
  return {
84
224
  module: this,
85
225
  aliases
@@ -91,45 +231,18 @@ class Module {
91
231
  this.registerImports(container);
92
232
  return container;
93
233
  }
234
+ validateConfiguration() {
235
+ validateDeclarations(this.displayName, this._declarations);
236
+ this.validateImports();
237
+ this.validateExports();
238
+ }
94
239
  validateImports() {
95
240
  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();
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);
133
246
  }
134
247
  registerDeclarations(container) {
135
248
  if (!this._declarations || 0 === this._declarations.length) return;
@@ -139,112 +252,19 @@ class Module {
139
252
  }
140
253
  }
141
254
  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
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
151
259
  });
152
260
  }
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
261
  validateExports() {
183
262
  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;
263
+ validateExportUniqueness(this.displayName, this._exports);
264
+ validateExportAvailability(this.displayName, this._exports, this._declarations, this._importScope);
245
265
  }
246
266
  }
247
267
  function createModule(options) {
248
- return new Module(options);
268
+ return new ModuleImpl(options);
249
269
  }
250
- export { createModule };
270
+ export { ModuleException, createModule, module_error_code_enum_ModuleErrorCodeEnum as ModuleErrorCodeEnum };
@@ -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.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -15,13 +15,13 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@husky-di/core": "1.0.1"
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
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"
@@ -1,194 +0,0 @@
1
- /**
2
- * @overview
3
- * @author AEPKILL
4
- * @created 2025-08-09 14:51:09
5
- */
6
- import { type IContainer, type IsRegisteredOptions, type ResolveInstance, type ResolveMiddleware, type ResolveOptions, type ServiceIdentifier } from "@husky-di/core";
7
- import type { Alias, CreateModuleOptions, Declaration, IModule, ModuleWithAliases } from "../interfaces/module.interface";
8
- export declare class Module implements IModule {
9
- get id(): string;
10
- get name(): string;
11
- get declarations(): Declaration<unknown>[] | undefined;
12
- get exports(): ServiceIdentifier<unknown>[] | undefined;
13
- get imports(): (IModule | ModuleWithAliases)[] | undefined;
14
- get displayName(): string;
15
- readonly container: IContainer;
16
- private _id;
17
- private _name;
18
- private _declarations?;
19
- private _imports?;
20
- private _exports?;
21
- private readonly _visitedModules;
22
- private readonly _visitStack;
23
- constructor(options: CreateModuleOptions);
24
- resolve<T, O extends ResolveOptions<T>>(serviceIdentifier: ServiceIdentifier<T>, options?: O): ResolveInstance<T, O>;
25
- isRegistered<T>(serviceIdentifier: ServiceIdentifier<T>, options?: IsRegisteredOptions): boolean;
26
- getServiceIdentifiers(): ServiceIdentifier<unknown>[];
27
- use(middleware: ResolveMiddleware<any, any>): void;
28
- unused(middleware: ResolveMiddleware<any, any>): void;
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;
194
- }