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::{
7 NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, ValueToStringRef, Vc, trace::TraceRawVcs,
8};
9use turbo_tasks_fs::{FileSystem, FileSystemPath, LinkType, VirtualFileSystem, rope::RopeBuilder};
10use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
11use turbopack_core::{
12 asset::{Asset, AssetContent},
13 chunk::{AsyncModuleInfo, 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 ResolveErrorMode,
26 origin::{ResolveOrigin, ResolveOriginExt},
27 parse::Request,
28 },
29};
30use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
31
32use crate::{
33 EcmascriptModuleContent,
34 chunk::{
35 EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
36 ecmascript_chunk_item,
37 },
38 references::async_module::{AsyncModule, OptionAsyncModule},
39 runtime_functions::{
40 TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXTERNAL_IMPORT,
41 TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_LOAD_BY_URL,
42 },
43 utils::StringifyJs,
44};
45
46#[derive(
47 Copy, Clone, Debug, Eq, PartialEq, TraceRawVcs, TaskInput, Hash, NonLocalValue, Encode, Decode,
48)]
49pub enum CachedExternalType {
50 CommonJs,
51 EcmaScriptViaRequire,
52 EcmaScriptViaImport,
53 Global,
54 Script,
55}
56
57#[derive(
58 Clone, Debug, Eq, PartialEq, TraceRawVcs, TaskInput, Hash, NonLocalValue, Encode, Decode,
59)]
60pub enum CachedExternalTracingMode {
63 Untraced,
64 Traced {
65 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
66 },
67}
68
69impl Display for CachedExternalType {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 CachedExternalType::CommonJs => write!(f, "cjs"),
73 CachedExternalType::EcmaScriptViaRequire => write!(f, "esm_require"),
74 CachedExternalType::EcmaScriptViaImport => write!(f, "esm_import"),
75 CachedExternalType::Global => write!(f, "global"),
76 CachedExternalType::Script => write!(f, "script"),
77 }
78 }
79}
80
81#[turbo_tasks::value]
82pub struct CachedExternalModule {
83 request: RcStr,
84 target: Option<FileSystemPath>,
85 external_type: CachedExternalType,
86 analyze_mode: CachedExternalTracingMode,
87}
88
89fn hashed_package_name(folder: &FileSystemPath) -> String {
93 let hash = encode_hex(hash_xxh3_hash64(&folder.path));
94
95 let parent = folder.parent();
96 let parent = parent.file_name();
97 let pkg = folder.file_name();
98 if parent.starts_with('@') {
99 format!("{parent}/{pkg}-{hash}")
100 } else {
101 format!("{pkg}-{hash}")
102 }
103}
104
105impl CachedExternalModule {
106 pub fn request(&self) -> Cow<'_, str> {
108 if let Some(target) = &self.target {
109 let hashed_package = hashed_package_name(target);
110
111 let request = if self.request.starts_with('@') {
112 self.request.split_once('/').unwrap().1
114 } else {
115 &*self.request
116 };
117
118 if let Some((_, subpath)) = request.split_once('/') {
119 Cow::Owned(format!("{hashed_package}/{subpath}"))
121 } else {
122 Cow::Owned(hashed_package)
124 }
125 } else {
126 Cow::Borrowed(&*self.request)
127 }
128 }
129}
130
131#[turbo_tasks::value_impl]
132impl CachedExternalModule {
133 #[turbo_tasks::function]
134 pub fn new(
135 request: RcStr,
136 target: Option<FileSystemPath>,
137 external_type: CachedExternalType,
138 analyze_mode: CachedExternalTracingMode,
139 ) -> Vc<Self> {
140 Self::cell(CachedExternalModule {
141 request,
142 target,
143 external_type,
144 analyze_mode,
145 })
146 }
147
148 #[turbo_tasks::function]
149 pub fn content(&self) -> Result<Vc<EcmascriptModuleContent>> {
150 let mut code = RopeBuilder::default();
151
152 match self.external_type {
153 CachedExternalType::EcmaScriptViaImport => {
154 writeln!(
155 code,
156 "var mod = await {TURBOPACK_EXTERNAL_IMPORT}({});",
157 StringifyJs(&self.request())
158 )?;
159 }
160 CachedExternalType::EcmaScriptViaRequire | CachedExternalType::CommonJs => {
161 let request = self.request();
162 writeln!(
163 code,
164 "var mod = {TURBOPACK_EXTERNAL_REQUIRE}({}, () => require({}));",
165 StringifyJs(&request),
166 StringifyJs(&request)
167 )?;
168 }
169 CachedExternalType::Global => {
170 if self.request.is_empty() {
171 writeln!(code, "var mod = {{}};")?;
172 } else {
173 writeln!(
174 code,
175 "var mod = globalThis[{}];",
176 StringifyJs(&self.request)
177 )?;
178 }
179 }
180 CachedExternalType::Script => {
181 if let Some(at_index) = self.request.find('@') {
184 let variable_name = &self.request[..at_index];
185 let url = &self.request[at_index + 1..];
186
187 writeln!(code, "var mod;")?;
189 writeln!(code, "try {{")?;
190
191 writeln!(
193 code,
194 " await {TURBOPACK_LOAD_BY_URL}({});",
195 StringifyJs(url)
196 )?;
197
198 writeln!(
200 code,
201 " if (typeof global[{}] === 'undefined') {{",
202 StringifyJs(variable_name)
203 )?;
204 writeln!(
205 code,
206 " throw new Error('Variable {} is not available on global object after \
207 loading {}');",
208 StringifyJs(variable_name),
209 StringifyJs(url)
210 )?;
211 writeln!(code, " }}")?;
212 writeln!(code, " mod = global[{}];", StringifyJs(variable_name))?;
213
214 writeln!(code, "}} catch (error) {{")?;
216 writeln!(
217 code,
218 " throw new Error('Failed to load external URL module {}: ' + \
219 (error.message || error));",
220 StringifyJs(&self.request)
221 )?;
222 writeln!(code, "}}")?;
223 } else {
224 writeln!(
226 code,
227 "throw new Error('Invalid URL external format. Expected \"variable@url\", \
228 got: {}');",
229 StringifyJs(&self.request)
230 )?;
231 writeln!(code, "var mod = undefined;")?;
232 }
233 }
234 }
235
236 writeln!(code)?;
237
238 if self.external_type == CachedExternalType::CommonJs {
239 writeln!(code, "module.exports = mod;")?;
240 } else if self.external_type == CachedExternalType::EcmaScriptViaImport
241 || self.external_type == CachedExternalType::EcmaScriptViaRequire
242 {
243 writeln!(code, "{TURBOPACK_EXPORT_NAMESPACE}(mod);")?;
244 } else {
245 writeln!(code, "{TURBOPACK_EXPORT_VALUE}(mod);")?;
246 }
247
248 Ok(EcmascriptModuleContent {
249 inner_code: code.build(),
250 source_map: None,
251 is_esm: self.external_type != CachedExternalType::CommonJs,
252 strict: false,
253 additional_ids: Default::default(),
254 }
255 .cell())
256 }
257}
258
259#[turbo_tasks::function]
261fn externals_fs_root() -> Vc<FileSystemPath> {
262 VirtualFileSystem::new_with_name(rcstr!("externals")).root()
263}
264
265#[turbo_tasks::value_impl]
266impl Module for CachedExternalModule {
267 #[turbo_tasks::function]
268 async fn ident(&self) -> Result<Vc<AssetIdent>> {
269 let mut ident = AssetIdent::from_path(externals_fs_root().await?.join(&self.request)?)
270 .with_layer(Layer::new(rcstr!("external")))
271 .with_modifier(self.request.clone())
272 .with_modifier(self.external_type.to_string().into());
273
274 if let Some(target) = &self.target {
275 ident = ident.with_modifier(target.to_string_ref().await?);
276 }
277
278 Ok(ident.into_vc())
279 }
280
281 #[turbo_tasks::function]
282 fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
283 Vc::cell(None)
284 }
285
286 #[turbo_tasks::function]
287 async fn references(&self) -> Result<Vc<ModuleReferences>> {
288 Ok(match &self.analyze_mode {
289 CachedExternalTracingMode::Untraced => ModuleReferences::empty(),
290 CachedExternalTracingMode::Traced { origin } => {
291 let external_result = match self.external_type {
292 CachedExternalType::EcmaScriptViaImport => {
293 esm_resolve(
294 **origin,
295 Request::parse_string(self.request.clone()),
296 Default::default(),
297 ResolveErrorMode::Error,
298 None,
299 )
300 .await?
301 .await?
302 }
303 CachedExternalType::CommonJs | CachedExternalType::EcmaScriptViaRequire => {
304 cjs_resolve(
305 **origin,
306 Request::parse_string(self.request.clone()),
307 Default::default(),
308 None,
309 ResolveErrorMode::Error,
310 )
311 .await?
312 }
313 CachedExternalType::Global | CachedExternalType::Script => {
314 origin
315 .resolve_asset(
316 Request::parse_string(self.request.clone()),
317 origin.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 Vc::upcast::<Box<dyn Module>>(RawModule::new_with_modifier(
333 **s,
334 rcstr!("affecting source"),
335 ))
336 })
337 .chain(
338 external_result
339 .primary_modules_raw_iter()
340 .map(|m| Vc::upcast(SideEffectfulModuleWithoutSelfAsync::new(*m))),
349 )
350 .map(|s| {
351 Vc::upcast::<Box<dyn ModuleReference>>(TracedModuleReference::new(s))
352 .to_resolved()
353 })
354 .try_join()
355 .await?;
356 Vc::cell(references)
357 }
358 })
359 }
360
361 #[turbo_tasks::function]
362 fn is_self_async(&self) -> Result<Vc<bool>> {
363 Ok(Vc::cell(
364 self.external_type == CachedExternalType::EcmaScriptViaImport
365 || self.external_type == CachedExternalType::Script,
366 ))
367 }
368
369 #[turbo_tasks::function]
370 fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
371 ModuleSideEffects::SideEffectful.cell()
372 }
373}
374
375#[turbo_tasks::value_impl]
376impl ChunkableModule for CachedExternalModule {
377 #[turbo_tasks::function]
378 fn as_chunk_item(
379 self: ResolvedVc<Self>,
380 module_graph: ResolvedVc<ModuleGraph>,
381 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
382 ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
383 ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
384 }
385}
386
387#[turbo_tasks::value_impl]
388impl EcmascriptChunkPlaceable for CachedExternalModule {
389 #[turbo_tasks::function]
390 fn get_exports(&self) -> Vc<EcmascriptExports> {
391 if self.external_type == CachedExternalType::CommonJs {
392 EcmascriptExports::CommonJs.cell()
393 } else {
394 EcmascriptExports::DynamicNamespace.cell()
395 }
396 }
397
398 #[turbo_tasks::function]
399 fn get_async_module(&self) -> Vc<OptionAsyncModule> {
400 Vc::cell(
401 if self.external_type == CachedExternalType::EcmaScriptViaImport
402 || self.external_type == CachedExternalType::Script
403 {
404 Some(
405 AsyncModule {
406 has_top_level_await: true,
407 import_externals: self.external_type
408 == CachedExternalType::EcmaScriptViaImport,
409 }
410 .resolved_cell(),
411 )
412 } else {
413 None
414 },
415 )
416 }
417
418 #[turbo_tasks::function]
419 fn chunk_item_content(
420 self: Vc<Self>,
421 chunking_context: Vc<Box<dyn ChunkingContext>>,
422 _module_graph: Vc<ModuleGraph>,
423 async_module_info: Option<Vc<AsyncModuleInfo>>,
424 _estimated: bool,
425 ) -> Vc<EcmascriptChunkItemContent> {
426 let async_module_options = self.get_async_module().module_options(async_module_info);
427
428 EcmascriptChunkItemContent::new(self.content(), chunking_context, async_module_options)
429 }
430
431 #[turbo_tasks::function]
432 async fn chunk_item_output_assets(
433 self: Vc<Self>,
434 chunking_context: Vc<Box<dyn ChunkingContext>>,
435 _module_graph: Vc<ModuleGraph>,
436 ) -> Result<Vc<OutputAssetsWithReferenced>> {
437 let module = self.await?;
438 let chunking_context_resolved = chunking_context.to_resolved().await?;
439 let assets = if let Some(target) = &module.target {
440 ResolvedVc::cell(vec![ResolvedVc::upcast(
441 ExternalsSymlinkAsset::new(
442 *chunking_context_resolved,
443 hashed_package_name(target).into(),
444 target.clone(),
445 )
446 .to_resolved()
447 .await?,
448 )])
449 } else {
450 OutputAssets::empty_resolved()
451 };
452 Ok(OutputAssetsWithReferenced {
453 assets,
454 referenced_assets: OutputAssets::empty_resolved(),
455 references: OutputAssetsReferences::empty_resolved(),
456 }
457 .cell())
458 }
459}
460
461#[turbo_tasks::value]
464struct SideEffectfulModuleWithoutSelfAsync {
465 module: ResolvedVc<Box<dyn Module>>,
466}
467
468#[turbo_tasks::value_impl]
469impl SideEffectfulModuleWithoutSelfAsync {
470 #[turbo_tasks::function]
471 fn new(module: ResolvedVc<Box<dyn Module>>) -> Vc<Self> {
472 Self::cell(SideEffectfulModuleWithoutSelfAsync { module })
473 }
474}
475
476#[turbo_tasks::value_impl]
477impl Module for SideEffectfulModuleWithoutSelfAsync {
478 #[turbo_tasks::function]
479 fn ident(&self) -> Vc<AssetIdent> {
480 self.module.ident()
481 }
482
483 #[turbo_tasks::function]
484 fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
485 self.module.source()
486 }
487
488 #[turbo_tasks::function]
489 fn references(&self) -> Vc<ModuleReferences> {
490 self.module.references()
491 }
492
493 #[turbo_tasks::function]
494 fn side_effects(&self) -> Vc<ModuleSideEffects> {
495 ModuleSideEffects::SideEffectful.cell()
496 }
497 }
499
500#[derive(Debug)]
501#[turbo_tasks::value(shared)]
502pub struct ExternalsSymlinkAsset {
503 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
504 hashed_package: RcStr,
505 target: FileSystemPath,
506}
507#[turbo_tasks::value_impl]
508impl ExternalsSymlinkAsset {
509 #[turbo_tasks::function]
510 pub fn new(
511 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
512 hashed_package: RcStr,
513 target: FileSystemPath,
514 ) -> Vc<Self> {
515 ExternalsSymlinkAsset {
516 chunking_context,
517 hashed_package,
518 target,
519 }
520 .cell()
521 }
522}
523#[turbo_tasks::value_impl]
524impl OutputAssetsReference for ExternalsSymlinkAsset {}
525
526#[turbo_tasks::value_impl]
527impl OutputAsset for ExternalsSymlinkAsset {
528 #[turbo_tasks::function]
529 async fn path(&self) -> Result<Vc<FileSystemPath>> {
530 Ok(self
531 .chunking_context
532 .output_root()
533 .await?
534 .join("node_modules")?
535 .join(&self.hashed_package)?
536 .cell())
537 }
538}
539
540#[turbo_tasks::value_impl]
541impl Asset for ExternalsSymlinkAsset {
542 #[turbo_tasks::function]
543 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
544 let this = self.await?;
545 let output_root_to_project_root = this.chunking_context.output_root_to_root_path().await?;
549 let project_root_to_target = &this.target.path;
550
551 let path = self.path().await?;
552 let path_to_output_root = path
553 .parent()
554 .get_relative_path_to(&*this.chunking_context.output_root().await?)
555 .context("path must be inside output root")?;
556
557 let target = format!(
558 "{path_to_output_root}/{output_root_to_project_root}/{project_root_to_target}",
559 )
560 .into();
561
562 Ok(AssetContent::Redirect {
563 target,
564 link_type: LinkType::DIRECTORY,
565 }
566 .cell())
567 }
568}