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::{
8    FileSystem, FileSystemPath, VirtualFileSystem, WriteLinkContent, WriteLinkTarget,
9    WriteLinkTargetType, rope::RopeBuilder,
10};
11use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
12use turbopack_core::{
13    asset::{Asset, AssetContent},
14    chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, TracedMode},
15    ident::{AssetIdent, Layer},
16    module::{Module, ModuleSideEffects},
17    module_graph::ModuleGraph,
18    output::{
19        OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsReferences,
20        OutputAssetsWithReferenced,
21    },
22    raw_module::RawModule,
23    reference::{ModuleReference, ModuleReferences, TracedModuleReference},
24    reference_type::ReferenceType,
25    resolve::{
26        ResolveErrorMode,
27        origin::{ResolveOrigin, ResolveOriginExt},
28        parse::Request,
29    },
30};
31use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
32
33use crate::{
34    EcmascriptModuleContent,
35    chunk::{
36        EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
37        ecmascript_chunk_item,
38    },
39    references::async_module::{AsyncModule, OptionAsyncModule},
40    runtime_functions::{
41        TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
42        TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
43    },
44    utils::StringifyJs,
45};
46
47#[turbo_tasks::task_input]
48#[derive(Copy, Clone, Debug, Eq, PartialEq, TraceRawVcs, Hash, Encode, Decode)]
49pub enum CachedExternalType {
50    CommonJs,
51    EcmaScriptViaRequire,
52    EcmaScriptViaImport,
53    Global,
54    Script,
55}
56
57#[turbo_tasks::task_input]
58#[derive(Clone, Debug, Eq, PartialEq, TraceRawVcs, Hash, Encode, Decode)]
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                    "var 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                    "var 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, "var mod = {{}};")?;
171                } else {
172                    writeln!(
173                        code,
174                        "var 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, "var 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, "var 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.to_string_ref().await?);
275        }
276
277        Ok(ident.into_vc())
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                            ResolveErrorMode::Error,
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                            ResolveErrorMode::Error,
309                        )
310                        .await?
311                    }
312                    CachedExternalType::Global | CachedExternalType::Script => {
313                        let resolve_options = origin.into_trait_ref().await?.resolve_options();
314                        origin
315                            .resolve_asset(
316                                Request::parse_string(self.request.clone()),
317                                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(external_result.primary_modules_raw_iter().map(|m| *m))
338                    .map(|s| {
339                        Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(
340                            s,
341                            TracedMode::Entry,
342                        ))
343                        .to_resolved()
344                    })
345                    .try_join()
346                    .await?;
347                Vc::cell(references)
348            }
349        })
350    }
351
352    #[turbo_tasks::function]
353    fn is_self_async(&self) -> Result<Vc<bool>> {
354        Ok(Vc::cell(
355            self.external_type == CachedExternalType::EcmaScriptViaImport
356                || self.external_type == CachedExternalType::Script,
357        ))
358    }
359
360    #[turbo_tasks::function]
361    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
362        ModuleSideEffects::SideEffectful.cell()
363    }
364}
365
366#[turbo_tasks::value_impl]
367impl ChunkableModule for CachedExternalModule {
368    #[turbo_tasks::function]
369    fn as_chunk_item(
370        self: ResolvedVc<Self>,
371        module_graph: ResolvedVc<ModuleGraph>,
372        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
373    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
374        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
375    }
376}
377
378#[turbo_tasks::value_impl]
379impl EcmascriptChunkPlaceable for CachedExternalModule {
380    #[turbo_tasks::function]
381    fn get_exports(&self) -> Vc<EcmascriptExports> {
382        if self.external_type == CachedExternalType::CommonJs {
383            EcmascriptExports::CommonJs(None).cell()
384        } else {
385            EcmascriptExports::DynamicNamespace.cell()
386        }
387    }
388
389    #[turbo_tasks::function]
390    fn get_async_module(&self) -> Vc<OptionAsyncModule> {
391        Vc::cell(
392            if self.external_type == CachedExternalType::EcmaScriptViaImport
393                || self.external_type == CachedExternalType::Script
394            {
395                Some(
396                    AsyncModule {
397                        has_top_level_await: true,
398                        import_externals: self.external_type
399                            == CachedExternalType::EcmaScriptViaImport,
400                    }
401                    .resolved_cell(),
402                )
403            } else {
404                None
405            },
406        )
407    }
408
409    #[turbo_tasks::function]
410    fn chunk_item_content(
411        self: Vc<Self>,
412        chunking_context: Vc<Box<dyn ChunkingContext>>,
413        _module_graph: Vc<ModuleGraph>,
414        async_module_info: Option<Vc<AsyncModuleInfo>>,
415        _estimated: bool,
416    ) -> Vc<EcmascriptChunkItemContent> {
417        let async_module_options = self.get_async_module().module_options(async_module_info);
418
419        EcmascriptChunkItemContent::new(self.content(), chunking_context, async_module_options)
420    }
421
422    #[turbo_tasks::function]
423    async fn chunk_item_output_assets(
424        self: Vc<Self>,
425        chunking_context: Vc<Box<dyn ChunkingContext>>,
426        _module_graph: Vc<ModuleGraph>,
427    ) -> Result<Vc<OutputAssetsWithReferenced>> {
428        let module = self.await?;
429        let chunking_context_resolved = chunking_context.to_resolved().await?;
430        let assets = if let Some(target) = &module.target {
431            ResolvedVc::cell(vec![ResolvedVc::upcast(
432                ExternalsSymlinkAsset::new(
433                    *chunking_context_resolved,
434                    hashed_package_name(target).into(),
435                    target.clone(),
436                )
437                .to_resolved()
438                .await?,
439            )])
440        } else {
441            OutputAssets::empty_resolved()
442        };
443        Ok(OutputAssetsWithReferenced {
444            assets,
445            referenced_assets: OutputAssets::empty_resolved(),
446            references: OutputAssetsReferences::empty_resolved(),
447        }
448        .cell())
449    }
450}
451
452#[derive(Debug)]
453#[turbo_tasks::value(shared)]
454pub struct ExternalsSymlinkAsset {
455    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
456    hashed_package: RcStr,
457    target: FileSystemPath,
458}
459#[turbo_tasks::value_impl]
460impl ExternalsSymlinkAsset {
461    #[turbo_tasks::function]
462    pub fn new(
463        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
464        hashed_package: RcStr,
465        target: FileSystemPath,
466    ) -> Vc<Self> {
467        ExternalsSymlinkAsset {
468            chunking_context,
469            hashed_package,
470            target,
471        }
472        .cell()
473    }
474}
475#[turbo_tasks::value_impl]
476impl OutputAssetsReference for ExternalsSymlinkAsset {}
477
478#[turbo_tasks::value_impl]
479impl OutputAsset for ExternalsSymlinkAsset {
480    #[turbo_tasks::function]
481    async fn path(&self) -> Result<Vc<FileSystemPath>> {
482        Ok(self
483            .chunking_context
484            .output_root()
485            .await?
486            .join("node_modules")?
487            .join(&self.hashed_package)?
488            .cell())
489    }
490}
491
492#[turbo_tasks::value_impl]
493impl Asset for ExternalsSymlinkAsset {
494    #[turbo_tasks::function]
495    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
496        let this = self.await?;
497        // path: [output]/bench/app-router-server/.next/node_modules/lodash-ee4fa714b6d81ca3
498        // target: [project]/node_modules/.pnpm/lodash@3.10.1/node_modules/lodash
499
500        let output_root_to_project_root = this.chunking_context.output_root_to_root_path().await?;
501        let project_root_to_target = &this.target.path;
502
503        let path = self.path().await?;
504        let path_to_output_root = path
505            .parent()
506            .get_relative_path_to(&*this.chunking_context.output_root().await?)
507            .context("path must be inside output root")?;
508
509        let target = format!(
510            "{path_to_output_root}/{output_root_to_project_root}/{project_root_to_target}",
511        )
512        .into();
513
514        Ok(AssetContent::Redirect(WriteLinkContent {
515            target: WriteLinkTarget::Relative(target),
516            target_type: WriteLinkTargetType::DirectoryOrJunctionPoint,
517        })
518        .cell())
519    }
520}