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)]
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 "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 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, "var 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, "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#[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 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 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}