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)]
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| Vc::upcast::<Box<dyn Module>>(RawModule::new(**s)))
327 .chain(
328 external_result
329 .primary_modules_raw_iter()
330 .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#[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 }
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 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}