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, ChunkItem, ChunkType, 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        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
34        EcmascriptChunkType, EcmascriptExports,
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| Vc::upcast::<Box<dyn Module>>(RawModule::new(**s)))
327                    .chain(
328                        external_result
329                            .primary_modules_raw_iter()
330                            // These modules aren't bundled but still need to be part of the module
331                            // graph for chunking. `compute_async_module_info` computes
332                            // `is_self_async` for every module, but at least for traced modules,
333                            // that value is never used as `ChunkingType::Traced.is_inherit_async()
334                            // == false`. Optimize this case by using
335                            // `SideEffectfulModuleWithoutSelfAsync` to
336                            // short circuit that computation and thus defer parsing traced modules
337                            // to emitting to not block all of chunking on this.
338                            .map(|m| Vc::upcast(SideEffectfulModuleWithoutSelfAsync::new(*m))),
339                    )
340                    .map(|s| {
341                        Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(s))
342                            .to_resolved()
343                    })
344                    .try_join()
345                    .await?;
346                Vc::cell(references)
347            }
348        })
349    }
350
351    #[turbo_tasks::function]
352    fn is_self_async(&self) -> Result<Vc<bool>> {
353        Ok(Vc::cell(
354            self.external_type == CachedExternalType::EcmaScriptViaImport
355                || self.external_type == CachedExternalType::Script,
356        ))
357    }
358
359    #[turbo_tasks::function]
360    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
361        ModuleSideEffects::SideEffectful.cell()
362    }
363}
364
365#[turbo_tasks::value_impl]
366impl ChunkableModule for CachedExternalModule {
367    #[turbo_tasks::function]
368    fn as_chunk_item(
369        self: ResolvedVc<Self>,
370        _module_graph: Vc<ModuleGraph>,
371        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
372    ) -> Vc<Box<dyn ChunkItem>> {
373        Vc::upcast(
374            CachedExternalModuleChunkItem {
375                module: self,
376                chunking_context,
377            }
378            .cell(),
379        )
380    }
381}
382
383#[turbo_tasks::value_impl]
384impl EcmascriptChunkPlaceable for CachedExternalModule {
385    #[turbo_tasks::function]
386    fn get_exports(&self) -> Vc<EcmascriptExports> {
387        if self.external_type == CachedExternalType::CommonJs {
388            EcmascriptExports::CommonJs.cell()
389        } else {
390            EcmascriptExports::DynamicNamespace.cell()
391        }
392    }
393
394    #[turbo_tasks::function]
395    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
396        Vc::cell(
397            if self.external_type == CachedExternalType::EcmaScriptViaImport
398                || self.external_type == CachedExternalType::Script
399            {
400                Some(
401                    AsyncModule {
402                        has_top_level_await: true,
403                        import_externals: self.external_type
404                            == CachedExternalType::EcmaScriptViaImport,
405                    }
406                    .resolved_cell(),
407                )
408            } else {
409                None
410            },
411        )
412    }
413}
414
415#[turbo_tasks::value]
416pub struct CachedExternalModuleChunkItem {
417    module: ResolvedVc<CachedExternalModule>,
418    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
419}
420
421#[turbo_tasks::value_impl]
422impl OutputAssetsReference for CachedExternalModuleChunkItem {
423    #[turbo_tasks::function]
424    async fn references(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
425        let module = self.module.await?;
426        let assets = if let Some(target) = &module.target {
427            ResolvedVc::cell(vec![ResolvedVc::upcast(
428                ExternalsSymlinkAsset::new(
429                    *self.chunking_context,
430                    hashed_package_name(target).into(),
431                    module.target.clone().unwrap(),
432                )
433                .to_resolved()
434                .await?,
435            )])
436        } else {
437            OutputAssets::empty_resolved()
438        };
439        Ok(OutputAssetsWithReferenced {
440            assets,
441            referenced_assets: OutputAssets::empty_resolved(),
442            references: OutputAssetsReferences::empty_resolved(),
443        }
444        .cell())
445    }
446}
447
448#[turbo_tasks::value_impl]
449impl ChunkItem for CachedExternalModuleChunkItem {
450    #[turbo_tasks::function]
451    fn asset_ident(&self) -> Vc<AssetIdent> {
452        self.module.ident()
453    }
454
455    #[turbo_tasks::function]
456    fn ty(self: Vc<Self>) -> Vc<Box<dyn ChunkType>> {
457        Vc::upcast(Vc::<EcmascriptChunkType>::default())
458    }
459
460    #[turbo_tasks::function]
461    fn module(&self) -> Vc<Box<dyn Module>> {
462        Vc::upcast(*self.module)
463    }
464
465    #[turbo_tasks::function]
466    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
467        *self.chunking_context
468    }
469}
470
471#[turbo_tasks::value_impl]
472impl EcmascriptChunkItem for CachedExternalModuleChunkItem {
473    #[turbo_tasks::function]
474    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
475        panic!("content() should not be called");
476    }
477
478    #[turbo_tasks::function]
479    fn content_with_async_module_info(
480        &self,
481        async_module_info: Option<Vc<AsyncModuleInfo>>,
482        _estimated: bool,
483    ) -> Vc<EcmascriptChunkItemContent> {
484        let async_module_options = self
485            .module
486            .get_async_module()
487            .module_options(async_module_info);
488
489        EcmascriptChunkItemContent::new(
490            self.module.content(),
491            *self.chunking_context,
492            async_module_options,
493        )
494    }
495}
496
497/// A wrapper "passthrough" module type that always returns `false` for `is_self_async` and
498/// `SideEffects` for `side_effects`.Be careful when using it, as it may hide async dependencies.
499#[turbo_tasks::value]
500struct SideEffectfulModuleWithoutSelfAsync {
501    module: ResolvedVc<Box<dyn Module>>,
502}
503
504#[turbo_tasks::value_impl]
505impl SideEffectfulModuleWithoutSelfAsync {
506    #[turbo_tasks::function]
507    fn new(module: ResolvedVc<Box<dyn Module>>) -> Vc<Self> {
508        Self::cell(SideEffectfulModuleWithoutSelfAsync { module })
509    }
510}
511
512#[turbo_tasks::value_impl]
513impl Module for SideEffectfulModuleWithoutSelfAsync {
514    #[turbo_tasks::function]
515    fn ident(&self) -> Vc<AssetIdent> {
516        self.module.ident()
517    }
518
519    #[turbo_tasks::function]
520    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
521        self.module.source()
522    }
523
524    #[turbo_tasks::function]
525    fn references(&self) -> Vc<ModuleReferences> {
526        self.module.references()
527    }
528
529    #[turbo_tasks::function]
530    fn side_effects(&self) -> Vc<ModuleSideEffects> {
531        ModuleSideEffects::SideEffectful.cell()
532    }
533    // Don't override and use default is_self_async that always returns false
534}
535
536#[derive(Debug)]
537#[turbo_tasks::value(shared)]
538pub struct ExternalsSymlinkAsset {
539    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
540    hashed_package: RcStr,
541    target: FileSystemPath,
542}
543#[turbo_tasks::value_impl]
544impl ExternalsSymlinkAsset {
545    #[turbo_tasks::function]
546    pub fn new(
547        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
548        hashed_package: RcStr,
549        target: FileSystemPath,
550    ) -> Vc<Self> {
551        ExternalsSymlinkAsset {
552            chunking_context,
553            hashed_package,
554            target,
555        }
556        .cell()
557    }
558}
559#[turbo_tasks::value_impl]
560impl OutputAssetsReference for ExternalsSymlinkAsset {}
561
562#[turbo_tasks::value_impl]
563impl OutputAsset for ExternalsSymlinkAsset {
564    #[turbo_tasks::function]
565    async fn path(&self) -> Result<Vc<FileSystemPath>> {
566        Ok(self
567            .chunking_context
568            .output_root()
569            .await?
570            .join("node_modules")?
571            .join(&self.hashed_package)?
572            .cell())
573    }
574}
575
576#[turbo_tasks::value_impl]
577impl Asset for ExternalsSymlinkAsset {
578    #[turbo_tasks::function]
579    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
580        let this = self.await?;
581        // path: [output]/bench/app-router-server/.next/node_modules/lodash-ee4fa714b6d81ca3
582        // target: [project]/node_modules/.pnpm/lodash@3.10.1/node_modules/lodash
583
584        let output_root_to_project_root = this.chunking_context.output_root_to_root_path().await?;
585        let project_root_to_target = &this.target.path;
586
587        let path = self.path().await?;
588        let path_to_output_root = path
589            .parent()
590            .get_relative_path_to(&*this.chunking_context.output_root().await?)
591            .context("path must be inside output root")?;
592
593        let target = format!(
594            "{path_to_output_root}/{output_root_to_project_root}/{project_root_to_target}",
595        )
596        .into();
597
598        Ok(AssetContent::Redirect {
599            target,
600            link_type: LinkType::DIRECTORY,
601        }
602        .cell())
603    }
604}