1- use std:: collections:: BTreeSet ;
1+ use std:: collections:: { BTreeSet , VecDeque } ;
22
33use anyhow:: { Result , bail} ;
44use serde_json:: json;
55use turbo_rcstr:: RcStr ;
66use turbo_tasks:: { ResolvedVc , Vc } ;
7- use turbo_tasks_fs:: { File , FileSystem , FileSystemPath } ;
7+ use turbo_tasks_fs:: { DirectoryEntry , File , FileSystem , FileSystemPath , glob :: Glob } ;
88use turbopack_core:: {
99 asset:: { Asset , AssetContent } ,
1010 output:: OutputAsset ,
@@ -30,20 +30,23 @@ pub struct NftJsonAsset {
3030 /// An example of this is the two-phase approach used by the `ClientReferenceManifest` in
3131 /// next.js.
3232 additional_assets : Vec < ResolvedVc < Box < dyn OutputAsset > > > ,
33+ page_name : Option < RcStr > ,
3334}
3435
3536#[ turbo_tasks:: value_impl]
3637impl NftJsonAsset {
3738 #[ turbo_tasks:: function]
3839 pub fn new (
3940 project : ResolvedVc < Project > ,
41+ page_name : Option < RcStr > ,
4042 chunk : ResolvedVc < Box < dyn OutputAsset > > ,
4143 additional_assets : Vec < ResolvedVc < Box < dyn OutputAsset > > > ,
4244 ) -> Vc < Self > {
4345 NftJsonAsset {
4446 chunk,
4547 project,
4648 additional_assets,
49+ page_name,
4750 }
4851 . cell ( )
4952 }
@@ -95,6 +98,41 @@ fn get_output_specifier(
9598 bail ! ( "NftJsonAsset: cannot handle filepath {}" , path_ref) ;
9699}
97100
101+ /// Apply outputFileTracingIncludes patterns to find additional files
102+ async fn apply_includes (
103+ project_root_path : Vc < FileSystemPath > ,
104+ glob : Vc < Glob > ,
105+ ident_folder : & FileSystemPath ,
106+ ) -> Result < BTreeSet < RcStr > > {
107+ // Read files matching the glob pattern from the project root
108+ let glob_result = project_root_path. read_glob ( glob) . await ?;
109+
110+ // Walk the full glob_result using an explicit stack to avoid async recursion overheads.
111+ let mut result = BTreeSet :: new ( ) ;
112+ let mut stack = VecDeque :: new ( ) ;
113+ stack. push_back ( glob_result) ;
114+ while let Some ( glob_result) = stack. pop_back ( ) {
115+ // Process direct results (files and directories at this level)
116+ for entry in glob_result. results . values ( ) {
117+ let DirectoryEntry :: File ( file_path) = entry else {
118+ continue ;
119+ } ;
120+
121+ let file_path_ref = file_path. await ?;
122+ // Convert to relative path from ident_folder to the file
123+ if let Some ( relative_path) = ident_folder. get_relative_path_to ( & file_path_ref) {
124+ result. insert ( relative_path) ;
125+ }
126+ }
127+
128+ for nested_result in glob_result. inner . values ( ) {
129+ let nested_result_ref = nested_result. await ?;
130+ stack. push_back ( nested_result_ref) ;
131+ }
132+ }
133+ Ok ( result)
134+ }
135+
98136#[ turbo_tasks:: value_impl]
99137impl Asset for NftJsonAsset {
100138 #[ turbo_tasks:: function]
@@ -104,15 +142,20 @@ impl Asset for NftJsonAsset {
104142
105143 let output_root_ref = this. project . output_fs ( ) . root ( ) . await ?;
106144 let project_root_ref = this. project . project_fs ( ) . root ( ) . await ?;
145+ let next_config = this. project . next_config ( ) ;
146+
147+ // Parse outputFileTracingIncludes and outputFileTracingExcludes from config
148+ let output_file_tracing_includes = & * next_config. output_file_tracing_includes ( ) . await ?;
149+ let output_file_tracing_excludes = & * next_config. output_file_tracing_excludes ( ) . await ?;
150+
107151 let client_root = this. project . client_fs ( ) . root ( ) ;
108152 let client_root_ref = client_root. await ?;
153+ let project_root_path = this. project . project_root_path ( ) ; // Example: [project]
109154
110155 // Example: [output]/apps/my-website/.next/server/app -- without the `.nft.json`
111156 let ident_folder = self . path ( ) . parent ( ) . await ?;
112157 // Example: [project]/apps/my-website/.next/server/app -- without the `.nft.json`
113- let ident_folder_in_project_fs = this
114- . project
115- . project_root_path ( ) // Example: [project]
158+ let ident_folder_in_project_fs = project_root_path
116159 . join ( ident_folder. path . clone ( ) ) // apps/my-website/.next/server/app
117160 . await ?;
118161
@@ -123,6 +166,51 @@ impl Asset for NftJsonAsset {
123166 . copied ( )
124167 . chain ( std:: iter:: once ( chunk) )
125168 . collect ( ) ;
169+
170+ let exclude_glob = if let Some ( route) = & this. page_name {
171+ let project_path = this. project . project_path ( ) . await ?;
172+
173+ if let Some ( excludes_config) = output_file_tracing_excludes {
174+ let mut combined_excludes = BTreeSet :: new ( ) ;
175+
176+ if let Some ( excludes_obj) = excludes_config. as_object ( ) {
177+ for ( glob_pattern, exclude_patterns) in excludes_obj {
178+ // Check if the route matches the glob pattern
179+ let glob = Glob :: new ( RcStr :: from ( glob_pattern. clone ( ) ) ) . await ?;
180+ if glob. matches ( route)
181+ && let Some ( patterns) = exclude_patterns. as_array ( )
182+ {
183+ for pattern in patterns {
184+ if let Some ( pattern_str) = pattern. as_str ( ) {
185+ combined_excludes. insert ( pattern_str) ;
186+ }
187+ }
188+ }
189+ }
190+ }
191+
192+ let glob = Glob :: new (
193+ format ! (
194+ "{project_path}/{{{}}}" ,
195+ combined_excludes
196+ . iter( )
197+ . copied( )
198+ . collect:: <Vec <_>>( )
199+ . join( "," )
200+ )
201+ . into ( ) ,
202+ )
203+ . await ?;
204+
205+ Some ( glob)
206+ } else {
207+ None
208+ }
209+ } else {
210+ None
211+ } ;
212+
213+ // Collect base assets first
126214 for referenced_chunk in all_assets_from_entries ( Vc :: cell ( entries) ) . await ? {
127215 if chunk. eq ( referenced_chunk) {
128216 continue ;
@@ -133,6 +221,12 @@ impl Asset for NftJsonAsset {
133221 continue ;
134222 }
135223
224+ if let Some ( ref exclude_glob) = exclude_glob
225+ && exclude_glob. matches ( referenced_chunk_path. path . as_str ( ) )
226+ {
227+ continue ;
228+ }
229+
136230 let Some ( specifier) = get_output_specifier (
137231 & referenced_chunk_path,
138232 & ident_folder,
@@ -147,6 +241,50 @@ impl Asset for NftJsonAsset {
147241 result. insert ( specifier) ;
148242 }
149243
244+ // Apply outputFileTracingIncludes and outputFileTracingExcludes
245+ // Extract route from chunk path for pattern matching
246+ if let Some ( route) = & this. page_name {
247+ let project_path = this. project . project_path ( ) ;
248+ let mut combined_includes = BTreeSet :: new ( ) ;
249+
250+ // Process includes
251+ if let Some ( includes_config) = output_file_tracing_includes
252+ && let Some ( includes_obj) = includes_config. as_object ( )
253+ {
254+ for ( glob_pattern, include_patterns) in includes_obj {
255+ // Check if the route matches the glob pattern
256+ let glob = Glob :: new ( glob_pattern. as_str ( ) . into ( ) ) . await ?;
257+ if glob. matches ( route)
258+ && let Some ( patterns) = include_patterns. as_array ( )
259+ {
260+ for pattern in patterns {
261+ if let Some ( pattern_str) = pattern. as_str ( ) {
262+ combined_includes. insert ( pattern_str) ;
263+ }
264+ }
265+ }
266+ }
267+ }
268+
269+ // Apply includes - find additional files that match the include patterns
270+ if !combined_includes. is_empty ( ) {
271+ let glob = Glob :: new (
272+ format ! (
273+ "{{{}}}" ,
274+ combined_includes
275+ . iter( )
276+ . copied( )
277+ . collect:: <Vec <_>>( )
278+ . join( "," )
279+ )
280+ . into ( ) ,
281+ ) ;
282+ let additional_files =
283+ apply_includes ( project_path, glob, & ident_folder_in_project_fs) . await ?;
284+ result. extend ( additional_files) ;
285+ }
286+ }
287+
150288 let json = json ! ( {
151289 "version" : 1 ,
152290 "files" : result
0 commit comments