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::{
8    FileContent, FileSystem, FileSystemPath, VirtualFileSystem, glob::Glob, rope::RopeBuilder,
9};
10use turbopack_core::{
11    asset::{Asset, AssetContent},
12    chunk::{AsyncModuleInfo, ChunkItem, ChunkType, ChunkableModule, ChunkingContext},
13    context::AssetContext,
14    ident::{AssetIdent, Layer},
15    module::Module,
16    module_graph::ModuleGraph,
17    raw_module::RawModule,
18    reference::{ModuleReference, ModuleReferences, TracedModuleReference},
19    reference_type::ReferenceType,
20    resolve::parse::Request,
21};
22
23use crate::{
24    EcmascriptModuleContent,
25    chunk::{
26        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
27        EcmascriptChunkType, EcmascriptExports,
28    },
29    references::async_module::{AsyncModule, OptionAsyncModule},
30    runtime_functions::{
31        TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
32        TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
33    },
34    utils::StringifyJs,
35};
36
37#[derive(
38    Copy,
39    Clone,
40    Debug,
41    Eq,
42    PartialEq,
43    Serialize,
44    Deserialize,
45    TraceRawVcs,
46    TaskInput,
47    Hash,
48    NonLocalValue,
49)]
50pub enum CachedExternalType {
51    CommonJs,
52    EcmaScriptViaRequire,
53    EcmaScriptViaImport,
54    Global,
55    Script,
56}
57
58#[derive(
59    Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TraceRawVcs, TaskInput, Hash, NonLocalValue,
60)]
61/// Whether to add a traced reference to the external module using the given context and resolve
62/// origin.
63pub enum CachedExternalTracingMode {
64    Untraced,
65    Traced {
66        externals_context: ResolvedVc<Box<dyn AssetContext>>,
67        root_origin: FileSystemPath,
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    tracing_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        tracing_mode: CachedExternalTracingMode,
97    ) -> Vc<Self> {
98        Self::cell(CachedExternalModule {
99            request,
100            external_type,
101            tracing_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.tracing_mode {
230            CachedExternalTracingMode::Untraced => ModuleReferences::empty(),
231            CachedExternalTracingMode::Traced {
232                externals_context,
233                root_origin,
234            } => {
235                let reference_type = match self.external_type {
236                    CachedExternalType::EcmaScriptViaImport => {
237                        ReferenceType::EcmaScriptModules(Default::default())
238                    }
239                    CachedExternalType::CommonJs | CachedExternalType::EcmaScriptViaRequire => {
240                        ReferenceType::CommonJs(Default::default())
241                    }
242                    _ => ReferenceType::Undefined,
243                };
244
245                let external_result = externals_context
246                    .resolve_asset(
247                        root_origin.clone(),
248                        Request::parse_string(self.request.clone()),
249                        externals_context
250                            .resolve_options(root_origin.clone(), reference_type.clone()),
251                        reference_type,
252                    )
253                    .await?;
254                let references = external_result
255                    .affecting_sources
256                    .iter()
257                    .map(|s| Vc::upcast::<Box<dyn Module>>(RawModule::new(**s)))
258                    .chain(
259                        external_result
260                            .primary_modules_raw_iter()
261                            // These modules aren't bundled but still need to be part of the module
262                            // graph for chunking. `compute_async_module_info` computes
263                            // `is_self_async` for every module, but at least for traced modules,
264                            // that value is never used as `ChunkingType::Traced.is_inherit_async()
265                            // == false`. Optimize this case by using `ModuleWithoutSelfAsync` to
266                            // short circuit that computation and thus defer parsing traced modules
267                            // to emitting to not block all of chunking on this.
268                            .map(|m| Vc::upcast(ModuleWithoutSelfAsync::new(*m))),
269                    )
270                    .map(|s| {
271                        Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(s))
272                            .to_resolved()
273                    })
274                    .try_join()
275                    .await?;
276                Vc::cell(references)
277            }
278        })
279    }
280
281    #[turbo_tasks::function]
282    fn is_self_async(&self) -> Result<Vc<bool>> {
283        Ok(Vc::cell(
284            self.external_type == CachedExternalType::EcmaScriptViaImport
285                || self.external_type == CachedExternalType::Script,
286        ))
287    }
288}
289
290#[turbo_tasks::value_impl]
291impl Asset for CachedExternalModule {
292    #[turbo_tasks::function]
293    fn content(self: Vc<Self>) -> Vc<AssetContent> {
294        // should be `NotFound` as this function gets called to detect source changes
295        AssetContent::file(FileContent::NotFound.cell())
296    }
297}
298
299#[turbo_tasks::value_impl]
300impl ChunkableModule for CachedExternalModule {
301    #[turbo_tasks::function]
302    fn as_chunk_item(
303        self: ResolvedVc<Self>,
304        _module_graph: Vc<ModuleGraph>,
305        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
306    ) -> Vc<Box<dyn ChunkItem>> {
307        Vc::upcast(
308            CachedExternalModuleChunkItem {
309                module: self,
310                chunking_context,
311            }
312            .cell(),
313        )
314    }
315}
316
317#[turbo_tasks::value_impl]
318impl EcmascriptChunkPlaceable for CachedExternalModule {
319    #[turbo_tasks::function]
320    fn get_exports(&self) -> Vc<EcmascriptExports> {
321        if self.external_type == CachedExternalType::CommonJs {
322            EcmascriptExports::CommonJs.cell()
323        } else {
324            EcmascriptExports::DynamicNamespace.cell()
325        }
326    }
327
328    #[turbo_tasks::function]
329    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
330        Vc::cell(
331            if self.external_type == CachedExternalType::EcmaScriptViaImport
332                || self.external_type == CachedExternalType::Script
333            {
334                Some(
335                    AsyncModule {
336                        has_top_level_await: true,
337                        import_externals: self.external_type
338                            == CachedExternalType::EcmaScriptViaImport,
339                    }
340                    .resolved_cell(),
341                )
342            } else {
343                None
344            },
345        )
346    }
347
348    #[turbo_tasks::function]
349    fn is_marked_as_side_effect_free(
350        self: Vc<Self>,
351        _side_effect_free_packages: Vc<Glob>,
352    ) -> Vc<bool> {
353        Vc::cell(false)
354    }
355}
356
357#[turbo_tasks::value]
358pub struct CachedExternalModuleChunkItem {
359    module: ResolvedVc<CachedExternalModule>,
360    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
361}
362
363#[turbo_tasks::value_impl]
364impl ChunkItem for CachedExternalModuleChunkItem {
365    #[turbo_tasks::function]
366    fn asset_ident(&self) -> Vc<AssetIdent> {
367        self.module.ident()
368    }
369
370    #[turbo_tasks::function]
371    fn ty(self: Vc<Self>) -> Vc<Box<dyn ChunkType>> {
372        Vc::upcast(Vc::<EcmascriptChunkType>::default())
373    }
374
375    #[turbo_tasks::function]
376    fn module(&self) -> Vc<Box<dyn Module>> {
377        Vc::upcast(*self.module)
378    }
379
380    #[turbo_tasks::function]
381    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
382        *self.chunking_context
383    }
384}
385
386#[turbo_tasks::value_impl]
387impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
388    #[turbo_tasks::function]
389    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
390        panic!("content() should not be called");
391    }
392
393    #[turbo_tasks::function]
394    fn content_with_async_module_info(
395        &self,
396        async_module_info: Option<Vc<AsyncModuleInfo>>,
397    ) -> Vc<EcmascriptChunkItemContent> {
398        let async_module_options = self
399            .module
400            .get_async_module()
401            .module_options(async_module_info);
402
403        EcmascriptChunkItemContent::new(
404            self.module.content(),
405            *self.chunking_context,
406            async_module_options,
407        )
408    }
409}
410
411/// A wrapper "passthrough" module type that always returns `false` for `is_self_async`. Be careful
412/// when using it, as it may hide async dependencies.
413#[turbo_tasks::value]
414pub struct ModuleWithoutSelfAsync {
415    module: ResolvedVc<Box<dyn Module>>,
416}
417
418#[turbo_tasks::value_impl]
419impl ModuleWithoutSelfAsync {
420    #[turbo_tasks::function]
421    pub fn new(module: ResolvedVc<Box<dyn Module>>) -> Vc<Self> {
422        Self::cell(ModuleWithoutSelfAsync { module })
423    }
424}
425
426#[turbo_tasks::value_impl]
427impl Asset for ModuleWithoutSelfAsync {
428    #[turbo_tasks::function]
429    fn content(&self) -> Vc<AssetContent> {
430        self.module.content()
431    }
432}
433
434#[turbo_tasks::value_impl]
435impl Module for ModuleWithoutSelfAsync {
436    #[turbo_tasks::function]
437    fn ident(&self) -> Vc<AssetIdent> {
438        self.module.ident()
439    }
440
441    #[turbo_tasks::function]
442    fn references(&self) -> Vc<ModuleReferences> {
443        self.module.references()
444    }
445
446    // Don't override and use default is_self_async that always returns false
447}