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        ))
214    }
215}
216
217#[turbo_tasks::value_impl]
218impl Asset for CachedExternalModule {
219    #[turbo_tasks::function]
220    fn content(self: Vc<Self>) -> Vc<AssetContent> {
221        // should be `NotFound` as this function gets called to detect source changes
222        AssetContent::file(FileContent::NotFound.cell())
223    }
224}
225
226#[turbo_tasks::value_impl]
227impl ChunkableModule for CachedExternalModule {
228    #[turbo_tasks::function]
229    fn as_chunk_item(
230        self: ResolvedVc<Self>,
231        _module_graph: Vc<ModuleGraph>,
232        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
233    ) -> Vc<Box<dyn ChunkItem>> {
234        Vc::upcast(
235            CachedExternalModuleChunkItem {
236                module: self,
237                chunking_context,
238            }
239            .cell(),
240        )
241    }
242}
243
244#[turbo_tasks::value_impl]
245impl EcmascriptChunkPlaceable for CachedExternalModule {
246    #[turbo_tasks::function]
247    fn get_exports(&self) -> Vc<EcmascriptExports> {
248        if self.external_type == CachedExternalType::CommonJs {
249            EcmascriptExports::CommonJs.cell()
250        } else {
251            EcmascriptExports::DynamicNamespace.cell()
252        }
253    }
254
255    #[turbo_tasks::function]
256    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
257        Vc::cell(
258            if self.external_type == CachedExternalType::EcmaScriptViaImport {
259                Some(
260                    AsyncModule {
261                        has_top_level_await: true,
262                        import_externals: true,
263                    }
264                    .resolved_cell(),
265                )
266            } else {
267                None
268            },
269        )
270    }
271
272    #[turbo_tasks::function]
273    fn is_marked_as_side_effect_free(
274        self: Vc<Self>,
275        _side_effect_free_packages: Vc<Glob>,
276    ) -> Vc<bool> {
277        Vc::cell(false)
278    }
279}
280
281#[turbo_tasks::value]
282pub struct CachedExternalModuleChunkItem {
283    module: ResolvedVc<CachedExternalModule>,
284    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
285}
286
287// Without this wrapper, VirtualFileSystem::new_with_name always returns a new filesystem
288#[turbo_tasks::function]
289fn external_fs() -> Vc<VirtualFileSystem> {
290    VirtualFileSystem::new_with_name(rcstr!("externals"))
291}
292
293#[turbo_tasks::value_impl]
294impl ChunkItem for CachedExternalModuleChunkItem {
295    #[turbo_tasks::function]
296    fn asset_ident(&self) -> Vc<AssetIdent> {
297        self.module.ident()
298    }
299
300    #[turbo_tasks::function]
301    fn ty(self: Vc<Self>) -> Vc<Box<dyn ChunkType>> {
302        Vc::upcast(Vc::<EcmascriptChunkType>::default())
303    }
304
305    #[turbo_tasks::function]
306    fn module(&self) -> Vc<Box<dyn Module>> {
307        Vc::upcast(*self.module)
308    }
309
310    #[turbo_tasks::function]
311    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
312        *self.chunking_context
313    }
314}
315
316#[turbo_tasks::value_impl]
317impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
318    #[turbo_tasks::function]
319    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
320        panic!("content() should not be called");
321    }
322
323    #[turbo_tasks::function]
324    fn content_with_async_module_info(
325        &self,
326        async_module_info: Option<Vc<AsyncModuleInfo>>,
327    ) -> Vc<EcmascriptChunkItemContent> {
328        let async_module_options = self
329            .module
330            .get_async_module()
331            .module_options(async_module_info);
332
333        EcmascriptChunkItemContent::new(
334            self.module.content(),
335            *self.chunking_context,
336            EcmascriptOptions::default().cell(),
337            async_module_options,
338        )
339    }
340}