@@ -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,51 @@ 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+ if (
508+ this . guardMacroOptions &&
509+ Object . keys ( this . guardMacroOptions ) . length
510+ ) {
511+ const macro = this . extender . macro
512+
513+ for ( const [ key , value ] of Object . entries ( this . guardMacroOptions ) ) {
514+ if ( ! ( key in macro ) ) continue
515+
516+ const macroDef = macro [ key ]
517+ const macroHook =
518+ typeof macroDef === 'function' ? macroDef ( value ) : macroDef
519+
520+ if (
521+ ! macroHook ||
522+ ( typeof macroDef === 'object' && value === false )
523+ )
524+ continue
525+
526+ const introspectFn =
527+ typeof macroHook === 'object' && macroHook !== null && 'introspect' in macroHook
528+ ? ( macroHook as { introspect ?: ( option : Record < string , any > , context : MacroIntrospectionMetadata ) => unknown } ) . introspect
529+ : undefined
530+
531+ if ( typeof introspectFn === 'function' ) {
532+ const introspectOptions : Record < string , any > = {
533+ [ key ] : value
534+ }
535+
536+ introspectFn ( introspectOptions , { path, method } )
537+ }
538+ }
539+ }
540+
541+ // Skip macro introspection for routes added to child instances inside groups
542+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
543+ if ( ! this . skipMacroIntrospection ) {
544+ this . applyMacro ( localHook , localHook , { metadata : { path, method } } )
545+ }
491546
492547 let standaloneValidators = [ ] as InputSchema [ ]
493548
@@ -511,9 +566,6 @@ export default class Elysia<
511566 this . standaloneValidator . global
512567 )
513568
514- if ( path !== '' && path . charCodeAt ( 0 ) !== 47 ) path = '/' + path
515- if ( this . config . prefix && ! skipPrefix ) path = this . config . prefix + path
516-
517569 if ( localHook ?. type )
518570 switch ( localHook . type ) {
519571 case 'text' :
@@ -3976,11 +4028,21 @@ export default class Elysia<
39764028 scoped : [ ...( this . standaloneValidator . scoped ?? [ ] ) ] ,
39774029 global : [ ...( this . standaloneValidator . global ?? [ ] ) ]
39784030 }
4031+ // Mark this instance as being inside a group to skip macro introspection
4032+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
4033+ instance . skipMacroIntrospection = true
39794034
39804035 const isSchema = typeof schemaOrRun === 'object'
39814036 const sandbox = ( isSchema ? run ! : schemaOrRun ) ( instance )
39824037 this . singleton = mergeDeep ( this . singleton , instance . singleton ) as any
39834038 this . definitions = mergeDeep ( this . definitions , instance . definitions )
4039+ // Merge macros from the group instance so introspect can be called when routes are added to the parent
4040+ if ( isNotEmpty ( instance . extender . macro ) ) {
4041+ this . extender . macro = {
4042+ ...this . extender . macro ,
4043+ ...instance . extender . macro
4044+ }
4045+ }
39844046
39854047 if ( sandbox . event . request ?. length )
39864048 this . event . request = [
@@ -4472,6 +4534,30 @@ export default class Elysia<
44724534 ) : AnyElysia {
44734535 if ( ! run ) {
44744536 if ( typeof hook === 'object' ) {
4537+ // Capture guard-level macro options so we can introspect them per-route later
4538+ const macro = this . extender . macro
4539+ if ( macro && typeof macro === 'object' ) {
4540+ const guardOptions : Record < string , any > = { }
4541+
4542+ for ( const [ key , value ] of Object . entries ( hook ) ) {
4543+ if ( ! ( key in macro ) ) continue
4544+
4545+ const macroDef = macro [ key ]
4546+
4547+ // Match object-style macro semantics: value=false disables the macro entirely
4548+ if ( typeof macroDef === 'object' && value === false )
4549+ continue
4550+
4551+ guardOptions [ key ] = value
4552+ }
4553+
4554+ if ( Object . keys ( guardOptions ) . length )
4555+ this . guardMacroOptions = {
4556+ ...this . guardMacroOptions ,
4557+ ...guardOptions
4558+ }
4559+ }
4560+
44754561 this . applyMacro ( hook )
44764562
44774563 if ( hook . detail ) {
@@ -5390,8 +5476,13 @@ export default class Elysia<
53905476 appliable : AnyLocalHook = localHook ,
53915477 {
53925478 iteration = 0 ,
5393- applied = { }
5394- } : { iteration ?: number ; applied ?: { [ key : number ] : true } } = { }
5479+ applied = { } ,
5480+ metadata
5481+ } : {
5482+ iteration ?: number
5483+ applied ?: { [ key : number ] : true }
5484+ metadata ?: MacroIntrospectionMetadata
5485+ } = { }
53955486 ) {
53965487 if ( iteration >= 16 ) return
53975488 const macro = this . extender . macro
@@ -5429,7 +5520,17 @@ export default class Elysia<
54295520 }
54305521
54315522 if ( k === 'introspect' ) {
5432- value ?.( localHook )
5523+ // Only run introspect when route metadata is available.
5524+ // Guard-level macros (configured via `.guard({ macro: ... })`)
5525+ // are introspected per-route in `add`.
5526+ if ( metadata ) {
5527+ // Call introspect with only the options relevant to the current macro key
5528+ const introspectOptions : Record < string , any > = {
5529+ [ key ] : appliable [ key ]
5530+ }
5531+
5532+ value ?.( introspectOptions , metadata )
5533+ }
54335534
54345535 delete localHook [ key ]
54355536 continue
@@ -5449,7 +5550,7 @@ export default class Elysia<
54495550 this . applyMacro (
54505551 localHook ,
54515552 { [ k ] : value } ,
5452- { applied, iteration : iteration + 1 }
5553+ { applied, iteration : iteration + 1 , metadata }
54535554 )
54545555
54555556 delete localHook [ key ]
@@ -8214,6 +8315,7 @@ export type {
82148315 MapResponse ,
82158316 BaseMacro ,
82168317 MacroManager ,
8318+ MacroIntrospectionMetadata ,
82178319 MacroToProperty ,
82188320 MergeElysiaInstances ,
82198321 MaybeArray ,
0 commit comments