Skip to main content

turbopack_ecmascript/references/
external_module.rs

1use std::{borrow::Cow, fmt::Display, io::Write};
2
3use anyhow::{Context, Result};
4use bincode::{Decode, Encode};
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueToStringRef, Vc, trace::TraceRawVcs};
7use turbo_tasks_fs::{FileSystem, FileSystemPath, LinkType, VirtualFileSystem, rope::RopeBuilder};
8use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
9use turbopack_core::{
10    asset::{Asset, AssetContent},
11    chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, TracedMode},
12    ident::{AssetIdent, Layer},
13    module::{Module, ModuleSideEffects},
14    module_graph::ModuleGraph,
15    output::{
16        OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsReferences,
17        OutputAssetsWithReferenced,
18    },
19    raw_module::RawModule,
20    reference::{ModuleReference, ModuleReferences, TracedModuleReference},
21    reference_type::ReferenceType,
22    resolve::{
23        ResolveErrorMode,
24        origin::{ResolveOrigin, ResolveOriginExt},
25        parse::Request,
26    },
27};
28use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
29
30use crate::{
31    EcmascriptModuleContent,
32    chunk::{
33        EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
34        ecmascript_chunk_item,
35    },
36    references::async_module::{AsyncModule, OptionAsyncModule},
37    runtime_functions::{
38        TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
39        TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
40    },
41    utils::StringifyJs,
42};
43
44#[turbo_tasks::task_input]
45#[derive(Copy, Clone, Debug, Eq, PartialEq, TraceRawVcs, Hash, Encode, Decode)]
46pub enum CachedExternalType {
47    CommonJs,
48    EcmaScriptViaRequire,
49    EcmaScriptViaImport,
50    Global,
51    Script,
52}
53
54#[turbo_tasks::task_input]
55#[derive(Clone, Debug, Eq, PartialEq, TraceRawVcs, Hash, Encode, Decode)]
56/// Whether to add a traced reference to the external module using the given context and resolve
57/// origin.
58pub enum CachedExternalTracingMode {
59    Untraced,
60    Traced {
61        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
62    },
63}
64
65impl Display for CachedExternalType {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            CachedExternalType::CommonJs => write!(f, "cjs"),
69            CachedExternalType::EcmaScriptViaRequire => write!(f, "esm_require"),
70            CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"),
71            CachedExternalType::Global => write!(f, "global"),
72            CachedExternalType::Script => write!(f, "script"),
73        }
74    }
75}
76
77#[turbo_tasks::value]
78pub struct CachedExternalModule {
79    request: RcStr,
80    target: Option<FileSystemPath>,
81    external_type: CachedExternalType,
82    analyze_mode: CachedExternalTracingMode,
83}
84
85/// For a given package folder inside of node_modules, generate a unique hashed package name.
86///
87/// E.g. `/path/to/node_modules/@swc/core` becomes `@swc/core-1149fa2b3c4d5e6f`
88fn hashed_package_name(folder: &FileSystemPath) -> String {
89    let hash = encode_hex(hash_xxh3_hash64(&folder.path));
90
91    let parent = folder.parent();
92    let parent = parent.file_name();
93    let pkg = folder.file_name();
94    if parent.starts_with('@') {
95        format!("{parent}/{pkg}-{hash}")
96    } else {
97        format!("{pkg}-{hash}")
98    }
99}
100
101impl CachedExternalModule {
102    /// Rewrites `self.request` to include the hashed package name if `self.target` is set.
103    pub fn request(&self) -> Cow<'_, str> {
104        if let Some(target) = &self.target {
105            let hashed_package = hashed_package_name(target);
106
107            let request = if self.request.starts_with('@') {
108                // Potentially strip off `@org/...`
109                self.request.split_once('/').unwrap().1
110            } else {
111                &*self.request
112            };
113
114            if let Some((_, subpath)) = request.split_once('/') {
115                // `pkg/subpath` case
116                Cow::Owned(format!("{hashed_package}/{subpath}"))
117            } else {
118                // `pkg` case
119                Cow::Owned(hashed_package)
120            }
121        } else {
122            Cow::Borrowed(&*self.request)
123        }
124    }
125}
126
127#[turbo_tasks::value_impl]
128impl CachedExternalModule {
129    #[turbo_tasks::function]
130    pub fn new(
131        request: RcStr,
132        target: Option<FileSystemPath>,
133        external_type: CachedExternalType,
134        analyze_mode: CachedExternalTracingMode,
135    ) -> Vc<Self> {
136        Self::cell(CachedExternalModule {
137            request,
138            target,
139            external_type,
140            analyze_mode,
141        })
142    }
143
144    #[turbo_tasks::function]
145    pub fn content(&self) -> Result<Vc<EcmascriptModuleContent>> {
146        let mut code = RopeBuilder::default();
147
148        match self.external_type {
149            CachedExternalType::EcmaScriptViaImport => {
150                writeln!(
151                    code,
152                    "var mod = await {TURBOPACK_EXTERNAL_IMPORT}({});",
153                    StringifyJs(&self.request())
154                )?;
155            }
156            CachedExternalType::EcmaScriptViaRequire | CachedExternalType::CommonJs => {
157                let request = self.request();
158                writeln!(
159                    code,
160                    "var mod = {TURBOPACK_EXTERNAL_REQUIRE}({}, () => require({}));",
161                    StringifyJs(&request),
162                    StringifyJs(&request)
163                )?;
164            }
165            CachedExternalType::Global => {
166                if self.request.is_empty() {
167                    writeln!(code, "var mod = {{}};")?;
168                } else {
169                    writeln!(
170                        code,
171                        "var mod = globalThis[{}];",
172                        StringifyJs(&self.request)
173                    )?;
174                }
175            }
176            CachedExternalType::Script => {
177                // Parse the request format: "variableName@url"
178                // e.g., "foo@https://test.test.com"
179                if let Some(at_index) = self.request.find('@') {
180                    let variable_name = &self.request[..at_index];
181                    let url = &self.request[at_index + 1..];
182
183                    // Wrap the loading and variable access in a try-catch block
184                    writeln!(code, "var mod;")?;
185                    writeln!(code, "try {{")?;
186
187                    // First load the URL
188                    writeln!(
189                        code,
190                        "  await {TURBOPACK_LOAD_BY_URL}({});",
191                        StringifyJs(url)
192                    )?;
193
194                    // Then get the variable from global with existence check
195                    writeln!(
196                        code,
197                        "  if (typeof global[{}] === 'undefined') {{",
198                        StringifyJs(variable_name)
199                    )?;
200                    writeln!(
201                        code,
202                        "    throw new Error('Variable {} is not available on global object after \
203                         loading {}');",
204                        StringifyJs(variable_name),
205                        StringifyJs(url)
206                    )?;
207                    writeln!(code, "  }}")?;
208                    writeln!(code, "  mod = global[{}];", StringifyJs(variable_name))?;
209
210                    // Catch and re-throw errors with more context
211                    writeln!(code, "}} catch (error) {{")?;
212                    writeln!(
213                        code,
214                        "  throw new Error('Failed to load external URL module {}: ' + \
215                         (error.message || error));",
216                        StringifyJs(&self.request)
217                    )?;
218                    writeln!(code, "}}")?;
219                } else {
220                    // Invalid format - throw error
221                    writeln!(
222                        code,
223                        "throw new Error('Invalid URL external format. Expected \"variable@url\", \
224                         got: {}');",
225                        StringifyJs(&self.request)
226                    )?;
227                    writeln!(code, "var mod = undefined;")?;
228                }
229            }
230        }
231
232        writeln!(code)?;
233
234        if self.external_type == CachedExternalType::CommonJs {
235            writeln!(code, "module.exports = mod;")?;
236        } else if self.external_type == CachedExternalType::EcmaScriptViaImport
237            || self.external_type == CachedExternalType::EcmaScriptViaRequire
238        {
239            writeln!(code, "{TURBOPACK_EXPORT_NAMESPACE}(mod);")?;
240        } else {
241            writeln!(code, "{TURBOPACK_EXPORT_VALUE}(mod);")?;
242        }
243
244        Ok(EcmascriptModuleContent {
245            inner_code: code.build(),
246            source_map: None,
247            is_esm: self.external_type != CachedExternalType::CommonJs,
248            strict: false,
249            additional_ids: Default::default(),
250        }
251        .cell())
252    }
253}
254
255/// A separate turbotask to create only a single VirtualFileSystem
256#[turbo_tasks::function]
257fn externals_fs_root() -> Vc<FileSystemPath> {
258    VirtualFileSystem::new_with_name(rcstr!("externals")).root()
259}
260
261#[turbo_tasks::value_impl]
262impl Module for CachedExternalModule {
263    #[turbo_tasks::function]
264    async fn ident(&self) -> Result<Vc<AssetIdent>> {
265        let mut ident = AssetIdent::from_path(externals_fs_root().await?.join(&self.request)?)
266            .with_layer(Layer::new(rcstr!("external")))
267            .with_modifier(self.request.clone())
268            .with_modifier(self.external_type.to_string().into());
269
270        if let Some(target) = &self.target {
271            ident = ident.with_modifier(target.to_string_ref().await?);
272        }
273
274        Ok(ident.into_vc())
275    }
276
277    #[turbo_tasks::function]
278    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
279        Vc::cell(None)
280    }
281
282    #[turbo_tasks::function]
283    async fn references(&self) -> Result<Vc<ModuleReferences>> {
284        Ok(match &self.analyze_mode {
285            CachedExternalTracingMode::Untraced => ModuleReferences::empty(),
286            CachedExternalTracingMode::Traced { origin } => {
287                let external_result = match self.external_type {
288                    CachedExternalType::EcmaScriptViaImport => {
289                        esm_resolve(
290                            **origin,
291                            Request::parse_string(self.request.clone()),
292                            Default::default(),
293                            ResolveErrorMode::Error,
294                            None,
295                        )
296                        .await?
297                        .await?
298                    }
299                    CachedExternalType::CommonJs | CachedExternalType::EcmaScriptViaRequire => {
300                        cjs_resolve(
301                            **origin,
302                            Request::parse_string(self.request.clone()),
303                            Default::default(),
304                            None,
305                            ResolveErrorMode::Error,
306                        )
307                        .await?
308                    }
309                    CachedExternalType::Global | CachedExternalType::Script => {
310                        let resolve_options = origin.into_trait_ref().await?.resolve_options();
311                        origin
312                            .resolve_asset(
313                                Request::parse_string(self.request.clone()),
314                                resolve_options,
315                                ReferenceType::Undefined,
316                            )
317                            .await?
318                            .await?
319                    }
320                };
321
322                let references = external_result
323                    .affecting_sources
324                    .iter()
325                    .map(|s| {
326                        // Add a modifier
327                        // it is possible to reference a module as an affecting source and as Module
328                        // so this will distinguish them
329                        Vc::upcast::<Box<dyn Module>>(RawModule::new_with_modifier(
330                            **s,
331                            rcstr!("affecting source"),
332                        ))
333                    })
334                    .chain(external_result.primary_modules_raw_iter().map(|m| *m))
335                    .map(|s| {
336                        Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(
337                            s,
338                            TracedMode::Entry,
339                        ))
340                        .to_resolved()
341                    })
342                    .try_join()
343                    .await?;
344                Vc::cell(references)
345            }
346        })
347    }
348
349    #[turbo_tasks::function]
350    fn is_self_async(&self) -> Result<Vc<bool>> {
351        Ok(Vc::cell(
352            self.external_type == CachedExternalType::EcmaScriptViaImport
353                || self.external_type == CachedExternalType::Script,
354        ))
355    }
356
357    #[turbo_tasks::function]
358    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
359        ModuleSideEffects::SideEffectful.cell()
360    }
361}
362
363#[turbo_tasks::value_impl]
364impl ChunkableModule for CachedExternalModule {
365    #[turbo_tasks::function]
366    fn as_chunk_item(
367        self: ResolvedVc<Self>,
368        module_graph: ResolvedVc<ModuleGraph>,
369        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
370    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
371        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
372    }
373}
374
375#[turbo_tasks::value_impl]
376impl EcmascriptChunkPlaceable for CachedExternalModule {
377    #[turbo_tasks::function]
378    fn get_exports(&self) -> Vc<EcmascriptExports> {
379        if self.external_type == CachedExternalType::CommonJs {
380            EcmascriptExports::CommonJs.cell()
381        } else {
382            EcmascriptExports::DynamicNamespace.cell()
383        }
384    }
385
386    #[turbo_tasks::function]
387    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
388        Vc::cell(
389            if self.external_type == CachedExternalType::EcmaScriptViaImport
390                || self.external_type == CachedExternalType::Script
391            {
392                Some(
393                    AsyncModule {
394                        has_top_level_await: true,
395                        import_externals: self.external_type
396                            == CachedExternalType::EcmaScriptViaImport,
397                    }
398                    .resolved_cell(),
399                )
400            } else {
401                None
402            },
403        )
404    }
405
406    #[turbo_tasks::function]
407    fn chunk_item_content(
408        self: Vc<Self>,
409        chunking_context: Vc<Box<dyn ChunkingContext>>,
410        _module_graph: Vc<ModuleGraph>,
411        async_module_info: Option<Vc<AsyncModuleInfo>>,
412        _estimated: bool,
413    ) -> Vc<EcmascriptChunkItemContent> {
414        let async_module_options = self.get_async_module().module_options(async_module_info);
415
416        EcmascriptChunkItemContent::new(self.content(), chunking_context, async_module_options)
417    }
418
419    #[turbo_tasks::function]
420    async fn chunk_item_output_assets(
421        self: Vc<Self>,
422        chunking_context: Vc<Box<dyn ChunkingContext>>,
423        _module_graph: Vc<ModuleGraph>,
424    ) -> Result<Vc<OutputAssetsWithReferenced>> {
425        let module = self.await?;
426        let chunking_context_resolved = chunking_context.to_resolved().await?;
427        let assets = if let Some(target) = &module.target {
428            ResolvedVc::cell(vec![ResolvedVc::upcast(
429                ExternalsSymlinkAsset::new(
430                    *chunking_context_resolved,
431                    hashed_package_name(target).into(),
432                    target.clone(),
433                )
434                .to_resolved()
435                .await?,
436            )])
437        } else {
438            OutputAssets::empty_resolved()
439        };
440        Ok(OutputAssetsWithReferenced {
441            assets,
442            referenced_assets: OutputAssets::empty_resolved(),
443            references: OutputAssetsReferences::empty_resolved(),
444        }
445        .cell())
446    }
447}
448
449#[derive(Debug)]
450#[turbo_tasks::value(shared)]
451pub struct ExternalsSymlinkAsset {
452    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
453    hashed_package: RcStr,
454    target: FileSystemPath,
455}
456#[turbo_tasks::value_impl]
457impl ExternalsSymlinkAsset {
458    #[turbo_tasks::function]
459    pub fn new(
460        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
461        hashed_package: RcStr,
462        target: FileSystemPath,
463    ) -> Vc<Self> {
464        ExternalsSymlinkAsset {
465            chunking_context,
466            hashed_package,
467            target,
468        }
469        .cell()
470    }
471}
472#[turbo_tasks::value_impl]
473impl OutputAssetsReference for ExternalsSymlinkAsset {}
474
475#[turbo_tasks::value_impl]
476impl OutputAsset for ExternalsSymlinkAsset {
477    #[turbo_tasks::function]
478    async fn path(&self) -> Result<Vc<FileSystemPath>> {
479        Ok(self
480            .chunking_context
481            .output_root()
482            .await?
483            .join("node_modules")?
484            .join(&self.hashed_package)?
485            .cell())
486    }
487}
488
489#[turbo_tasks::value_impl]
490impl Asset for ExternalsSymlinkAsset {
491    #[turbo_tasks::function]
492    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
493        let this = self.await?;
494        // path: [output]/bench/app-router-server/.next/node_modules/lodash-ee4fa714b6d81ca3
495        // target: [project]/node_modules/.pnpm/lodash@3.10.1/node_modules/lodash
496
497        let output_root_to_project_root = this.chunking_context.output_root_to_root_path().await?;
498        let project_root_to_target = &this.target.path;
499
500        let path = self.path().await?;
501        let path_to_output_root = path
502            .parent()
503            .get_relative_path_to(&*this.chunking_context.output_root().await?)
504            .context("path must be inside output root")?;
505
506        let target = format!(
507            "{path_to_output_root}/{output_root_to_project_root}/{project_root_to_target}",
508        )
509        .into();
510
511        Ok(AssetContent::Redirect {
512            target,
513            link_type: LinkType::DIRECTORY,
514        }
515        .cell())
516    }
517}