@@ -117,6 +117,7 @@ import type {
117117 MapResponse ,
118118 Checksum ,
119119 MacroManager ,
120+ MacroIntrospectionMetadata ,
120121 MacroToProperty ,
121122 TransformHandler ,
122123 MetadataBase ,
@@ -281,6 +282,16 @@ export default class Elysia<
281282 }
282283 }
283284
285+ // Stores macro options applied via `.guard({ ...macros })`
286+ // so we can run macro `introspect` once per route with full metadata.
287+ // Is there a way to do this without a separate store?
288+ protected guardMacroOptions : Record < string , any > = { }
289+
290+ // Flag to skip macro introspection for routes added to child instances inside groups
291+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
292+ // Is there a way to do this without a flag?
293+ protected skipMacroIntrospection ?: boolean
294+
284295 protected standaloneValidator : StandaloneValidator = {
285296 global : null ,
286297 scoped : null ,
@@ -487,7 +498,54 @@ export default class Elysia<
487498
488499 localHook ??= { }
489500
490- this . applyMacro ( localHook )
501+ // Normalize path before macro introspection to ensure metadata has the correct path
502+ if ( path !== '' && path . charCodeAt ( 0 ) !== 47 ) path = '/' + path
503+ if ( this . config . prefix && ! skipPrefix ) path = this . config . prefix + path
504+
505+ // Run macro introspection for guard-level macros (configured via `.guard({ macro: ... })`)
506+ // once per route with the fully-resolved path.
507+ // Skip introspection for routes added to child instances inside groups
508+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
509+ if (
510+ ! this . skipMacroIntrospection &&
511+ this . guardMacroOptions &&
512+ Object . keys ( this . guardMacroOptions ) . length
513+ ) {
514+ const macro = this . extender . macro
515+
516+ for ( const [ key , value ] of Object . entries ( this . guardMacroOptions ) ) {
517+ if ( ! ( key in macro ) ) continue
518+
519+ const macroDef = macro [ key ]
520+ const macroHook =
521+ typeof macroDef === 'function' ? macroDef ( value ) : macroDef
522+
523+ if (
524+ ! macroHook ||
525+ ( typeof macroDef === 'object' && value === false )
526+ )
527+ continue
528+
529+ const introspectFn =
530+ typeof macroHook === 'object' && macroHook !== null && 'introspect' in macroHook
531+ ? ( macroHook as { introspect ?: ( option : Record < string , any > , context : MacroIntrospectionMetadata ) => unknown } ) . introspect
532+ : undefined
533+
534+ if ( typeof introspectFn === 'function' ) {
535+ const introspectOptions : Record < string , any > = {
536+ [ key ] : value
537+ }
538+
539+ introspectFn ( introspectOptions , { path, method } )
540+ }
541+ }
542+ }
543+
544+ // Skip macro introspection for routes added to child instances inside groups
545+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
546+ if ( ! this . skipMacroIntrospection ) {
547+ this . applyMacro ( localHook , localHook , { metadata : { path, method } } )
548+ }
491549
492550 let standaloneValidators = [ ] as InputSchema [ ]
493551
@@ -511,9 +569,6 @@ export default class Elysia<
511569 this . standaloneValidator . global
512570 )
513571
514- if ( path !== '' && path . charCodeAt ( 0 ) !== 47 ) path = '/' + path
515- if ( this . config . prefix && ! skipPrefix ) path = this . config . prefix + path
516-
517572 if ( localHook ?. type )
518573 switch ( localHook . type ) {
519574 case 'text' :
@@ -3976,11 +4031,21 @@ export default class Elysia<
39764031 scoped : [ ...( this . standaloneValidator . scoped ?? [ ] ) ] ,
39774032 global : [ ...( this . standaloneValidator . global ?? [ ] ) ]
39784033 }
4034+ // Mark this instance as being inside a group to skip macro introspection
4035+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
4036+ instance . skipMacroIntrospection = true
39794037
39804038 const isSchema = typeof schemaOrRun === 'object'
39814039 const sandbox = ( isSchema ? run ! : schemaOrRun ) ( instance )
39824040 this . singleton = mergeDeep ( this . singleton , instance . singleton ) as any
39834041 this . definitions = mergeDeep ( this . definitions , instance . definitions )
4042+ // Merge macros from the group instance so introspect can be called when routes are added to the parent
4043+ if ( isNotEmpty ( instance . extender . macro ) ) {
4044+ this . extender . macro = {
4045+ ...this . extender . macro ,
4046+ ...instance . extender . macro
4047+ }
4048+ }
39844049
39854050 if ( sandbox . event . request ?. length )
39864051 this . event . request = [
@@ -3996,6 +4061,20 @@ export default class Elysia<
39964061
39974062 this . model ( sandbox . definitions . type )
39984063
4064+ // Merge guardMacroOptions from group instance to parent during route merging
4065+ // so guard macro introspection happens with correct options when routes are added
4066+ // The parent's guardMacroOptions will be restored after merging is complete
4067+ const originalGuardMacroOptions = this . guardMacroOptions
4068+ if (
4069+ instance . guardMacroOptions &&
4070+ Object . keys ( instance . guardMacroOptions ) . length
4071+ ) {
4072+ this . guardMacroOptions = {
4073+ ...this . guardMacroOptions ,
4074+ ...instance . guardMacroOptions
4075+ }
4076+ }
4077+
39994078 Object . values ( instance . router . history ) . forEach (
40004079 ( { method, path, handler, hooks } ) => {
40014080 path =
@@ -4066,6 +4145,9 @@ export default class Elysia<
40664145 }
40674146 )
40684147
4148+ // Restore original guardMacroOptions
4149+ this . guardMacroOptions = originalGuardMacroOptions
4150+
40694151 return this as any
40704152 }
40714153
@@ -4472,6 +4554,30 @@ export default class Elysia<
44724554 ) : AnyElysia {
44734555 if ( ! run ) {
44744556 if ( typeof hook === 'object' ) {
4557+ // Capture guard-level macro options so we can introspect them per-route later
4558+ const macro = this . extender . macro
4559+ if ( macro && typeof macro === 'object' ) {
4560+ const guardOptions : Record < string , any > = { }
4561+
4562+ for ( const [ key , value ] of Object . entries ( hook ) ) {
4563+ if ( ! ( key in macro ) ) continue
4564+
4565+ const macroDef = macro [ key ]
4566+
4567+ // Match object-style macro semantics: value=false disables the macro entirely
4568+ if ( typeof macroDef === 'object' && value === false )
4569+ continue
4570+
4571+ guardOptions [ key ] = value
4572+ }
4573+
4574+ if ( Object . keys ( guardOptions ) . length )
4575+ this . guardMacroOptions = {
4576+ ...this . guardMacroOptions ,
4577+ ...guardOptions
4578+ }
4579+ }
4580+
44754581 this . applyMacro ( hook )
44764582
44774583 if ( hook . detail ) {
@@ -5390,8 +5496,13 @@ export default class Elysia<
53905496 appliable : AnyLocalHook = localHook ,
53915497 {
53925498 iteration = 0 ,
5393- applied = { }
5394- } : { iteration ?: number ; applied ?: { [ key : number ] : true } } = { }
5499+ applied = { } ,
5500+ metadata
5501+ } : {
5502+ iteration ?: number
5503+ applied ?: { [ key : number ] : true }
5504+ metadata ?: MacroIntrospectionMetadata
5505+ } = { }
53955506 ) {
53965507 if ( iteration >= 16 ) return
53975508 const macro = this . extender . macro
@@ -5429,7 +5540,17 @@ export default class Elysia<
54295540 }
54305541
54315542 if ( k === 'introspect' ) {
5432- value ?.( localHook )
5543+ // Only run introspect when route metadata is available.
5544+ // Guard-level macros (configured via `.guard({ macro: ... })`)
5545+ // are introspected per-route in `add`.
5546+ if ( metadata ) {
5547+ // Call introspect with only the options relevant to the current macro key
5548+ const introspectOptions : Record < string , any > = {
5549+ [ key ] : appliable [ key ]
5550+ }
5551+
5552+ value ?.( introspectOptions , metadata )
5553+ }
54335554
54345555 delete localHook [ key ]
54355556 continue
@@ -5449,7 +5570,7 @@ export default class Elysia<
54495570 this . applyMacro (
54505571 localHook ,
54515572 { [ k ] : value } ,
5452- { applied, iteration : iteration + 1 }
5573+ { applied, iteration : iteration + 1 , metadata }
54535574 )
54545575
54555576 delete localHook [ key ]
@@ -8214,6 +8335,7 @@ export type {
82148335 MapResponse ,
82158336 BaseMacro ,
82168337 MacroManager ,
8338+ MacroIntrospectionMetadata ,
82178339 MacroToProperty ,
82188340 MergeElysiaInstances ,
82198341 MaybeArray ,
0 commit comments