@@ -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,65 @@ 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' &&
531+ macroHook !== null &&
532+ 'introspect' in macroHook
533+ ? (
534+ macroHook as {
535+ introspect ?: (
536+ option : Record < string , any > ,
537+ context : MacroIntrospectionMetadata
538+ ) => unknown
539+ }
540+ ) . introspect
541+ : undefined
542+
543+ if ( typeof introspectFn === 'function' ) {
544+ const introspectOptions : Record < string , any > = {
545+ [ key ] : value
546+ }
547+
548+ introspectFn ( introspectOptions , { path, method } )
549+ }
550+ }
551+ }
552+
553+ // Skip macro introspection for routes added to child instances inside groups
554+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
555+ if ( ! this . skipMacroIntrospection ) {
556+ this . applyMacro ( localHook , localHook , {
557+ metadata : { path, method }
558+ } )
559+ }
491560
492561 let standaloneValidators = [ ] as InputSchema [ ]
493562
@@ -511,9 +580,6 @@ export default class Elysia<
511580 this . standaloneValidator . global
512581 )
513582
514- if ( path !== '' && path . charCodeAt ( 0 ) !== 47 ) path = '/' + path
515- if ( this . config . prefix && ! skipPrefix ) path = this . config . prefix + path
516-
517583 if ( localHook ?. type )
518584 switch ( localHook . type ) {
519585 case 'text' :
@@ -3976,11 +4042,21 @@ export default class Elysia<
39764042 scoped : [ ...( this . standaloneValidator . scoped ?? [ ] ) ] ,
39774043 global : [ ...( this . standaloneValidator . global ?? [ ] ) ]
39784044 }
4045+ // Mark this instance as being inside a group to skip macro introspection
4046+ // Macro introspection will happen when routes are added to the parent with the fully-resolved path
4047+ instance . skipMacroIntrospection = true
39794048
39804049 const isSchema = typeof schemaOrRun === 'object'
39814050 const sandbox = ( isSchema ? run ! : schemaOrRun ) ( instance )
39824051 this . singleton = mergeDeep ( this . singleton , instance . singleton ) as any
39834052 this . definitions = mergeDeep ( this . definitions , instance . definitions )
4053+ // Merge macros from the group instance so introspect can be called when routes are added to the parent
4054+ if ( isNotEmpty ( instance . extender . macro ) ) {
4055+ this . extender . macro = {
4056+ ...this . extender . macro ,
4057+ ...instance . extender . macro
4058+ }
4059+ }
39844060
39854061 if ( sandbox . event . request ?. length )
39864062 this . event . request = [
@@ -3996,6 +4072,20 @@ export default class Elysia<
39964072
39974073 this . model ( sandbox . definitions . type )
39984074
4075+ // Merge guardMacroOptions from group instance to parent during route merging
4076+ // so guard macro introspection happens with correct options when routes are added
4077+ // The parent's guardMacroOptions will be restored after merging is complete
4078+ const originalGuardMacroOptions = this . guardMacroOptions
4079+ if (
4080+ instance . guardMacroOptions &&
4081+ Object . keys ( instance . guardMacroOptions ) . length
4082+ ) {
4083+ this . guardMacroOptions = {
4084+ ...this . guardMacroOptions ,
4085+ ...instance . guardMacroOptions
4086+ }
4087+ }
4088+
39994089 Object . values ( instance . router . history ) . forEach (
40004090 ( { method, path, handler, hooks } ) => {
40014091 path =
@@ -4066,6 +4156,9 @@ export default class Elysia<
40664156 }
40674157 )
40684158
4159+ // Restore original guardMacroOptions
4160+ this . guardMacroOptions = originalGuardMacroOptions
4161+
40694162 return this as any
40704163 }
40714164
@@ -4472,6 +4565,38 @@ export default class Elysia<
44724565 ) : AnyElysia {
44734566 if ( ! run ) {
44744567 if ( typeof hook === 'object' ) {
4568+ // Capture guard-level macro options so we can introspect them per-route later
4569+ const macro = this . extender . macro
4570+ if ( macro && typeof macro === 'object' ) {
4571+ const updatedGuardMacroOptions = {
4572+ ...this . guardMacroOptions
4573+ }
4574+ let guardMacroOptionsChanged = false
4575+
4576+ for ( const [ key , value ] of Object . entries ( hook ) ) {
4577+ if ( ! ( key in macro ) ) continue
4578+
4579+ const macroDef = macro [ key ]
4580+
4581+ // Match object-style macro semantics: value=false disables the macro entirely
4582+ if ( typeof macroDef === 'object' && value === false ) {
4583+ if ( key in updatedGuardMacroOptions ) {
4584+ delete updatedGuardMacroOptions [ key ]
4585+ guardMacroOptionsChanged = true
4586+ }
4587+ continue
4588+ }
4589+
4590+ if ( updatedGuardMacroOptions [ key ] !== value ) {
4591+ updatedGuardMacroOptions [ key ] = value
4592+ guardMacroOptionsChanged = true
4593+ }
4594+ }
4595+
4596+ if ( guardMacroOptionsChanged )
4597+ this . guardMacroOptions = updatedGuardMacroOptions
4598+ }
4599+
44754600 this . applyMacro ( hook )
44764601
44774602 if ( hook . detail ) {
@@ -4558,6 +4683,41 @@ export default class Elysia<
45584683 instance . definitions = { ...this . definitions }
45594684 instance . inference = cloneInference ( this . inference )
45604685 instance . extender = { ...this . extender }
4686+ instance . guardMacroOptions = { ...this . guardMacroOptions }
4687+ // Apply guard hook's macro options to child instance so routes are introspected
4688+ // with the correct options when added (not when merged)
4689+ if ( typeof hook === 'object' ) {
4690+ const macro = this . extender . macro
4691+ if ( macro && typeof macro === 'object' ) {
4692+ const updatedGuardMacroOptions = {
4693+ ...instance . guardMacroOptions
4694+ }
4695+ let guardMacroOptionsChanged = false
4696+
4697+ for ( const [ key , value ] of Object . entries ( hook ) ) {
4698+ if ( ! ( key in macro ) ) continue
4699+
4700+ const macroDef = macro [ key ]
4701+
4702+ // Match object-style macro semantics: value=false disables the macro entirely
4703+ if ( typeof macroDef === 'object' && value === false ) {
4704+ if ( key in updatedGuardMacroOptions ) {
4705+ delete updatedGuardMacroOptions [ key ]
4706+ guardMacroOptionsChanged = true
4707+ }
4708+ continue
4709+ }
4710+
4711+ if ( updatedGuardMacroOptions [ key ] !== value ) {
4712+ updatedGuardMacroOptions [ key ] = value
4713+ guardMacroOptionsChanged = true
4714+ }
4715+ }
4716+
4717+ if ( guardMacroOptionsChanged )
4718+ instance . guardMacroOptions = updatedGuardMacroOptions
4719+ }
4720+ }
45614721 instance . getServer = ( ) => this . getServer ( )
45624722
45634723 const sandbox = run ( instance )
@@ -4581,6 +4741,15 @@ export default class Elysia<
45814741
45824742 this . model ( sandbox . definitions . type )
45834743
4744+ // Routes are already introspected for guard-level macros when added to child instances
4745+ // so we don't need to introspect guard-level macros again when merging routes back
4746+ // Temporarily skip macro introspection and set guardMacroOptions to empty
4747+ // Then manually apply macros without introspection to ensure macros are still applied
4748+ const originalGuardMacroOptions = this . guardMacroOptions
4749+ const originalSkipMacroIntrospection = this . skipMacroIntrospection
4750+ this . skipMacroIntrospection = true
4751+ this . guardMacroOptions = { }
4752+
45844753 Object . values ( instance . router . history ) . forEach (
45854754 ( { method, path, handler, hooks : localHook } ) => {
45864755 const {
@@ -4596,41 +4765,50 @@ export default class Elysia<
45964765 const hasStandaloneSchema =
45974766 body || headers || query || params || cookie || response
45984767
4768+ const mergedHook = mergeHook ( guardHook as AnyLocalHook , {
4769+ ...( ( localHook || { } ) as AnyLocalHook ) ,
4770+ error : ! localHook . error
4771+ ? sandbox . event . error
4772+ : Array . isArray ( localHook . error )
4773+ ? [
4774+ ...( localHook . error ?? [ ] ) ,
4775+ ...( sandbox . event . error ?? [ ] )
4776+ ]
4777+ : [
4778+ localHook . error ,
4779+ ...( sandbox . event . error ?? [ ] )
4780+ ] ,
4781+ standaloneValidator : ! hasStandaloneSchema
4782+ ? localHook . standaloneValidator
4783+ : [
4784+ ...( localHook . standaloneValidator ?? [ ] ) ,
4785+ {
4786+ body,
4787+ headers,
4788+ query,
4789+ params,
4790+ cookie,
4791+ response
4792+ }
4793+ ]
4794+ } )
4795+
4796+ // Apply macros without introspection (no metadata passed) before adding route
4797+ this . applyMacro ( mergedHook , mergedHook )
4798+
45994799 this . add (
46004800 method ,
46014801 path ,
46024802 handler ,
4603- mergeHook ( guardHook as AnyLocalHook , {
4604- ...( ( localHook || { } ) as AnyLocalHook ) ,
4605- error : ! localHook . error
4606- ? sandbox . event . error
4607- : Array . isArray ( localHook . error )
4608- ? [
4609- ...( localHook . error ?? [ ] ) ,
4610- ...( sandbox . event . error ?? [ ] )
4611- ]
4612- : [
4613- localHook . error ,
4614- ...( sandbox . event . error ?? [ ] )
4615- ] ,
4616- standaloneValidator : ! hasStandaloneSchema
4617- ? localHook . standaloneValidator
4618- : [
4619- ...( localHook . standaloneValidator ?? [ ] ) ,
4620- {
4621- body,
4622- headers,
4623- query,
4624- params,
4625- cookie,
4626- response
4627- }
4628- ]
4629- } )
4803+ mergedHook
46304804 )
46314805 }
46324806 )
46334807
4808+ // Restore original guardMacroOptions and skipMacroIntrospection
4809+ this . guardMacroOptions = originalGuardMacroOptions
4810+ this . skipMacroIntrospection = originalSkipMacroIntrospection
4811+
46344812 return this as any
46354813 }
46364814
@@ -5390,8 +5568,13 @@ export default class Elysia<
53905568 appliable : AnyLocalHook = localHook ,
53915569 {
53925570 iteration = 0 ,
5393- applied = { }
5394- } : { iteration ?: number ; applied ?: { [ key : number ] : true } } = { }
5571+ applied = { } ,
5572+ metadata
5573+ } : {
5574+ iteration ?: number
5575+ applied ?: { [ key : number ] : true }
5576+ metadata ?: MacroIntrospectionMetadata
5577+ } = { }
53955578 ) {
53965579 if ( iteration >= 16 ) return
53975580 const macro = this . extender . macro
@@ -5429,7 +5612,17 @@ export default class Elysia<
54295612 }
54305613
54315614 if ( k === 'introspect' ) {
5432- value ?.( localHook )
5615+ // Only run introspect when route metadata is available.
5616+ // Guard-level macros (configured via `.guard({ macro: ... })`)
5617+ // are introspected per-route in `add`.
5618+ if ( metadata ) {
5619+ // Call introspect with only the options relevant to the current macro key
5620+ const introspectOptions : Record < string , any > = {
5621+ [ key ] : appliable [ key ]
5622+ }
5623+
5624+ value ?.( introspectOptions , metadata )
5625+ }
54335626
54345627 delete localHook [ key ]
54355628 continue
@@ -5449,7 +5642,7 @@ export default class Elysia<
54495642 this . applyMacro (
54505643 localHook ,
54515644 { [ k ] : value } ,
5452- { applied, iteration : iteration + 1 }
5645+ { applied, iteration : iteration + 1 , metadata }
54535646 )
54545647
54555648 delete localHook [ key ]
@@ -8214,6 +8407,7 @@ export type {
82148407 MapResponse ,
82158408 BaseMacro ,
82168409 MacroManager ,
8410+ MacroIntrospectionMetadata ,
82178411 MacroToProperty ,
82188412 MergeElysiaInstances ,
82198413 MaybeArray ,
0 commit comments