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