@@ -32,6 +32,7 @@ export type InjectionToken<T = unknown> = string | symbol | Constructor<T>
3232 * {
3333 * provide: 'MyService',
3434 * useClass: MyServiceImpl,
35+ * deps: [ConfigService], // Optional: for weight calculation
3536 * onInit: async (instance) => await instance.initialize()
3637 * }
3738 */
@@ -40,6 +41,9 @@ export interface ClassProvider<T = unknown> {
4041 provide : InjectionToken < T >
4142 /** Class constructor to instantiate */
4243 useClass : Constructor < T >
44+ /** Optional dependencies (affects resolution order/weight) */
45+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
46+ deps ?: ( InjectionToken | Constructor < any > ) [ ]
4347 /** Optional lifecycle hook called after instantiation */
4448 onInit ?: ( instance : T ) => Promise < void > | void
4549}
@@ -421,6 +425,119 @@ export function getInjectableMetadata(
421425 return Reflect . getMetadata ( 'injectable:options' , target )
422426}
423427
428+ // ============================================================================
429+ // Group Decorator (for grouping providers)
430+ // ============================================================================
431+
432+ /**
433+ * Options for @Group decorator
434+ * Used to group related providers together
435+ */
436+ export interface GroupOptions {
437+ /**
438+ * Providers that belong to this group
439+ * Can include classes, providers, or other groups
440+ */
441+ providers ?: Provider [ ]
442+
443+ /**
444+ * Dependencies for weight calculation
445+ * These affect resolution order even if not directly used
446+ */
447+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
448+ deps ?: ( InjectionToken | Constructor < any > ) [ ]
449+
450+ /**
451+ * Additional options (for future extensibility)
452+ */
453+ [ key : string ] : unknown
454+ }
455+
456+ /**
457+ * Group class decorator
458+ *
459+ * Marks a class as a provider group. Groups allow you to organize related
460+ * providers together and can be used in deps arrays or bootstrap.
461+ * Groups are flattened during resolution.
462+ *
463+ * @param options - Configuration for the group
464+ * @returns A class decorator
465+ *
466+ * @example
467+ * // Create a group of auth-related services
468+ * @Group({
469+ * providers: [AuthService, TokenService, UserService]
470+ * })
471+ * class AuthModule {}
472+ *
473+ * @example
474+ * // Use group in deps
475+ * container.register({
476+ * provide: AppService,
477+ * useFactory: () => new AppService(),
478+ * deps: [AuthModule, ConfigService] // AuthModule gets flattened
479+ * })
480+ *
481+ * @example
482+ * // Bootstrap with groups
483+ * await container.bootstrap([
484+ * ConfigModule,
485+ * AuthModule,
486+ * AppService
487+ * ])
488+ */
489+ export function Group ( options : GroupOptions = { } ) : ClassDecorator {
490+ return ( target : object ) => {
491+ // Store group metadata
492+ const metadata = {
493+ providers : [ ] ,
494+ deps : [ ] ,
495+ ...options ,
496+ }
497+ Reflect . defineMetadata ( 'group:options' , metadata , target )
498+ }
499+ }
500+
501+ /**
502+ * Get group metadata from a class
503+ *
504+ * Retrieves the metadata stored by the @Group() decorator.
505+ *
506+ * @param target - The class to get metadata from
507+ * @returns The group options or undefined if not decorated with @Group
508+ *
509+ * @example
510+ * @Group({ providers: [ServiceA, ServiceB] })
511+ * class MyModule {}
512+ *
513+ * const metadata = getGroupMetadata(MyModule)
514+ * console.log(metadata?.providers) // [ServiceA, ServiceB]
515+ */
516+ export function getGroupMetadata (
517+ // biome-ignore lint/suspicious/noExplicitAny: Constructor can be of any type
518+ target : Constructor < any > ,
519+ ) : GroupOptions | undefined {
520+ return Reflect . getMetadata ( 'group:options' , target )
521+ }
522+
523+ /**
524+ * Check if a constructor is decorated with @Group
525+ *
526+ * @param target - The constructor to check
527+ * @returns True if the constructor has @Group decorator
528+ *
529+ * @example
530+ * if (isGroup(MyModule)) {
531+ * console.log('MyModule is a group')
532+ * }
533+ */
534+ // biome-ignore lint/suspicious/noExplicitAny: Constructor can be of any type
535+ export function isGroup ( target : any ) : target is Constructor < any > {
536+ return (
537+ typeof target === 'function' && Reflect . hasMetadata ( 'group:options' , target )
538+ )
539+ }
540+
424541// ============================================================================
425542// Container with Injection Tokens
426543// ============================================================================
@@ -934,11 +1051,15 @@ export class Container {
9341051
9351052 // Get dependencies based on provider type
9361053 if ( this . isFactoryProvider ( provider ) ) {
937- // Factory provider: use explicit deps
938- deps = provider . deps || [ ]
1054+ // Factory provider: use explicit deps (flatten groups)
1055+ deps = provider . deps ? this . flattenDeps ( provider . deps ) : [ ]
9391056 } else if ( this . isClassProvider ( provider ) ) {
940- // Class provider: get constructor dependencies
941- deps = this . getClassDependencies ( provider . useClass )
1057+ // Class provider: merge explicit deps with constructor dependencies
1058+ const explicitDeps = provider . deps ? this . flattenDeps ( provider . deps ) : [ ]
1059+ const constructorDeps = this . getClassDependencies ( provider . useClass )
1060+ // Combine both, using Set to avoid duplicates
1061+ const allDeps = [ ...new Set ( [ ...explicitDeps , ...constructorDeps ] ) ]
1062+ deps = allDeps
9421063 } else {
9431064 // Plain constructor
9441065 // biome-ignore lint/suspicious/noExplicitAny: Provider is a constructor of any type
@@ -1128,6 +1249,7 @@ export class Container {
11281249 *
11291250 * This is a convenient way to register and resolve multiple providers at once.
11301251 * All providers are registered first, then resolved in optimal order.
1252+ * Groups are automatically flattened during registration.
11311253 *
11321254 * @param providersOrConfig - Array of providers or config object with providers
11331255 * @returns The container instance (for chaining)
@@ -1145,6 +1267,14 @@ export class Container {
11451267 * await container.bootstrap({
11461268 * providers: [UserService, DatabaseService, AuthService]
11471269 * })
1270+ *
1271+ * @example
1272+ * // With groups
1273+ * await container.bootstrap([
1274+ * ConfigModule, // Group gets flattened
1275+ * AuthModule, // Group gets flattened
1276+ * AppService
1277+ * ])
11481278 */
11491279 public async bootstrap (
11501280 providersOrConfig : Provider [ ] | { providers : Provider [ ] } ,
@@ -1156,8 +1286,15 @@ export class Container {
11561286 ? providersOrConfig
11571287 : providersOrConfig . providers
11581288
1289+ // Flatten groups in the providers array
1290+ console . log ( 'Flattening groups...' )
1291+ const flattenedProviders = this . flattenProviders ( providers )
1292+ console . log (
1293+ `Flattened ${ providers . length } items into ${ flattenedProviders . length } providers\n` ,
1294+ )
1295+
11591296 // Register all providers
1160- for ( const provider of providers ) {
1297+ for ( const provider of flattenedProviders ) {
11611298 this . register ( provider )
11621299 }
11631300
@@ -1218,6 +1355,106 @@ export class Container {
12181355 // Type Guards and Helper Methods
12191356 // ============================================================================
12201357
1358+ /**
1359+ * Flatten groups in a providers array
1360+ *
1361+ * Recursively expands any groups found in the providers array.
1362+ * Groups are expanded to their constituent providers.
1363+ *
1364+ * @private
1365+ * @param providers - Array of providers that may contain groups
1366+ * @returns Flattened array of providers with groups expanded
1367+ */
1368+ private flattenProviders ( providers : Provider [ ] ) : Provider [ ] {
1369+ const result : Provider [ ] = [ ]
1370+ const visited = new Set < Constructor < unknown > > ( )
1371+
1372+ const flatten = ( items : Provider [ ] ) => {
1373+ for ( const item of items ) {
1374+ // Check if it's a group (plain constructor with @Group decorator)
1375+ if ( typeof item === 'function' && isGroup ( item ) ) {
1376+ // Prevent infinite recursion
1377+ if ( visited . has ( item ) ) {
1378+ continue
1379+ }
1380+ visited . add ( item )
1381+
1382+ const groupMeta = getGroupMetadata ( item )
1383+ if ( groupMeta ?. providers && groupMeta . providers . length > 0 ) {
1384+ // Recursively flatten nested groups
1385+ flatten ( groupMeta . providers )
1386+ }
1387+ } else {
1388+ // Regular provider, add it
1389+ result . push ( item )
1390+ }
1391+ }
1392+ }
1393+
1394+ flatten ( providers )
1395+ return result
1396+ }
1397+
1398+ /**
1399+ * Flatten groups in a deps array
1400+ *
1401+ * Expands any groups found in the deps array to their constituent providers.
1402+ * Also includes the group's own deps for weight calculation.
1403+ *
1404+ * @private
1405+ * @param deps - Array of dependencies that may contain groups
1406+ * @returns Flattened array of dependencies with groups expanded
1407+ */
1408+ private flattenDeps (
1409+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
1410+ deps : ( InjectionToken | Constructor < any > ) [ ] ,
1411+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
1412+ ) : ( InjectionToken | Constructor < any > ) [ ] {
1413+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
1414+ const result : ( InjectionToken | Constructor < any > ) [ ] = [ ]
1415+ const visited = new Set < Constructor < unknown > > ( )
1416+
1417+ // biome-ignore lint/suspicious/noExplicitAny: Dependencies can be of any type
1418+ const flatten = ( items : ( InjectionToken | Constructor < any > ) [ ] ) => {
1419+ for ( const item of items ) {
1420+ // Check if it's a group
1421+ if ( typeof item === 'function' && isGroup ( item ) ) {
1422+ // Prevent infinite recursion
1423+ if ( visited . has ( item ) ) {
1424+ continue
1425+ }
1426+ visited . add ( item )
1427+
1428+ const groupMeta = getGroupMetadata ( item )
1429+ if ( groupMeta ) {
1430+ // Add the group's deps first (for weight calculation)
1431+ if ( groupMeta . deps && groupMeta . deps . length > 0 ) {
1432+ flatten ( groupMeta . deps )
1433+ }
1434+
1435+ // Then flatten the group's providers
1436+ if ( groupMeta . providers && groupMeta . providers . length > 0 ) {
1437+ const flattenedProviders = this . flattenProviders (
1438+ groupMeta . providers ,
1439+ )
1440+ // Extract tokens from providers
1441+ for ( const provider of flattenedProviders ) {
1442+ const token = this . getProviderKey ( provider )
1443+ result . push ( token )
1444+ }
1445+ }
1446+ }
1447+ } else {
1448+ // Regular dependency, add it
1449+ result . push ( item )
1450+ }
1451+ }
1452+ }
1453+
1454+ flatten ( deps )
1455+ return result
1456+ }
1457+
12211458 /**
12221459 * Check if a provider is a class provider
12231460 *
0 commit comments