@@ -23,6 +23,7 @@ import type { ParsedUrlQuery } from 'querystring'
23
23
import type { StaticMetadata } from './types/icons'
24
24
import type { WorkStore } from '../../server/app-render/work-async-storage.external'
25
25
import type { Params } from '../../server/request/params'
26
+ import type { SearchParams } from '../../server/request/search-params'
26
27
27
28
// eslint-disable-next-line import/no-extraneous-dependencies
28
29
import 'server-only'
@@ -58,15 +59,31 @@ import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
58
59
import * as Log from '../../build/output/log'
59
60
import { createServerParamsForMetadata } from '../../server/request/params'
60
61
import type { MetadataBaseURL } from './resolvers/resolve-url'
62
+ import {
63
+ getUseCacheFunctionInfo ,
64
+ isUseCacheFunction ,
65
+ } from '../client-and-server-references'
66
+ import type {
67
+ UseCacheLayoutProps ,
68
+ UseCachePageProps ,
69
+ } from '../../server/use-cache/use-cache-wrapper'
70
+ import { createLazyResult } from '../../server/lib/lazy-result'
61
71
62
72
type StaticIcons = Pick < ResolvedIcons , 'icon' | 'apple' >
63
73
64
- type MetadataResolver = (
65
- parent : ResolvingMetadata
66
- ) => Metadata | Promise < Metadata >
67
- type ViewportResolver = (
68
- parent : ResolvingViewport
69
- ) => Viewport | Promise < Viewport >
74
+ type Resolved < T > = T extends Metadata ? ResolvedMetadata : ResolvedViewport
75
+
76
+ type InstrumentedResolver < TData > = ( (
77
+ parent : Promise < Resolved < TData > >
78
+ ) => TData | Promise < TData > ) & {
79
+ $$original : (
80
+ props : unknown ,
81
+ parent : Promise < Resolved < TData > >
82
+ ) => TData | Promise < TData >
83
+ }
84
+
85
+ type MetadataResolver = InstrumentedResolver < Metadata >
86
+ type ViewportResolver = InstrumentedResolver < Viewport >
70
87
71
88
export type MetadataErrorType = 'not-found' | 'forbidden' | 'unauthorized'
72
89
@@ -87,13 +104,17 @@ type BuildState = {
87
104
}
88
105
89
106
type LayoutProps = {
90
- params : { [ key : string ] : any }
107
+ params : Promise < Params >
91
108
}
109
+
92
110
type PageProps = {
93
- params : { [ key : string ] : any }
94
- searchParams : { [ key : string ] : any }
111
+ params : Promise < Params >
112
+ searchParams : Promise < SearchParams >
95
113
}
96
114
115
+ type SegmentProps = LayoutProps | PageProps
116
+ type UseCacheSegmentProps = UseCacheLayoutProps | UseCachePageProps
117
+
97
118
function isFavicon ( icon : IconDescriptor | undefined ) : boolean {
98
119
if ( ! icon ) {
99
120
return false
@@ -461,51 +482,77 @@ function mergeViewport({
461
482
462
483
function getDefinedViewport (
463
484
mod : any ,
464
- props : any ,
485
+ props : SegmentProps ,
465
486
tracingProps : { route : string }
466
487
) : Viewport | ViewportResolver | null {
467
488
if ( typeof mod . generateViewport === 'function' ) {
468
489
const { route } = tracingProps
469
- return ( parent : ResolvingViewport ) =>
470
- getTracer ( ) . trace (
471
- ResolveMetadataSpan . generateViewport ,
472
- {
473
- spanName : `generateViewport ${ route } ` ,
474
- attributes : {
475
- 'next.page' : route ,
490
+ const segmentProps = createSegmentProps ( mod . generateViewport , props )
491
+
492
+ return Object . assign (
493
+ ( parent : ResolvingViewport ) =>
494
+ getTracer ( ) . trace (
495
+ ResolveMetadataSpan . generateViewport ,
496
+ {
497
+ spanName : `generateViewport ${ route } ` ,
498
+ attributes : {
499
+ 'next.page' : route ,
500
+ } ,
476
501
} ,
477
- } ,
478
- ( ) => mod . generateViewport ( props , parent )
479
- )
502
+ ( ) => mod . generateViewport ( segmentProps , parent )
503
+ ) ,
504
+ { $$original : mod . generateViewport }
505
+ )
480
506
}
481
507
return mod . viewport || null
482
508
}
483
509
484
510
function getDefinedMetadata (
485
511
mod : any ,
486
- props : any ,
512
+ props : SegmentProps ,
487
513
tracingProps : { route : string }
488
514
) : Metadata | MetadataResolver | null {
489
515
if ( typeof mod . generateMetadata === 'function' ) {
490
516
const { route } = tracingProps
491
- return ( parent : ResolvingMetadata ) =>
492
- getTracer ( ) . trace (
493
- ResolveMetadataSpan . generateMetadata ,
494
- {
495
- spanName : `generateMetadata ${ route } ` ,
496
- attributes : {
497
- 'next.page' : route ,
517
+ const segmentProps = createSegmentProps ( mod . generateMetadata , props )
518
+
519
+ return Object . assign (
520
+ ( parent : ResolvingMetadata ) =>
521
+ getTracer ( ) . trace (
522
+ ResolveMetadataSpan . generateMetadata ,
523
+ {
524
+ spanName : `generateMetadata ${ route } ` ,
525
+ attributes : {
526
+ 'next.page' : route ,
527
+ } ,
498
528
} ,
499
- } ,
500
- ( ) => mod . generateMetadata ( props , parent )
501
- )
529
+ ( ) => mod . generateMetadata ( segmentProps , parent )
530
+ ) ,
531
+ { $$original : mod . generateMetadata }
532
+ )
502
533
}
503
534
return mod . metadata || null
504
535
}
505
536
537
+ /**
538
+ * If `fn` is a `'use cache'` function, we add special markers to the props,
539
+ * that the cache wrapper reads and removes, before passing the props to the
540
+ * user function.
541
+ */
542
+ function createSegmentProps (
543
+ fn : Function ,
544
+ props : SegmentProps
545
+ ) : SegmentProps | UseCacheSegmentProps {
546
+ return isUseCacheFunction ( fn )
547
+ ? 'searchParams' in props
548
+ ? { ...props , $$isPage : true }
549
+ : { ...props , $$isLayout : true }
550
+ : props
551
+ }
552
+
506
553
async function collectStaticImagesFiles (
507
554
metadata : AppDirModules [ 'metadata' ] ,
508
- props : any ,
555
+ props : SegmentProps ,
509
556
type : keyof NonNullable < AppDirModules [ 'metadata' ] >
510
557
) {
511
558
if ( ! metadata ?. [ type ] ) return undefined
@@ -522,7 +569,7 @@ async function collectStaticImagesFiles(
522
569
523
570
async function resolveStaticMetadata (
524
571
modules : AppDirModules ,
525
- props : any
572
+ props : SegmentProps
526
573
) : Promise < StaticMetadata > {
527
574
const { metadata } = modules
528
575
if ( ! metadata ) return null
@@ -557,7 +604,7 @@ async function collectMetadata({
557
604
tree : LoaderTree
558
605
metadataItems : MetadataItems
559
606
errorMetadataItem : MetadataItems [ number ]
560
- props : any
607
+ props : SegmentProps
561
608
route : string
562
609
errorConvention ?: MetadataErrorType
563
610
} ) {
@@ -608,7 +655,7 @@ async function collectViewport({
608
655
tree : LoaderTree
609
656
viewportItems : ViewportItems
610
657
errorViewportItemRef : ErrorViewportItemRef
611
- props : any
658
+ props : SegmentProps
612
659
route : string
613
660
errorConvention ?: MetadataErrorType
614
661
} ) {
@@ -700,25 +747,14 @@ async function resolveMetadataItemsImpl(
700
747
}
701
748
702
749
const params = createServerParamsForMetadata ( currentParams , workStore )
703
-
704
- let layerProps : LayoutProps | PageProps
705
- if ( isPage ) {
706
- layerProps = {
707
- params,
708
- searchParams,
709
- }
710
- } else {
711
- layerProps = {
712
- params,
713
- }
714
- }
750
+ const props : SegmentProps = isPage ? { params, searchParams } : { params }
715
751
716
752
await collectMetadata ( {
717
753
tree,
718
754
metadataItems,
719
755
errorMetadataItem,
720
756
errorConvention,
721
- props : layerProps ,
757
+ props,
722
758
route : currentTreePrefix
723
759
// __PAGE__ shouldn't be shown in a route
724
760
. filter ( ( s ) => s !== PAGE_SEGMENT_KEY )
@@ -963,7 +999,7 @@ function prerenderMetadata(metadataItems: MetadataItems) {
963
999
> = [ ]
964
1000
for ( let i = 0 ; i < metadataItems . length ; i ++ ) {
965
1001
const metadataExport = metadataItems [ i ] [ 0 ]
966
- getResult ( resolversAndResults , metadataExport )
1002
+ getResult < Metadata > ( resolversAndResults , metadataExport )
967
1003
}
968
1004
return resolversAndResults
969
1005
}
@@ -977,32 +1013,66 @@ function prerenderViewport(viewportItems: ViewportItems) {
977
1013
> = [ ]
978
1014
for ( let i = 0 ; i < viewportItems . length ; i ++ ) {
979
1015
const viewportExport = viewportItems [ i ]
980
- getResult ( resolversAndResults , viewportExport )
1016
+ getResult < Viewport > ( resolversAndResults , viewportExport )
981
1017
}
982
1018
return resolversAndResults
983
1019
}
984
1020
985
- type Resolved < T > = T extends Metadata ? ResolvedMetadata : ResolvedViewport
1021
+ const noop = ( ) => { }
986
1022
987
- function getResult < T extends Metadata | Viewport > (
988
- resolversAndResults : Array < ( ( value : Resolved < T > ) => void ) | Result < T > > ,
989
- exportForResult : null | T | ( ( parent : Promise < Resolved < T > > ) => Result < T > )
1023
+ function getResult < TData extends object > (
1024
+ resolversAndResults : Array <
1025
+ ( ( value : Resolved < TData > ) => void ) | Result < TData >
1026
+ > ,
1027
+ exportForResult : null | TData | InstrumentedResolver < TData >
990
1028
) {
991
1029
if ( typeof exportForResult === 'function' ) {
992
- const result = exportForResult (
993
- new Promise < Resolved < T > > ( ( resolve ) => resolversAndResults . push ( resolve ) )
1030
+ // If the function is a 'use cache' function that uses the parent data as
1031
+ // the second argument, we don't want to eagerly execute it during
1032
+ // metadata/viewport pre-rendering, as the parent data might also be
1033
+ // computed from another 'use cache' function. To ensure that the hanging
1034
+ // input abort signal handling works in this case (i.e. the depending
1035
+ // function waits for the cached input to resolve while encoding its args),
1036
+ // they must be called sequentially. This can be accomplished by wrapping
1037
+ // the call in a lazy promise, so that the original function is only called
1038
+ // when the result is actually awaited.
1039
+ const useCacheFunctionInfo = getUseCacheFunctionInfo (
1040
+ exportForResult . $$original
994
1041
)
995
- resolversAndResults . push ( result )
996
- if ( result instanceof Promise ) {
997
- // since we eager execute generateMetadata and
998
- // they can reject at anytime we need to ensure
999
- // we attach the catch handler right away to
1000
- // prevent unhandled rejections crashing the process
1001
- result . catch ( ( err ) => {
1002
- return {
1003
- __nextError : err ,
1004
- }
1005
- } )
1042
+ if ( useCacheFunctionInfo && useCacheFunctionInfo . usedArgs [ 1 ] ) {
1043
+ const promise = new Promise < Resolved < TData > > ( ( resolve ) =>
1044
+ resolversAndResults . push ( resolve )
1045
+ )
1046
+ resolversAndResults . push (
1047
+ createLazyResult ( async ( ) => exportForResult ( promise ) )
1048
+ )
1049
+ } else {
1050
+ let result : TData | Promise < TData >
1051
+ if ( useCacheFunctionInfo ) {
1052
+ resolversAndResults . push ( noop )
1053
+ // @ts -expect-error We intentionally omit the parent argument, because
1054
+ // we know from the check above that the 'use cache' function does not
1055
+ // use it.
1056
+ result = exportForResult ( )
1057
+ } else {
1058
+ result = exportForResult (
1059
+ new Promise < Resolved < TData > > ( ( resolve ) =>
1060
+ resolversAndResults . push ( resolve )
1061
+ )
1062
+ )
1063
+ }
1064
+ resolversAndResults . push ( result )
1065
+ if ( result instanceof Promise ) {
1066
+ // since we eager execute generateMetadata and
1067
+ // they can reject at anytime we need to ensure
1068
+ // we attach the catch handler right away to
1069
+ // prevent unhandled rejections crashing the process
1070
+ result . catch ( ( err ) => {
1071
+ return {
1072
+ __nextError : err ,
1073
+ }
1074
+ } )
1075
+ }
1006
1076
}
1007
1077
} else if ( typeof exportForResult === 'object' ) {
1008
1078
resolversAndResults . push ( exportForResult )
0 commit comments