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, 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    reference::{ModuleReference, ModuleReferences},
15};
16
17use crate::{
18    EcmascriptModuleContent, EcmascriptOptions,
19    chunk::{
20        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
21        EcmascriptChunkType, EcmascriptExports,
22    },
23    references::async_module::{AsyncModule, OptionAsyncModule},
24    runtime_functions::{
25        TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE,
26        TURBOPACK_LOAD_BY_URL,
27    },
28    utils::StringifyJs,
29};
30
31#[derive(
32    Copy,
33    Clone,
34    Debug,
35    Eq,
36    PartialEq,
37    Serialize,
38    Deserialize,
39    TraceRawVcs,
40    TaskInput,
41    Hash,
42    NonLocalValue,
43)]
44pub enum CachedExternalType {
45    CommonJs,
46    EcmaScriptViaRequire,
47    EcmaScriptViaImport,
48    Global,
49    Script,
50}
51
52impl Display for CachedExternalType {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            CachedExternalType::CommonJs => write!(f, "cjs"),
56            CachedExternalType::EcmaScriptViaRequire => write!(f, "esm_require"),
57            CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"),
58            CachedExternalType::Global => write!(f, "global"),
59            CachedExternalType::Script => write!(f, "script"),
60        }
61    }
62}
63
64#[turbo_tasks::value]
65pub struct CachedExternalModule {
66    pub request: RcStr,
67    pub external_type: CachedExternalType,
68    pub additional_references: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
69}
70
71#[turbo_tasks::value_impl]
72impl CachedExternalModule {
73    #[turbo_tasks::function]
74    pub fn new(
75        request: RcStr,
76        external_type: CachedExternalType,
77        additional_references: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
78    ) -> Vc<Self> {
79        Self::cell(CachedExternalModule {
80            request,
81            external_type,
82            additional_references,
83        })
84    }
85
86    #[turbo_tasks::function]
87    pub fn content(&self) -> Result<Vc<EcmascriptModuleContent>> {
88        let mut code = RopeBuilder::default();
89
90        match self.external_type {
91            CachedExternalType::EcmaScriptViaImport => {
92                writeln!(
93                    code,
94                    "const mod = await {TURBOPACK_EXTERNAL_IMPORT}({});",
95                    StringifyJs(&self.request)
96                )?;
97            }
98            CachedExternalType::Global => {
99                if self.request.is_empty() {
100                    writeln!(code, "const mod = {{}};")?;
101                } else {
102                    writeln!(
103                        code,
104                        "const mod = globalThis[{}];",
105                        StringifyJs(&self.request)
106                    )?;
107                }
108            }
109            CachedExternalType::Script => {
110                // Parse the request format: "variableName@url"
111                // e.g., "foo@https://test.test.com"
112                if let Some(at_index) = self.request.find('@') {
113                    let variable_name = &self.request[..at_index];
114                    let url = &self.request[at_index + 1..];
115
116                    // Wrap the loading and variable access in a try-catch block
117                    writeln!(code, "let mod;")?;
118                    writeln!(code, "try {{")?;
119
120                    // First load the URL
121                    writeln!(
122                        code,
123                        "  await {TURBOPACK_LOAD_BY_URL}({});",
124                        StringifyJs(url)
125                    )?;
126
127                    // Then get the variable from global with existence check
128                    writeln!(
129                        code,
130                        "  if (typeof global[{}] === 'undefined') {{",
131                        StringifyJs(variable_name)
132                    )?;
133                    writeln!(
134                        code,
135                        "    throw new Error('Variable {} is not available on global object after \
136                         loading {}');",
137                        StringifyJs(variable_name),
138                        StringifyJs(url)
139                    )?;
140                    writeln!(code, "  }}")?;
141                    writeln!(code, "  mod = global[{}];", StringifyJs(variable_name))?;
142
143                    // Catch and re-throw errors with more context
144                    writeln!(code, "}} catch (error) {{")?;
145                    writeln!(
146                        code,
147                        "  throw new Error('Failed to load external URL module {}: ' + \
148                         (error.message || error));",
149                        StringifyJs(&self.request)
150                    )?;
151                    writeln!(code, "}}")?;
152                } else {
153                    // Invalid format - throw error
154                    writeln!(
155                        code,
156                        "throw new Error('Invalid URL external format. Expected \"variable@url\", \
157                         got: {}');",
158                        StringifyJs(&self.request)
159                    )?;
160                    writeln!(code, "const mod = undefined;")?;
161                }
162            }
163            CachedExternalType::EcmaScriptViaRequire | CachedExternalType::CommonJs => {
164                writeln!(
165                    code,
166                    "const mod = {TURBOPACK_EXTERNAL_REQUIRE}({}, () => require({}));",
167                    StringifyJs(&self.request),
168                    StringifyJs(&self.request)
169                )?;
170            }
171        }
172
173        writeln!(code)?;
174
175        if self.external_type == CachedExternalType::CommonJs {
176            writeln!(code, "module.exports = mod;")?;
177        } else {
178            writeln!(code, "{TURBOPACK_EXPORT_NAMESPACE}(mod);")?;
179        }
180
181        Ok(EcmascriptModuleContent {
182            inner_code: code.build(),
183            source_map: None,
184            is_esm: self.external_type != CachedExternalType::CommonJs,
185            strict: false,
186            additional_ids: Default::default(),
187        }
188        .cell())
189    }
190}
191
192#[turbo_tasks::value_impl]
193impl Module for CachedExternalModule {
194    #[turbo_tasks::function]
195    async fn ident(&self) -> Result<Vc<AssetIdent>> {
196        let fs = VirtualFileSystem::new_with_name(rcstr!("externals"));
197
198        Ok(AssetIdent::from_path(fs.root().await?.join(&self.request)?)
199            .with_layer(Layer::new(rcstr!("external")))
200            .with_modifier(self.request.clone())
201            .with_modifier(self.external_type.to_string().into()))
202    }
203
204    #[turbo_tasks::function]
205    fn references(&self) -> Result<Vc<ModuleReferences>> {
206        Ok(Vc::cell(self.additional_references.clone()))
207    }
208
209    #[turbo_tasks::function]
210    fn is_self_async(&self) -> Result<Vc<bool>> {
211        Ok(Vc::cell(
212            self.external_type == CachedExternalType::EcmaScriptViaImport
213                || self.external_type == CachedExternalType::Script,
214        ))
215    }
216}
217
218#[turbo_tasks::value_impl]
219impl Asset for CachedExternalModule {
220    #[turbo_tasks::function]
221    fn content(self: Vc<Self>) -> Vc<AssetContent> {
222        // should be `NotFound` as this function gets called to detect source changes
223        AssetContent::file(FileContent::NotFound.cell())
224    }
225}
226
227#[turbo_tasks::value_impl]
228impl ChunkableModule for CachedExternalModule {
229    #[turbo_tasks::function]
230    fn as_chunk_item(
231        self: ResolvedVc<Self>,
232        _module_graph: Vc<ModuleGraph>,
233        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
234    ) -> Vc<Box<dyn ChunkItem>> {
235        Vc::upcast(
236            CachedExternalModuleChunkItem {
237                module: self,
238                chunking_context,
239            }
240            .cell(),
241        )
242    }
243}
244
245#[turbo_tasks::value_impl]
246impl EcmascriptChunkPlaceable for CachedExternalModule {
247    #[turbo_tasks::function]
248    fn get_exports(&self) -> Vc<EcmascriptExports> {
249        if self.external_type == CachedExternalType::CommonJs {
250            EcmascriptExports::CommonJs.cell()
251        } else {
252            EcmascriptExports::DynamicNamespace.cell()
253        }
254    }
255
256    #[turbo_tasks::function]
257    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
258        Vc::cell(
259            if self.external_type == CachedExternalType::EcmaScriptViaImport
260                || self.external_type == CachedExternalType::Script
261            {
262                Some(
263                    AsyncModule {
264                        has_top_level_await: true,
265                        import_externals: self.external_type
266                            == CachedExternalType::EcmaScriptViaImport,
267                    }
268                    .resolved_cell(),
269                )
270            } else {
271                None
272            },
273        )
274    }
275
276    #[turbo_tasks::function]
277    fn is_marked_as_side_effect_free(
278        self: Vc<Self>,
279        _side_effect_free_packages: Vc<Glob>,
280    ) -> Vc<bool> {
281        Vc::cell(false)
282    }
283}
284
285#[turbo_tasks::value]
286pub struct CachedExternalModuleChunkItem {
287    module: ResolvedVc<CachedExternalModule>,
288    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
289}
290
291// Without this wrapper, VirtualFileSystem::new_with_name always returns a new filesystem
292#[turbo_tasks::function]
293fn external_fs() -> Vc<VirtualFileSystem> {
294    VirtualFileSystem::new_with_name(rcstr!("externals"))
295}
296
297#[turbo_tasks::value_impl]
298impl ChunkItem for CachedExternalModuleChunkItem {
299    #[turbo_tasks::function]
300    fn asset_ident(&self) -> Vc<AssetIdent> {
301        self.module.ident()
302    }
303
304    #[turbo_tasks::function]
305    fn ty(self: Vc<Self>) -> Vc<Box<dyn ChunkType>> {
306        Vc::upcast(Vc::<EcmascriptChunkType>::default())
307    }
308
309    #[turbo_tasks::function]
310    fn module(&self) -> Vc<Box<dyn Module>> {
311        Vc::upcast(*self.module)
312    }
313
314    #[turbo_tasks::function]
315    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
316        *self.chunking_context
317    }
318}
319
320#[turbo_tasks::value_impl]
321impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
322    #[turbo_tasks::function]
323    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
324        panic!("content() should not be called");
325    }
326
327    #[turbo_tasks::function]
328    fn content_with_async_module_info(
329        &self,
330        async_module_info: Option<Vc<AsyncModuleInfo>>,
331    ) -> Vc<EcmascriptChunkItemContent> {
332        let async_module_options = self
333            .module
334            .get_async_module()
335            .module_options(async_module_info);
336
337        EcmascriptChunkItemContent::new(
338            self.module.content(),
339            *self.chunking_context,
340            EcmascriptOptions::default().cell(),
341            async_module_options,
342        )
343    }
344}