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