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