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