turbopack_ecmascript/async_chunk/
proxy.rs1use anyhow::Result;
2use indoc::formatdoc;
3use turbo_rcstr::{RcStr, rcstr};
4use turbo_tasks::{ResolvedVc, State, ValueToString, Vc};
5use turbo_tasks_hash::hash_xxh3_hash64;
6use turbopack_core::{
7 chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, ChunkingType},
8 ident::AssetIdent,
9 module::{Module, ModuleSideEffects},
10 module_graph::ModuleGraph,
11 reference::{ModuleReference, ModuleReferences},
12 resolve::{BindingUsage, ExportUsage, ImportUsage, ModuleResolveResult},
13};
14
15use crate::{
16 EcmascriptModuleAsset,
17 chunk::{
18 EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
19 ecmascript_chunk_item,
20 },
21 module_canonicalization::{EcmascriptModuleCanonicalization, canonicalize_ecmascript_module},
22 runtime_functions::{TURBOPACK_ASYNC_LOADER, TURBOPACK_EXPORT_NAMESPACE},
23 utils::{StringifyJs, StringifyModuleId},
24};
25
26#[turbo_tasks::value(serialization = "skip", evict = "never")]
27pub struct LazyCompilationState {
28 active: State<bool>,
29}
30
31impl LazyCompilationState {
32 pub fn is_active(&self) -> bool {
33 *self.active.get()
34 }
35
36 pub fn activate(&self) {
37 self.active.set(true);
38 }
39}
40
41const ACTIVATION_KEY_MARKER: &str = "lazy-compilation-";
44const ACTIVATION_KEY_HEX_LEN: usize = 16;
45
46pub fn activation_key(proxy_ident: &str) -> RcStr {
51 format!(
52 "{ACTIVATION_KEY_MARKER}{:0width$x}",
53 hash_xxh3_hash64(proxy_ident),
54 width = ACTIVATION_KEY_HEX_LEN
55 )
56 .into()
57}
58
59pub fn activation_key_from_chunk_path(path: &str) -> Option<RcStr> {
62 let start = path.rfind(ACTIVATION_KEY_MARKER)?;
63 let key = path.get(start..start + ACTIVATION_KEY_MARKER.len() + ACTIVATION_KEY_HEX_LEN)?;
64 key[ACTIVATION_KEY_MARKER.len()..]
65 .bytes()
66 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
67 .then(|| key.into())
68}
69
70#[turbo_tasks::function]
74pub fn lazy_compilation_state(key: RcStr) -> Vc<LazyCompilationState> {
75 let _ = key;
76 LazyCompilationState {
77 active: State::new(false),
78 }
79 .cell()
80}
81
82#[turbo_tasks::value]
83#[derive(ValueToString)]
84#[value_to_string("lazy compilation reference")]
85enum LazyCompilationReference {
86 Deferred {
87 module: ResolvedVc<EcmascriptModuleAsset>,
88 canonicalization: EcmascriptModuleCanonicalization,
89 },
90 Direct(ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>),
91}
92
93impl LazyCompilationReference {
94 async fn referenced_module(&self) -> Result<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>> {
95 match self {
96 Self::Deferred {
97 module,
98 canonicalization,
99 } => {
100 canonicalize_ecmascript_module(**module, canonicalization.clone())
101 .to_resolved()
102 .await
103 }
104 Self::Direct(module) => Ok(*module),
105 }
106 }
107}
108
109#[turbo_tasks::value_impl]
110impl LazyCompilationReference {
111 #[turbo_tasks::function]
112 fn deferred(
113 module: ResolvedVc<EcmascriptModuleAsset>,
114 canonicalization: EcmascriptModuleCanonicalization,
115 ) -> Vc<Self> {
116 Self::cell(Self::Deferred {
117 module,
118 canonicalization,
119 })
120 }
121
122 #[turbo_tasks::function]
123 fn direct(module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>) -> Vc<Self> {
124 Self::cell(Self::Direct(module))
125 }
126
127 #[turbo_tasks::function]
128 async fn proxy_ident(&self) -> Result<Vc<AssetIdent>> {
129 let ident = match self {
130 Self::Deferred {
131 module,
132 canonicalization,
133 } => module
134 .ident()
135 .owned()
136 .await?
137 .with_modifier(format!("lazy compilation proxy {canonicalization:?}").into()),
138 Self::Direct(module) => module
139 .ident()
140 .owned()
141 .await?
142 .with_modifier(rcstr!("lazy compilation proxy")),
143 };
144 Ok(ident.into_vc())
145 }
146}
147
148#[turbo_tasks::value_impl]
149impl ModuleReference for LazyCompilationReference {
150 #[turbo_tasks::function]
151 async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
152 let module = self.referenced_module().await?;
153 Ok(*ModuleResolveResult::module(ResolvedVc::upcast(module)))
154 }
155
156 fn chunking_type(&self) -> Option<ChunkingType> {
157 Some(ChunkingType::Async)
158 }
159
160 fn binding_usage(&self) -> BindingUsage {
161 BindingUsage {
162 import: ImportUsage::TopLevel,
163 export: ExportUsage::All,
164 }
165 }
166}
167
168#[turbo_tasks::value]
169pub struct LazyCompilationProxyModule {
170 target: ResolvedVc<LazyCompilationReference>,
171 ident: ResolvedVc<AssetIdent>,
172 pub(crate) key: RcStr,
174}
175
176#[turbo_tasks::value_impl]
177impl LazyCompilationProxyModule {
178 #[turbo_tasks::function]
179 pub async fn new_deferred(
180 module: ResolvedVc<EcmascriptModuleAsset>,
181 canonicalization: EcmascriptModuleCanonicalization,
182 ) -> Result<Vc<Self>> {
183 let target = LazyCompilationReference::deferred(*module, canonicalization)
184 .to_resolved()
185 .await?;
186 Ok(Self::new(*target))
187 }
188
189 #[turbo_tasks::function]
190 pub async fn new_direct(
191 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
192 ) -> Result<Vc<Self>> {
193 let target = LazyCompilationReference::direct(*module)
194 .to_resolved()
195 .await?;
196 Ok(Self::new(*target))
197 }
198
199 #[turbo_tasks::function]
200 async fn new(target: ResolvedVc<LazyCompilationReference>) -> Result<Vc<Self>> {
201 let ident = target.proxy_ident().to_resolved().await?;
202 let key = activation_key(&ident.to_string().await?);
203 Ok(Self::cell(Self { target, ident, key }))
204 }
205}
206
207#[turbo_tasks::value_impl]
208impl Module for LazyCompilationProxyModule {
209 #[turbo_tasks::function]
210 fn ident(&self) -> Vc<AssetIdent> {
211 *self.ident
212 }
213
214 #[turbo_tasks::function]
215 fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
216 Vc::cell(None)
217 }
218
219 #[turbo_tasks::function]
220 async fn references(&self) -> Result<Vc<ModuleReferences>> {
221 if !lazy_compilation_state(self.key.clone()).await?.is_active() {
222 return Ok(ModuleReferences::empty());
223 }
224
225 Ok(Vc::cell(vec![ResolvedVc::upcast(self.target)]))
226 }
227
228 #[turbo_tasks::function]
229 fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
230 ModuleSideEffects::SideEffectful.cell()
231 }
232}
233
234#[turbo_tasks::value_impl]
235impl ChunkableModule for LazyCompilationProxyModule {
236 #[turbo_tasks::function]
237 fn as_chunk_item(
238 self: ResolvedVc<Self>,
239 module_graph: ResolvedVc<ModuleGraph>,
240 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
241 ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
242 ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
243 }
244}
245
246#[turbo_tasks::value_impl]
247impl EcmascriptChunkPlaceable for LazyCompilationProxyModule {
248 #[turbo_tasks::function]
249 fn get_exports(&self) -> Vc<EcmascriptExports> {
250 EcmascriptExports::DynamicNamespace.cell()
251 }
252
253 #[turbo_tasks::function]
254 async fn chunk_item_content(
255 self: Vc<Self>,
256 chunking_context: Vc<Box<dyn ChunkingContext>>,
257 _module_graph: Vc<ModuleGraph>,
258 _async_module_info: Option<Vc<AsyncModuleInfo>>,
259 estimated: bool,
260 ) -> Result<Vc<EcmascriptChunkItemContent>> {
261 let this = self.await?;
262 if estimated {
263 return Ok(EcmascriptChunkItemContent::default().cell());
264 }
265
266 let state = lazy_compilation_state(this.key.clone()).await?;
267 let inner_code = if state.is_active() {
268 let target = this.target.await?.referenced_module().await?;
269 let loader_ident = chunking_context.async_loader_chunk_item_ident(Vc::upcast(*target));
270 let loader_id = chunking_context
271 .chunk_item_id_strategy()
272 .await?
273 .get_id_from_ident(loader_ident)
274 .await?;
275 formatdoc! {
276 r#"
277 {TURBOPACK_EXPORT_NAMESPACE}({TURBOPACK_ASYNC_LOADER}({loader_id}));
278 "#,
279 loader_id = StringifyModuleId(&loader_id),
280 }
281 } else {
282 formatdoc! {
285 r#"
286 {TURBOPACK_EXPORT_NAMESPACE}(Promise.reject(new Error({message})));
287 "#,
288 message = StringifyJs(&format!(
289 "Lazily compiled dynamic import of {} was not activated",
290 this.ident.to_string().await?
291 )),
292 }
293 };
294
295 Ok(EcmascriptChunkItemContent {
296 inner_code: inner_code.into(),
297 ..Default::default()
298 }
299 .cell())
300 }
301
302 #[turbo_tasks::function]
303 async fn chunk_item_content_ident(
304 self: Vc<Self>,
305 _chunking_context: Vc<Box<dyn ChunkingContext>>,
306 _module_graph: Vc<ModuleGraph>,
307 ) -> Result<Vc<AssetIdent>> {
308 let this = self.await?;
309 let state = lazy_compilation_state(this.key.clone()).await?;
310 Ok(self
311 .ident()
312 .owned()
313 .await?
314 .with_modifier(if state.is_active() {
315 rcstr!("active")
316 } else {
317 rcstr!("inactive")
318 })
319 .into_vc())
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::{activation_key, activation_key_from_chunk_path};
326
327 #[test]
328 fn recovers_the_key_from_a_chunk_path() {
329 let key = activation_key("[project]/app/target.tsx [app-client] (ecmascript)");
330 let path = format!("static/chunks/app_target_tsx_{key}_1a2b3c4.js");
333 assert_eq!(
334 activation_key_from_chunk_path(&path).as_deref(),
335 Some(&*key)
336 );
337 }
338
339 #[test]
340 fn ignores_paths_without_a_key() {
341 assert_eq!(
342 activation_key_from_chunk_path("static/chunks/app_page_1a2b3c4.js"),
343 None
344 );
345 assert_eq!(
346 activation_key_from_chunk_path("static/chunks/a_lazy-compilation-0123.js"),
347 None
348 );
349 assert_eq!(
350 activation_key_from_chunk_path("static/chunks/a_lazy-compilation-zzzzzzzzzzzzzzzz.js"),
351 None
352 );
353 }
354}