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