Skip to main content

turbopack_ecmascript/async_chunk/
proxy.rs

1use 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
41/// Marks the file name of a lazily compiled dynamic import's manifest chunk, followed by
42/// [`ACTIVATION_KEY_HEX_LEN`] hex digits.
43const ACTIVATION_KEY_MARKER: &str = "lazy-compilation-";
44const ACTIVATION_KEY_HEX_LEN: usize = 16;
45
46/// The key that activates the lazily compiled dynamic import represented by `proxy_ident`.
47///
48/// The key is embedded in the file name of the proxy's manifest chunk (see
49/// `ManifestAsyncModule::ident`), so a request for that chunk names the import to activate.
50pub 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
59/// Recovers the [`activation_key`] from the path of a requested chunk, if it is the manifest chunk
60/// of a lazily compiled dynamic import.
61pub 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/// The activation state of the lazily compiled dynamic import identified by `key`.
71///
72/// Memoized per key, so every proxy and every reader of a given key observe the same bit.
73#[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    /// The key that requesting this proxy's manifest chunk activates.
173    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            // The manifest chunk that lists this chunk is only served once the proxy is active,
283            // so this is only reachable through a stale cache.
284            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        // How the key survives `AssetIdent::output_name`: `/` and `.` become `_`, and the ident
331        // hash is appended after it.
332        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}