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)]
58pub 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
87fn 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 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 self.request.split_once('/').unwrap().1
112 } else {
113 &*self.request
114 };
115
116 if let Some((_, subpath)) = request.split_once('/') {
117 Cow::Owned(format!("{hashed_package}/{subpath}"))
119 } else {
120 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 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 writeln!(code, "let mod;")?;
187 writeln!(code, "try {{")?;
188
189 writeln!(
191 code,
192 " await {TURBOPACK_LOAD_BY_URL}({});",
193 StringifyJs(url)
194 )?;
195
196 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 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 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#[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 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 .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#[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 }
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 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}