1- use anyhow:: Result ;
2- use mime:: APPLICATION_JSON ;
3- use turbo_tasks:: { primitives:: StringsVc , TryFlatMapRecursiveJoinIterExt , TryJoinIterExt } ;
1+ use anyhow:: { Context , Result } ;
2+ use indexmap:: IndexMap ;
3+ use mime:: { APPLICATION_JAVASCRIPT_UTF_8 , APPLICATION_JSON } ;
4+ use serde:: Serialize ;
5+ use turbo_tasks:: {
6+ primitives:: { StringVc , StringsVc } ,
7+ TryFlatMapRecursiveJoinIterExt , TryJoinIterExt ,
8+ } ;
49use turbo_tasks_fs:: File ;
510use turbopack_core:: asset:: AssetContentVc ;
611use turbopack_dev_server:: source:: {
@@ -11,15 +16,23 @@ use turbopack_node::render::{
1116 node_api_source:: NodeApiContentSourceVc , rendered_source:: NodeRenderContentSourceVc ,
1217} ;
1318
19+ use crate :: {
20+ embed_js:: next_js_file,
21+ next_config:: { NextConfigVc , RewritesReadRef } ,
22+ util:: get_asset_path_from_route,
23+ } ;
24+
1425/// A content source which creates the next.js `_devPagesManifest.json` and
1526/// `_devMiddlewareManifest.json` which are used for client side navigation.
1627#[ turbo_tasks:: value( shared) ]
1728pub struct DevManifestContentSource {
1829 pub page_roots : Vec < ContentSourceVc > ,
30+ pub next_config : NextConfigVc ,
1931}
2032
2133#[ turbo_tasks:: value_impl]
2234impl DevManifestContentSourceVc {
35+ /// Recursively find all routes in the `page_roots` content sources.
2336 #[ turbo_tasks:: function]
2437 async fn find_routes ( self ) -> Result < StringsVc > {
2538 let this = & * self . await ?;
@@ -61,10 +74,73 @@ impl DevManifestContentSourceVc {
6174 . flatten ( )
6275 . collect :: < Vec < _ > > ( ) ;
6376
64- routes. sort ( ) ;
77+ routes. sort_by_cached_key ( |s| s . split ( '/' ) . map ( PageSortKey :: from ) . collect :: < Vec < _ > > ( ) ) ;
6578
6679 Ok ( StringsVc :: cell ( routes) )
6780 }
81+
82+ /// Recursively find all pages in the `page_roots` content sources
83+ /// (excluding api routes).
84+ #[ turbo_tasks:: function]
85+ async fn find_pages ( self ) -> Result < StringsVc > {
86+ let routes = & * self . find_routes ( ) . await ?;
87+
88+ // we don't need to sort as it's already sorted by `find_routes`
89+ let pages = routes
90+ . iter ( )
91+ . filter ( |s| !s. starts_with ( "/api" ) )
92+ . cloned ( )
93+ . collect ( ) ;
94+
95+ Ok ( StringsVc :: cell ( pages) )
96+ }
97+
98+ /// Create a build manifest with all pages.
99+ #[ turbo_tasks:: function]
100+ async fn create_build_manifest ( self ) -> Result < StringVc > {
101+ let this = & * self . await ?;
102+
103+ let sorted_pages = & * self . find_pages ( ) . await ?;
104+ let routes = sorted_pages
105+ . iter ( )
106+ . map ( |p| {
107+ (
108+ p,
109+ vec ! [ format!(
110+ "_next/static/chunks/pages/{}" ,
111+ get_asset_path_from_route( p. split_at( 1 ) . 1 , ".js" )
112+ ) ] ,
113+ )
114+ } )
115+ . collect ( ) ;
116+
117+ let manifest = BuildManifest {
118+ rewrites : this. next_config . rewrites ( ) . await ?,
119+ sorted_pages,
120+ routes,
121+ } ;
122+
123+ let manifest = next_js_file ( "entry/manifest/buildManifest.js" )
124+ . await ?
125+ . as_content ( )
126+ . context ( "embedded buildManifest file missing" ) ?
127+ . content ( )
128+ . to_str ( ) ?
129+ . replace ( "$$MANIFEST$$" , & serde_json:: to_string ( & manifest) ?) ;
130+
131+ Ok ( StringVc :: cell ( manifest) )
132+ }
133+ }
134+
135+ #[ derive( Serialize ) ]
136+ #[ serde( rename_all = "camelCase" ) ]
137+ struct BuildManifest < ' a > {
138+ #[ serde( rename = "__rewrites" ) ]
139+ rewrites : RewritesReadRef ,
140+ sorted_pages : & ' a Vec < String > ,
141+
142+ #[ serde( flatten) ]
143+ routes : IndexMap < & ' a String , Vec < String > > ,
68144}
69145
70146#[ turbo_tasks:: value_impl]
@@ -75,25 +151,53 @@ impl ContentSource for DevManifestContentSource {
75151 path : & str ,
76152 _data : turbo_tasks:: Value < ContentSourceData > ,
77153 ) -> Result < ContentSourceResultVc > {
78- let manifest_content = match path {
154+ let manifest_file = match path {
79155 "_next/static/development/_devPagesManifest.json" => {
80156 let pages = & * self_vc. find_routes ( ) . await ?;
81157
82- serde_json:: to_string ( & serde_json:: json!( {
158+ File :: from ( serde_json:: to_string ( & serde_json:: json!( {
83159 "pages" : pages,
84- } ) ) ?
160+ } ) ) ?)
161+ . with_content_type ( APPLICATION_JSON )
162+ }
163+ "_next/static/development/_buildManifest.js" => {
164+ let build_manifest = & * self_vc. create_build_manifest ( ) . await ?;
165+
166+ File :: from ( build_manifest. as_str ( ) ) . with_content_type ( APPLICATION_JAVASCRIPT_UTF_8 )
85167 }
86168 "_next/static/development/_devMiddlewareManifest.json" => {
87169 // empty middleware manifest
88- "[]" . to_string ( )
170+ File :: from ( "[]" ) . with_content_type ( APPLICATION_JSON )
89171 }
90172 _ => return Ok ( ContentSourceResultVc :: not_found ( ) ) ,
91173 } ;
92174
93- let file = File :: from ( manifest_content) . with_content_type ( APPLICATION_JSON ) ;
94-
95175 Ok ( ContentSourceResultVc :: exact (
96- ContentSourceContentVc :: static_content ( AssetContentVc :: from ( file) . into ( ) ) . into ( ) ,
176+ ContentSourceContentVc :: static_content ( AssetContentVc :: from ( manifest_file) . into ( ) )
177+ . into ( ) ,
97178 ) )
98179 }
99180}
181+
182+ /// PageSortKey is necessary because the next.js client code looks for matches
183+ /// in the order the pages are sent in the manifest,if they're sorted
184+ /// alphabetically this means \[slug] and \[\[catchall]] routes are prioritized
185+ /// over fixed paths, so we have to override the ordering with this.
186+ #[ derive( Ord , PartialOrd , Eq , PartialEq ) ]
187+ enum PageSortKey {
188+ Static ( String ) ,
189+ Slug ,
190+ CatchAll ,
191+ }
192+
193+ impl From < & str > for PageSortKey {
194+ fn from ( value : & str ) -> Self {
195+ if value. starts_with ( "[[" ) && value. ends_with ( "]]" ) {
196+ PageSortKey :: CatchAll
197+ } else if value. starts_with ( '[' ) && value. ends_with ( ']' ) {
198+ PageSortKey :: Slug
199+ } else {
200+ PageSortKey :: Static ( value. to_string ( ) )
201+ }
202+ }
203+ }
0 commit comments