@@ -28,22 +28,80 @@ use turbopack_binding::{
28
28
} ,
29
29
} ;
30
30
31
+ /// Scans the RSC entry point's full module graph looking for exported Server
32
+ /// Actions (identifiable by a magic comment in the transformed module's
33
+ /// output), and constructs a evaluatable "action loader" entry point and
34
+ /// manifest describing the found actions.
35
+ ///
36
+ /// If Server Actions are not enabled, this returns an empty manifest and a None
37
+ /// loader.
38
+ pub ( crate ) async fn create_server_actions_manifest (
39
+ entry : Vc < Box < dyn EcmascriptChunkPlaceable > > ,
40
+ node_root : Vc < FileSystemPath > ,
41
+ app_page_name : & str ,
42
+ runtime : NextRuntime ,
43
+ asset_context : Vc < Box < dyn AssetContext > > ,
44
+ chunking_context : Vc < Box < dyn EcmascriptChunkingContext > > ,
45
+ enable_server_actions : Vc < bool > ,
46
+ ) -> Result < (
47
+ Option < Vc < Box < dyn EvaluatableAsset > > > ,
48
+ Vc < Box < dyn OutputAsset > > ,
49
+ ) > {
50
+ // If actions aren't enabled, then there's no need to scan the module graph. We
51
+ // still need to generate an empty manifest so that the TS side can merge
52
+ // the manifest later on.
53
+ if !* enable_server_actions. await ? {
54
+ let manifest = build_manifest (
55
+ node_root,
56
+ app_page_name,
57
+ runtime,
58
+ ModuleActionMap :: empty ( ) ,
59
+ Vc :: < String > :: empty ( ) ,
60
+ )
61
+ . await ?;
62
+ return Ok ( ( None , manifest) ) ;
63
+ }
64
+
65
+ let actions = get_actions ( Vc :: upcast ( entry) ) ;
66
+ let loader =
67
+ build_server_actions_loader ( node_root, app_page_name, actions, asset_context) . await ?;
68
+ let Some ( evaluable) = Vc :: try_resolve_sidecast :: < Box < dyn EvaluatableAsset > > ( loader) . await ?
69
+ else {
70
+ bail ! ( "loader module must be evaluatable" ) ;
71
+ } ;
72
+
73
+ let loader_id = loader. as_chunk_item ( chunking_context) . id ( ) . to_string ( ) ;
74
+ let manifest = build_manifest ( node_root, app_page_name, runtime, actions, loader_id) . await ?;
75
+ Ok ( ( Some ( evaluable) , manifest) )
76
+ }
77
+
78
+ /// Builds the "action loader" entry point, which reexports every found action
79
+ /// behind a lazy dynamic import.
80
+ ///
81
+ /// The actions are reexported under a hashed name (comprised of the exporting
82
+ /// file's name and the action name). This hash matches the id sent to the
83
+ /// client and present inside the paired manifest.
31
84
async fn build_server_actions_loader (
32
85
node_root : Vc < FileSystemPath > ,
33
- original_name : & str ,
86
+ app_page_name : & str ,
34
87
actions : Vc < ModuleActionMap > ,
35
88
asset_context : Vc < Box < dyn AssetContext > > ,
36
89
) -> Result < Vc < Box < dyn EcmascriptChunkPlaceable > > > {
37
90
let actions = actions. await ?;
38
91
39
92
let mut contents = RopeBuilder :: from ( "__turbopack_export_value__({\n " ) ;
40
93
let mut import_map = IndexMap :: with_capacity ( actions. len ( ) ) ;
94
+
95
+ // Every module which exports an action (that is accessible starting from our
96
+ // app page entry point) will be present. We generate a single loader file
97
+ // which lazily imports the respective module's chunk_item id and invokes
98
+ // the exported action function.
41
99
for ( i, ( module, actions_map) ) in actions. iter ( ) . enumerate ( ) {
42
- for ( id , name) in & * actions_map. await ? {
100
+ for ( hash_id , name) in & * actions_map. await ? {
43
101
writedoc ! (
44
102
contents,
45
103
"
46
- \x20 '{id }': (...args) => import('ACTIONS_MODULE{i}')
104
+ \x20 '{hash_id }': (...args) => import('ACTIONS_MODULE{i}')
47
105
.then(mod => (0, mod['{name}'])(...args)),\n
48
106
" ,
49
107
) ?;
@@ -52,7 +110,7 @@ async fn build_server_actions_loader(
52
110
}
53
111
write ! ( contents, "}});" ) ?;
54
112
55
- let output_path = node_root. join ( format ! ( "server/app{original_name }/actions.js" ) ) ;
113
+ let output_path = node_root. join ( format ! ( "server/app{app_page_name }/actions.js" ) ) ;
56
114
let file = File :: from ( contents. build ( ) ) ;
57
115
let source = VirtualSource :: new ( output_path, AssetContent :: file ( file. into ( ) ) ) ;
58
116
let module = asset_context. process (
@@ -69,73 +127,49 @@ async fn build_server_actions_loader(
69
127
Ok ( placeable)
70
128
}
71
129
72
- pub ( crate ) async fn create_server_actions_manifest (
73
- entry : Vc < Box < dyn EcmascriptChunkPlaceable > > ,
130
+ /// Builds a manifest containing every action's hashed id, with an internal
131
+ /// module id which exports a function using that hashed name.
132
+ async fn build_manifest (
74
133
node_root : Vc < FileSystemPath > ,
75
- original_name : & str ,
134
+ app_page_name : & str ,
76
135
runtime : NextRuntime ,
77
- asset_context : Vc < Box < dyn AssetContext > > ,
78
- chunking_context : Vc < Box < dyn EcmascriptChunkingContext > > ,
79
- ) -> Result < (
80
- Option < Vc < Box < dyn EvaluatableAsset > > > ,
81
- Vc < Box < dyn OutputAsset > > ,
82
- ) > {
83
- let actions = get_actions ( Vc :: upcast ( entry) ) ;
84
- let actions_value = actions. await ?;
85
-
86
- let path = node_root. join ( format ! (
87
- "server/app{original_name}/server-reference-manifest.json" ,
136
+ actions : Vc < ModuleActionMap > ,
137
+ loader_id : Vc < String > ,
138
+ ) -> Result < Vc < Box < dyn OutputAsset > > > {
139
+ let manifest_path = node_root. join ( format ! (
140
+ "server/app{app_page_name}/server-reference-manifest.json" ,
88
141
) ) ;
89
142
let mut manifest = ServerReferenceManifest {
90
143
..Default :: default ( )
91
144
} ;
92
145
93
- if actions_value. is_empty ( ) {
94
- let manifest = Vc :: upcast ( VirtualOutputAsset :: new (
95
- path,
96
- AssetContent :: file ( File :: from ( serde_json:: to_string_pretty ( & manifest) ?) . into ( ) ) ,
97
- ) ) ;
98
- return Ok ( ( None , manifest) ) ;
99
- }
100
-
146
+ let actions_value = actions. await ?;
147
+ let loader_id_value = loader_id. await ?;
101
148
let mapping = match runtime {
102
149
NextRuntime :: Edge => & mut manifest. edge ,
103
150
NextRuntime :: NodeJs => & mut manifest. node ,
104
151
} ;
105
152
106
- let loader =
107
- build_server_actions_loader ( node_root, original_name, actions, asset_context) . await ?;
108
- let chunk_item_id = loader
109
- . as_chunk_item ( chunking_context)
110
- . id ( )
111
- . to_string ( )
112
- . await ?;
113
-
114
153
for value in actions_value. values ( ) {
115
154
let value = value. await ?;
116
155
for hash in value. keys ( ) {
117
156
let entry = mapping. entry ( hash. clone ( ) ) . or_default ( ) ;
118
157
entry. workers . insert (
119
- format ! ( "app{original_name }" ) ,
120
- ActionManifestWorkerEntry :: String ( chunk_item_id . clone_value ( ) ) ,
158
+ format ! ( "app{app_page_name }" ) ,
159
+ ActionManifestWorkerEntry :: String ( loader_id_value . clone_value ( ) ) ,
121
160
) ;
122
161
}
123
162
}
124
- let manifest = Vc :: upcast ( VirtualOutputAsset :: new (
125
- path,
126
- AssetContent :: file ( File :: from ( serde_json:: to_string_pretty ( & manifest) ?) . into ( ) ) ,
127
- ) ) ;
128
-
129
- let Some ( evaluable) = Vc :: try_resolve_sidecast :: < Box < dyn EvaluatableAsset > > ( loader) . await ?
130
- else {
131
- bail ! ( "loader module must be evaluatable" ) ;
132
- } ;
133
163
134
- Ok ( ( Some ( evaluable) , manifest) )
164
+ Ok ( Vc :: upcast ( VirtualOutputAsset :: new (
165
+ manifest_path,
166
+ AssetContent :: file ( File :: from ( serde_json:: to_string_pretty ( & manifest) ?) . into ( ) ) ,
167
+ ) ) )
135
168
}
136
169
137
- /// Finds the first page component in our loader tree, which should be the page
138
- /// we're currently rendering.
170
+ /// Traverses the entire module graph starting from [module], looking for magic
171
+ /// comment which identifies server actions. Every found server action will be
172
+ /// returned along with the module which exports that action.
139
173
#[ turbo_tasks:: function]
140
174
async fn get_actions ( module : Vc < Box < dyn Module > > ) -> Result < Vc < ModuleActionMap > > {
141
175
let mut all_actions = IndexMap :: new ( ) ;
@@ -155,6 +189,9 @@ async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>>
155
189
Ok ( Vc :: cell ( all_actions) )
156
190
}
157
191
192
+ /// Inspects the comments inside [module] looking for the magic actions comment.
193
+ /// If found, we return the mapping of every action's hashed id to the name of
194
+ /// the exported action function. If not, we return a None.
158
195
#[ turbo_tasks:: function]
159
196
async fn parse_actions ( module : Vc < Box < dyn Module > > ) -> Result < Vc < OptionActionMap > > {
160
197
let Some ( ecmascript_asset) =
@@ -173,15 +210,24 @@ async fn parse_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<OptionActionMap
173
210
Ok ( Vc :: cell ( actions. map ( Vc :: cell) ) )
174
211
}
175
212
213
+ /// A mapping of every module which exports a Server Action, with the hashed id
214
+ /// and exported name of each found action.
176
215
#[ turbo_tasks:: value( transparent) ]
177
216
struct ModuleActionMap ( IndexMap < Vc < Box < dyn Module > > , Vc < ActionMap > > ) ;
178
217
179
- /// Maps the hashed `(filename, exported_action_name) -> exported_action_name`,
180
- /// so that we can invoke the correct action function when we receive a request
181
- /// with the hash in `Next-Action` header.
218
+ #[ turbo_tasks:: value_impl]
219
+ impl ModuleActionMap {
220
+ #[ turbo_tasks:: function]
221
+ pub fn empty ( ) -> Vc < Self > {
222
+ Vc :: cell ( IndexMap :: new ( ) )
223
+ }
224
+ }
225
+
226
+ /// Maps the hashed action id to the action's exported function name.
182
227
#[ turbo_tasks:: value( transparent) ]
183
228
struct ActionMap ( IndexMap < String , String > ) ;
184
229
230
+ /// An Option wrapper around [ActionMap].
185
231
#[ turbo_tasks:: value( transparent) ]
186
232
struct OptionActionMap ( Option < Vc < ActionMap > > ) ;
187
233
0 commit comments