@@ -27,6 +27,9 @@ export class Moro extends EventEmitter {
2727 private lazyModules = new Map < string , ModuleConfig > ( ) ;
2828 private routeHandlers : Record < string , Function > = { } ;
2929 private moduleDiscovery ?: any ; // Store for cleanup
30+ private autoDiscoveryOptions : MoroOptions | null = null ;
31+ private autoDiscoveryInitialized = false ;
32+ private autoDiscoveryPromise : Promise < void > | null = null ;
3033 // Enterprise event system integration
3134 private eventBus : MoroEventBus ;
3235 // Application logger
@@ -118,15 +121,10 @@ export class Moro extends EventEmitter {
118121 ...options ,
119122 } ) ;
120123
121- // Auto-discover modules if enabled
122- if ( options . autoDiscover !== false ) {
123- // Initialize auto-discovery asynchronously
124- this . initializeAutoDiscovery ( options ) . catch ( error => {
125- this . logger . error ( 'Auto-discovery initialization failed' , 'Framework' , {
126- error : error instanceof Error ? error . message : String ( error ) ,
127- } ) ;
128- } ) ;
129- }
124+ // Store auto-discovery options for later initialization
125+ // IMPORTANT: Auto-discovery is deferred to ensure user middleware (like auth)
126+ // is registered before module middleware that might bypass it
127+ this . autoDiscoveryOptions = options . autoDiscover !== false ? options : null ;
130128
131129 // Emit initialization event through enterprise event bus
132130 this . eventBus . emit ( 'framework:initialized' , {
@@ -355,6 +353,13 @@ export class Moro extends EventEmitter {
355353 version : moduleOrPath . version || '1.0.0' ,
356354 } ) ;
357355 }
356+
357+ // IMPORTANT: If modules are loaded manually after auto-discovery,
358+ // ensure the final module handler is set up to maintain middleware order
359+ if ( this . autoDiscoveryInitialized ) {
360+ this . coreFramework . setupFinalModuleHandler ( ) ;
361+ }
362+
358363 return this ;
359364 }
360365
@@ -496,35 +501,162 @@ export class Moro extends EventEmitter {
496501 this . registerDirectRoutes ( ) ;
497502 }
498503
499- const actualCallback = ( ) => {
500- const displayHost = host || 'localhost' ;
501- this . logger . info ( 'Moro Server Started' , 'Server' ) ;
502- this . logger . info ( `Runtime: ${ this . runtimeType } ` , 'Server' ) ;
503- this . logger . info ( `HTTP API: http://${ displayHost } :${ port } ` , 'Server' ) ;
504- if ( this . config . websocket . enabled ) {
505- this . logger . info ( `WebSocket: ws://${ displayHost } :${ port } ` , 'Server' ) ;
506- }
507- this . logger . info ( 'Learn more at https://morojs.com' , 'Server' ) ;
504+ const startServer = ( ) => {
505+ const actualCallback = ( ) => {
506+ const displayHost = host || 'localhost' ;
507+ this . logger . info ( 'Moro Server Started' , 'Server' ) ;
508+ this . logger . info ( `Runtime: ${ this . runtimeType } ` , 'Server' ) ;
509+ this . logger . info ( `HTTP API: http://${ displayHost } :${ port } ` , 'Server' ) ;
510+ if ( this . config . websocket . enabled ) {
511+ this . logger . info ( `WebSocket: ws://${ displayHost } :${ port } ` , 'Server' ) ;
512+ }
513+ this . logger . info ( 'Learn more at https://morojs.com' , 'Server' ) ;
514+
515+ // Log intelligent routes info
516+ const intelligentRoutes = this . intelligentRouting . getIntelligentRoutes ( ) ;
517+ if ( intelligentRoutes . length > 0 ) {
518+ this . logger . info ( `Intelligent Routes: ${ intelligentRoutes . length } registered` , 'Server' ) ;
519+ }
520+
521+ this . eventBus . emit ( 'server:started' , { port, runtime : this . runtimeType } ) ;
522+ if ( callback ) callback ( ) ;
523+ } ;
508524
509- // Log intelligent routes info
510- const intelligentRoutes = this . intelligentRouting . getIntelligentRoutes ( ) ;
511- if ( intelligentRoutes . length > 0 ) {
512- this . logger . info ( `Intelligent Routes: ${ intelligentRoutes . length } registered` , 'Server' ) ;
525+ if ( host && typeof host === 'string' ) {
526+ this . coreFramework . listen ( port , host , actualCallback ) ;
527+ } else {
528+ this . coreFramework . listen ( port , actualCallback ) ;
513529 }
530+ } ;
514531
515- this . eventBus . emit ( 'server:started' , { port, runtime : this . runtimeType } ) ;
516- if ( callback ) callback ( ) ;
532+ // Ensure auto-discovery is complete before starting server
533+ this . ensureAutoDiscoveryComplete ( )
534+ . then ( ( ) => {
535+ startServer ( ) ;
536+ } )
537+ . catch ( error => {
538+ this . logger . error ( 'Auto-discovery initialization failed during server start' , 'Framework' , {
539+ error : error instanceof Error ? error . message : String ( error ) ,
540+ } ) ;
541+ // Start server anyway - auto-discovery failure shouldn't prevent startup
542+ startServer ( ) ;
543+ } ) ;
544+ }
545+
546+ // Public method to manually initialize auto-discovery
547+ // Useful for ensuring auth middleware is registered before auto-discovery
548+ async initializeAutoDiscoveryNow ( ) : Promise < void > {
549+ return this . ensureAutoDiscoveryComplete ( ) ;
550+ }
551+
552+ // Public API: Initialize modules explicitly after middleware setup
553+ // This provides users with explicit control over module loading timing
554+ // IMPORTANT: This forces module loading even if autoDiscovery.enabled is false
555+ // Usage: app.initModules() or app.initModules({ paths: ['./my-modules'] })
556+ initModules ( options ?: {
557+ paths ?: string [ ] ;
558+ patterns ?: string [ ] ;
559+ recursive ?: boolean ;
560+ loadingStrategy ?: 'eager' | 'lazy' | 'conditional' ;
561+ watchForChanges ?: boolean ;
562+ ignorePatterns ?: string [ ] ;
563+ loadOrder ?: 'alphabetical' | 'dependency' | 'custom' ;
564+ failOnError ?: boolean ;
565+ maxDepth ?: number ;
566+ } ) : void {
567+ this . logger . info ( 'User-requested module initialization' , 'ModuleSystem' ) ;
568+
569+ // If already initialized, do nothing
570+ if ( this . autoDiscoveryInitialized ) {
571+ this . logger . debug ( 'Auto-discovery already completed, skipping' , 'ModuleSystem' ) ;
572+ return ;
573+ }
574+
575+ // Store the options and mark that we want to force initialization
576+ this . autoDiscoveryOptions = {
577+ autoDiscover : {
578+ enabled : true , // Force enabled regardless of original config
579+ paths : options ?. paths || [ './modules' , './src/modules' ] ,
580+ patterns : options ?. patterns || [
581+ '**/*.module.{ts,js}' ,
582+ '**/index.{ts,js}' ,
583+ '**/*.config.{ts,js}' ,
584+ ] ,
585+ recursive : options ?. recursive ?? true ,
586+ loadingStrategy : options ?. loadingStrategy || ( 'eager' as const ) ,
587+ watchForChanges : options ?. watchForChanges ?? false ,
588+ ignorePatterns : options ?. ignorePatterns || [
589+ '**/*.test.{ts,js}' ,
590+ '**/*.spec.{ts,js}' ,
591+ '**/node_modules/**' ,
592+ ] ,
593+ loadOrder : options ?. loadOrder || ( 'dependency' as const ) ,
594+ failOnError : options ?. failOnError ?? false ,
595+ maxDepth : options ?. maxDepth ?? 5 ,
596+ } ,
517597 } ;
518598
519- if ( host && typeof host === 'string' ) {
520- this . coreFramework . listen ( port , host , actualCallback ) ;
521- } else {
522- this . coreFramework . listen ( port , actualCallback ) ;
599+ this . logger . debug (
600+ 'Module initialization options stored, will execute on next listen/getHandler call' ,
601+ 'ModuleSystem'
602+ ) ;
603+ }
604+
605+ // Robust method to ensure auto-discovery is complete, handling race conditions
606+ private async ensureAutoDiscoveryComplete ( ) : Promise < void > {
607+ // If already initialized, nothing to do
608+ if ( this . autoDiscoveryInitialized ) {
609+ return ;
610+ }
611+
612+ // If auto-discovery is disabled, mark as initialized
613+ if ( ! this . autoDiscoveryOptions ) {
614+ this . autoDiscoveryInitialized = true ;
615+ return ;
616+ }
617+
618+ // If already in progress, wait for it to complete
619+ if ( this . autoDiscoveryPromise ) {
620+ return this . autoDiscoveryPromise ;
621+ }
622+
623+ // Start auto-discovery
624+ this . autoDiscoveryPromise = this . performAutoDiscovery ( ) ;
625+
626+ try {
627+ await this . autoDiscoveryPromise ;
628+ this . autoDiscoveryInitialized = true ;
629+ } catch ( error ) {
630+ // Reset promise on error so it can be retried
631+ this . autoDiscoveryPromise = null ;
632+ throw error ;
633+ } finally {
634+ this . autoDiscoveryOptions = null ; // Clear after attempt
523635 }
524636 }
525637
638+ // Perform the actual auto-discovery work
639+ private async performAutoDiscovery ( optionsOverride ?: MoroOptions ) : Promise < void > {
640+ const optionsToUse = optionsOverride || this . autoDiscoveryOptions ;
641+ if ( ! optionsToUse ) return ;
642+
643+ this . logger . debug ( 'Starting auto-discovery initialization' , 'AutoDiscovery' ) ;
644+
645+ await this . initializeAutoDiscovery ( optionsToUse ) ;
646+
647+ this . logger . debug ( 'Auto-discovery initialization completed' , 'AutoDiscovery' ) ;
648+ }
649+
526650 // Get handler for non-Node.js runtimes
527651 getHandler ( ) {
652+ // Ensure auto-discovery is complete for non-Node.js runtimes
653+ // This handles the case where users call getHandler() immediately after createApp()
654+ this . ensureAutoDiscoveryComplete ( ) . catch ( error => {
655+ this . logger . error ( 'Auto-discovery initialization failed for runtime handler' , 'Framework' , {
656+ error : error instanceof Error ? error . message : String ( error ) ,
657+ } ) ;
658+ } ) ;
659+
528660 // Create a unified request handler that works with the runtime adapter
529661 const handler = async ( req : HttpRequest , res : HttpResponse ) => {
530662 // Add documentation middleware first (if enabled)
@@ -902,6 +1034,9 @@ export class Moro extends EventEmitter {
9021034 // Load modules based on strategy
9031035 await this . loadDiscoveredModules ( modules , autoDiscoveryConfig ) ;
9041036
1037+ // Setup final module handler to run after user middleware (like auth)
1038+ this . coreFramework . setupFinalModuleHandler ( ) ;
1039+
9051040 // Setup file watching if enabled
9061041 if ( autoDiscoveryConfig . watchForChanges ) {
9071042 this . moduleDiscovery . watchModulesAdvanced (
0 commit comments