turbopack_ecmascript/references/
external_module.rs

1use std::{fmt::Display, io::Write};
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs};
7use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem, glob::Glob, rope::RopeBuilder};
8use turbopack_core::{
9    asset::{Asset, AssetContent},
10    chunk::{AsyncModuleInfo, ChunkItem, ChunkType, ChunkableModule, ChunkingContext},
11    ident::{AssetIdent, Layer},
12    module::Module,
13    module_graph::ModuleGraph,
14    raw_module::RawModule,
15    reference::{ModuleReference, ModuleReferences, TracedModuleReference},
16    reference_type::ReferenceType,
17    resolve::{
18        origin::{ResolveOrigin, ResolveOriginExt},
19        parse::Request,
20    },
21};
22use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
23
24use crate::{
25    EcmascriptModuleContent,
26    chunk::{
27        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
28        EcmascriptChunkType, EcmascriptExports,
29    },
30    references::async_module::{AsyncModule, OptionAsyncModule},
31    runtime_functions::{
32        TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
33        TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
34    },
35    utils::StringifyJs,
36};
37
38#[derive(
39    Copy,
40    Clone,
41    Debug,
42    Eq,
43    PartialEq,
44    Serialize,
45    Deserialize,
46    TraceRawVcs,
47    TaskInput,
48    Hash,
49    NonLocalValue,
50)]
51pub enum CachedExternalType {
52    CommonJs,
53    EcmaScriptViaRequire,
54    EcmaScriptViaImport,
55    Global,
56    Script,
57}
58
59#[derive(
60    Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TraceRawVcs, TaskInput, Hash, NonLocalValue,
61)]
62/// Whether to add a traced reference to the external module using the given context and resolve
63/// origin.
64pub enum CachedExternalTracingMode {
65    Untraced,
66    Traced {
67        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
68    },
69}
70
71impl Display for CachedExternalType {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            CachedExternalType::CommonJs => write!(f, "cjs"),
75            CachedExternalType::EcmaScriptViaRequire => write!(f, "esm_require"),
76            CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"),
77            CachedExternalType::Global => write!(f, "global"),
78            CachedExternalType::Script => write!(f, "script"),
79        }
80    }
81}
82
83#[turbo_tasks::value]
84pub struct CachedExternalModule {
85    request: RcStr,
86    external_type: CachedExternalType,
87    analyze_mode: CachedExternalTracingMode,
88}
89
90#[turbo_tasks::value_impl]
91impl CachedExternalModule {
92    #[turbo_tasks::function]
93    pub fn new(
94        request: RcStr,
95        external_type: CachedExternalType,
96        analyze_mode: CachedExternalTracingMode,
97    ) -> Vc<Self> {
98        Self::cell(CachedExternalModule {
99            request,
100            external_type,
101            analyze_mode,
102        })
103    }
104
105    #[turbo_tasks::function]
106    pub fn content(&self) -> Result<Vc<EcmascriptModuleContent>> {
107        let mut code = RopeBuilder::default();
108
109        match self.external_type {
110            CachedExternalType::EcmaScriptViaImport => {
111                writeln!(
112                    code,
113                    "const mod = await {TURBOPACK_EXTERNAL_IMPORT}({});",
114                    StringifyJs(&self.request)
115                )?;
116            }
117            CachedExternalType::Global => {
118                if self.request.is_empty() {
119                    writeln!(code, "const mod = {{}};")?;
120                } else {
121                    writeln!(
122                        code,
123                        "const mod = globalThis[{}];",
124                        StringifyJs(&self.request)
125                    )?;
126                }
127            }
128            CachedExternalType::Script => {
129                // Parse the request format: "variableName@url"
130                // e.g., "foo@https://test.test.com"
131                if let Some(at_index) = self.request.find('@') {
132                    let variable_name = &self.request[..at_index];
133                    let url = &self.request[at_index + 1..];
134
135                    // Wrap the loading and variable access in a try-catch block
136                    writeln!(code, "let mod;")?;
137                    writeln!(code, "try {{")?;
138
139                    // First load the URL
140                    writeln!(
141                        code,
142                        "  await {TURBOPACK_LOAD_BY_URL}({});",
143                        StringifyJs(url)
144                    )?;
145
146                    // Then get the variable from global with existence check
147                    writeln!(
148                        code,
149                        "  if (typeof global[{}] === 'undefined') {{",
150                        StringifyJs(variable_name)
151                    )?;
152                    writeln!(
153                        code,
154                        "    throw new Error('Variable {} is not available on global object after \
155                         loading {}');",
156                        StringifyJs(variable_name),
157                        StringifyJs(url)
158                    )?;
159                    writeln!(code, "  }}")?;
160                    writeln!(code, "  mod = global[{}];", StringifyJs(variable_name))?;
161
162                    // Catch and re-throw errors with more context
163                    writeln!(code, "}} catch (error) {{")?;
164                    writeln!(
165                        code,
166                        "  throw new Error('Failed to load external URL module {}: ' + \
167                         (error.message || error));",
168                        StringifyJs(&self.request)
169                    )?;
170                    writeln!(code, "}}")?;
171                } else {
172                    // Invalid format - throw error
173                    writeln!(
174                        code,
175                        "throw new Error('Invalid URL external format. Expected \"variable@url\", \
176                         got: {}');",
177                        StringifyJs(&self.request)
178                    )?;
179                    writeln!(code, "const mod = undefined;")?;
180                }
181            }
182            CachedExternalType::EcmaScriptViaRequire | CachedExternalType::CommonJs => {
183                writeln!(
184                    code,
185                    "const mod = {TURBOPACK_EXTERNAL_REQUIRE}({}, () => require({}));",
186                    StringifyJs(&self.request),
187                    StringifyJs(&self.request)
188                )?;
189            }
190        }
191
192        writeln!(code)?;
193
194        if self.external_type == CachedExternalType::CommonJs {
195            writeln!(code, "module.exports = mod;")?;
196        } else if self.external_type == CachedExternalType::EcmaScriptViaImport
197            || self.external_type == CachedExternalType::EcmaScriptViaRequire
198        {
199            writeln!(code, "{TURBOPACK_EXPORT_NAMESPACE}(mod);")?;
200        } else {
201            writeln!(code, "{TURBOPACK_EXPORT_VALUE}(mod);")?;
202        }
203
204        Ok(EcmascriptModuleContent {
205            inner_code: code.build(),
206            source_map: None,
207            is_esm: self.external_type != CachedExternalType::CommonJs,
208            strict: false,
209            additional_ids: Default::default(),
210        }
211        .cell())
212    }
213}
214
215#[turbo_tasks::value_impl]
216impl Module for CachedExternalModule {
217    #[turbo_tasks::function]
218    async fn ident(&self) -> Result<Vc<AssetIdent>> {
219        let fs = VirtualFileSystem::new_with_name(rcstr!("externals"));
220
221        Ok(AssetIdent::from_path(fs.root().await?.join(&self.request)?)
222            .with_layer(Layer::new(rcstr!("external")))
223            .with_modifier(self.request.clone())
224            .with_modifier(self.external_type.to_string().into()))
225    }
226
227    #[turbo_tasks::function]
228    async fn references(&self) -> Result<Vc<ModuleReferences>> {
229        Ok(match &self.analyze_mode {
230            CachedExternalTracingMode::Untraced => ModuleReferences::empty(),
231            CachedExternalTracingMode::Traced { origin } => {
232                let external_result = match self.external_type {
233                    CachedExternalType::EcmaScriptViaImport => {
234                        esm_resolve(
235                            **origin,
236                            Request::parse_string(self.request.clone()),
237                            Default::default(),
238                            false,
239                            None,
240                        )
241                        .await?
242                        .await?
243                    }
244                    CachedExternalType::CommonJs | CachedExternalType::EcmaScriptViaRequire => {
245                        cjs_resolve(
246                            **origin,
247                            Request::parse_string(self.request.clone()),
248                            Default::default(),
249                            None,
250                            false,
251                        )
252                        .await?
253                    }
254                    CachedExternalType::Global | CachedExternalType::Script => {
255                        origin
256                            .resolve_asset(
257                                Request::parse_string(self.request.clone()),
258                                origin.resolve_options(ReferenceType::Undefined).await?,
259                                ReferenceType::Undefined,
260                            )
261                            .await?
262                            .await?
263                    }
264                };
265
266                let references = external_result
267                    .affecting_sources
268                    .iter()
269                    .map(|s| Vc::upcast::<Box<dyn Module>>(RawModule::new(**s)))
270                    .chain(
271                        external_result
272                            .primary_modules_raw_iter()
273                            // These modules aren't bundled but still need to be part of the module
274                            // graph for chunking. `compute_async_module_info` computes
275                            // `is_self_async` for every module, but at least for traced modules,
276                            // that value is never used as `ChunkingType::Traced.is_inherit_async()
277                            // == false`. Optimize this case by using `ModuleWithoutSelfAsync` to
278                            // short circuit that computation and thus defer parsing traced modules
279                            // to emitting to not block all of chunking on this.
280                            .map(|m| Vc::upcast(ModuleWithoutSelfAsync::new(*m))),
281                    )
282                    .map(|s| {
283                        Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(s))
284                            .to_resolved()
285                    })
286                    .try_join()
287                    .await?;
288                Vc::cell(references)
289            }
290        })
291    }
292
293    #[turbo_tasks::function]
294    fn is_self_async(&self) -> Result<Vc<bool>> {
295        Ok(Vc::cell(
296            self.external_type == CachedExternalType::EcmaScriptViaImport
297                || self.external_type == CachedExternalType::Script,
298        ))
299    }
300}
301
302#[turbo_tasks::value_impl]
303impl Asset for CachedExternalModule {
304    #[turbo_tasks::function]
305    fn content(self: Vc<Self>) -> Vc<AssetContent> {
306        // should be `NotFound` as this function gets called to detect source changes
307        AssetContent::file(FileContent::NotFound.cell())
308    }
309}
310
311#[turbo_tasks::value_impl]
312impl ChunkableModule for CachedExternalModule {
313    #[turbo_tasks::function]
314    fn as_chunk_item(
315        self: ResolvedVc<Self>,
316        _module_graph: Vc<ModuleGraph>,
317        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
318    ) -> Vc<Box<dyn ChunkItem>> {
319        Vc::upcast(
320            CachedExternalModuleChunkItem {
321                module: self,
322                chunking_context,
323            }
324            .cell(),
325        )
326    }
327}
328
329#[turbo_tasks::value_impl]
330impl EcmascriptChunkPlaceable for CachedExternalModule {
331    #[turbo_tasks::function]
332    fn get_exports(&self) -> Vc<EcmascriptExports> {
333        if self.external_type == CachedExternalType::CommonJs {
334            EcmascriptExports::CommonJs.cell()
335        } else {
336            EcmascriptExports::DynamicNamespace.cell()
337        }
338    }
339
340    #[turbo_tasks::function]
341    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
342        Vc::cell(
343            if self.external_type == CachedExternalType::EcmaScriptViaImport
344                || self.external_type == CachedExternalType::Script
345            {
346                Some(
347                    AsyncModule {
348                        has_top_level_await: true,
349                        import_externals: self.external_type
350                            == CachedExternalType::EcmaScriptViaImport,
351                    }
352                    .resolved_cell(),
353                )
354            } else {
355                None
356            },
357        )
358    }
359
360    #[turbo_tasks::function]
361    fn is_marked_as_side_effect_free(
362        self: Vc<Self>,
363        _side_effect_free_packages: Vc<Glob>,
364    ) -> Vc<bool> {
365        Vc::cell(false)
366    }
367}
368
369#[turbo_tasks::value]
370pub struct CachedExternalModuleChunkItem {
371    module: ResolvedVc<CachedExternalModule>,
372    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
373}
374
375#[turbo_tasks::value_impl]
376impl ChunkItem for CachedExternalModuleChunkItem {
377    #[turbo_tasks::function]
378    fn asset_ident(&self) -> Vc<AssetIdent> {
379        self.module.ident()
380    }
381
382    #[turbo_tasks::function]
383    fn ty(self: Vc<Self>) -> Vc<Box<dyn ChunkType>> {
384        Vc::upcast(Vc::<EcmascriptChunkType>::default())
385    }
386
387    #[turbo_tasks::function]
388    fn module(&self) -> Vc<Box<dyn Module>> {
389        Vc::upcast(*self.module)
390    }
391
392    #[turbo_tasks::function]
393    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
394        *self.chunking_context
395    }
396}
397
398#[turbo_tasks::value_impl]
399impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
400    #[turbo_tasks::function]
401    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
402        panic!("content() should not be called");
403    }
404
405    #[turbo_tasks::function]
406    fn content_with_async_module_info(
407        &self,
408        async_module_info: Option<Vc<AsyncModuleInfo>>,
409    ) -> Vc<EcmascriptChunkItemContent> {
410        let async_module_options = self
411            .module
412            .get_async_module()
413            .module_options(async_module_info);
414
415        EcmascriptChunkItemContent::new(
416            self.module.content(),
417            *self.chunking_context,
418            async_module_options,
419        )
420    }
421}
422
423/// A wrapper "passthrough" module type that always returns `false` for `is_self_async`. Be careful
424/// when using it, as it may hide async dependencies.
425#[turbo_tasks::value]
426pub struct ModuleWithoutSelfAsync {
427    module: ResolvedVc<Box<dyn Module>>,
428}
429
430#[turbo_tasks::value_impl]
431impl ModuleWithoutSelfAsync {
432    #[turbo_tasks::function]
433    pub fn new(module: ResolvedVc<Box<dyn Module>>) -> Vc<Self> {
434        Self::cell(ModuleWithoutSelfAsync { module })
435    }
436}
437
438#[turbo_tasks::value_impl]
439impl Asset for ModuleWithoutSelfAsync {
440    #[turbo_tasks::function]
441    fn content(&self) -> Vc<AssetContent> {
442        self.module.content()
443    }
444}
445
446#[turbo_tasks::value_impl]
447impl Module for ModuleWithoutSelfAsync {
448    #[turbo_tasks::function]
449    fn ident(&self) -> Vc<AssetIdent> {
450        self.module.ident()
451    }
452
453    #[turbo_tasks::function]
454    fn references(&self) -> Vc<ModuleReferences> {
455        self.module.references()
456    }
457
458    // Don't override and use default is_self_async that always returns false
459}