1use std::{iter, process::ExitStatus, time::Duration};
2
3use anyhow::{Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use bytes::Bytes;
7use futures_retry::{FutureRetry, RetryPolicy};
8use serde::{Deserialize, Serialize, de::DeserializeOwned};
9use serde_json::Value as JsonValue;
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{
12 Completion, FxIndexMap, OperationVc, PrettyPrintError, ResolvedVc, TryJoinIterExt, Vc,
13 duration_span, fxindexmap, parallel::available_parallelism,
14 resolve_strongly_consistent_and_take_and_apply_effects,
15};
16use turbo_tasks_env::{EnvMap, ProcessEnv};
17use turbo_tasks_fs::{File, FileContent, FileSystemPath, to_sys_path};
18use turbopack_core::{
19 asset::AssetContent,
20 changed::content_changed,
21 chunk::{ChunkingContext, ChunkingContextExt, EvaluatableAsset, EvaluatableAssets},
22 context::AssetContext,
23 file_source::FileSource,
24 ident::AssetIdent,
25 issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
26 module::Module,
27 module_graph::{
28 GraphEntries, ModuleGraph,
29 chunk_group_info::{ChunkGroupEntry, ChunkGroupKey, EntryHeuristics},
30 },
31 output::{OutputAsset, OutputAssets},
32 reference_type::{InnerAssets, ReferenceType},
33 source::Source,
34 virtual_source::VirtualSource,
35};
36
37use crate::{
38 AssetsForSourceMapping,
39 backend::{CreatePoolOptions, NodeBackend},
40 embed_js::embed_file_path,
41 emit, emit_package_json,
42 format::FormattingMode,
43 internal_assets_for_source_mapping,
44 pool_stats::PoolStatsSnapshot,
45 source_map::StructuredError,
46};
47
48#[derive(Serialize)]
49#[serde(tag = "type", rename_all = "camelCase")]
50enum EvalJavaScriptOutgoingMessage<'a> {
51 #[serde(rename_all = "camelCase")]
52 Evaluate { args: Vec<&'a JsonValue> },
53 Result {
54 id: u64,
55 data: Option<JsonValue>,
56 error: Option<String>,
57 },
58}
59
60#[derive(Deserialize, Debug)]
61#[serde(tag = "type", rename_all = "camelCase")]
62enum EvalJavaScriptIncomingMessage {
63 Info { data: JsonValue },
64 Request { id: u64, data: JsonValue },
65 End { data: Option<String> },
66 Error(StructuredError),
67}
68
69#[turbo_tasks::value(
70 cell = "new",
71 serialization = "skip",
72 evict = "last",
73 eq = "manual",
74 shared
75)]
76pub struct EvaluatePool {
77 #[turbo_tasks(unsafe_ignore, debug_ignore)]
78 pool: Box<dyn EvaluateOperation>,
79 pub assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
80 pub assets_root: FileSystemPath,
81 pub project_dir: FileSystemPath,
82}
83
84impl EvaluatePool {
85 pub(crate) fn new(
86 pool: Box<dyn EvaluateOperation>,
87 assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
88 assets_root: FileSystemPath,
89 project_dir: FileSystemPath,
90 ) -> Self {
91 Self {
92 pool,
93 assets_for_source_mapping,
94 assets_root,
95 project_dir,
96 }
97 }
98
99 pub async fn operation(&self) -> Result<Box<dyn Operation>> {
100 self.pool.operation().await
101 }
102
103 pub fn stats(&self) -> PoolStatsSnapshot {
104 self.pool.stats()
105 }
106
107 pub fn pre_warm(&self) {
108 self.pool.pre_warm()
109 }
110}
111
112#[async_trait::async_trait]
113pub trait EvaluateOperation: Send + Sync {
114 async fn operation(&self) -> Result<Box<dyn Operation>>;
115 fn stats(&self) -> PoolStatsSnapshot;
116 fn pre_warm(&self);
122}
123
124#[async_trait::async_trait]
125pub trait Operation: Send {
126 async fn recv(&mut self) -> Result<Bytes>;
127
128 async fn send(&mut self, data: Bytes) -> Result<()>;
129
130 async fn wait_or_kill(&mut self) -> Result<ExitStatus>;
131
132 fn disallow_reuse(&mut self) -> ();
133}
134
135#[turbo_tasks::value]
136struct EmittedEvaluatePoolAssets {
137 bootstrap: ResolvedVc<Box<dyn OutputAsset>>,
138 output_root: FileSystemPath,
139 entrypoint: FileSystemPath,
140}
141
142#[turbo_tasks::function(operation, root)]
143async fn emit_evaluate_pool_assets_operation(
144 entries: ResolvedVc<EvaluateEntries>,
145 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
146 module_graph: ResolvedVc<ModuleGraph>,
147) -> Result<Vc<EmittedEvaluatePoolAssets>> {
148 let EvaluateEntries {
149 entries,
150 main_entry_ident,
151 } = &*entries.await?;
152
153 let entrypoint = chunking_context
154 .chunk_path(
155 None,
156 **main_entry_ident,
157 Some(rcstr!("pool_entry")),
158 rcstr!(".js"),
159 )
160 .owned()
161 .await?;
162
163 let bootstrap = chunking_context.root_entry_chunk_group_asset(
164 entrypoint.clone(),
165 ChunkGroupKey::Entry(entries.iter().cloned().map(ResolvedVc::upcast).collect()),
166 *module_graph,
167 OutputAssets::empty(),
168 OutputAssets::empty(),
169 );
170
171 let output_root = chunking_context.output_root().owned().await?;
172 emit_package_json(output_root.clone())?
173 .as_side_effect()
174 .await?;
175 emit(bootstrap, output_root.clone())
176 .as_side_effect()
177 .await?;
178
179 Ok(EmittedEvaluatePoolAssets {
180 bootstrap: bootstrap.to_resolved().await?,
181 output_root,
182 entrypoint,
183 }
184 .cell())
185}
186
187#[turbo_tasks::function(operation, root, session_dependent)]
188async fn create_evaluate_pool_assets_operation(
189 entries: ResolvedVc<EvaluateEntries>,
190 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
191 module_graph: ResolvedVc<ModuleGraph>,
192) -> Result<Vc<EmittedEvaluatePoolAssets>> {
193 let operation = emit_evaluate_pool_assets_operation(entries, chunking_context, module_graph);
194 let assets = resolve_strongly_consistent_and_take_and_apply_effects(operation).await?;
204
205 Ok(*assets)
206}
207
208#[turbo_tasks::task_input]
209#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Encode, Decode)]
210pub enum EnvVarTracking {
211 WholeEnvTracked,
212 Untracked,
213}
214
215#[turbo_tasks::function(operation, root)]
216pub async fn get_evaluate_pool(
219 entries: ResolvedVc<EvaluateEntries>,
220 cwd: FileSystemPath,
221 env: ResolvedVc<Box<dyn ProcessEnv>>,
222 node_backend: ResolvedVc<Box<dyn NodeBackend>>,
223 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
224 module_graph: ResolvedVc<ModuleGraph>,
225 additional_invalidation: ResolvedVc<Completion>,
226 debug: bool,
227 env_var_tracking: EnvVarTracking,
228) -> Result<Vc<EvaluatePool>> {
229 let assets_op = create_evaluate_pool_assets_operation(entries, chunking_context, module_graph);
230 let assets = assets_op.read_strongly_consistent().await?;
233
234 let EmittedEvaluatePoolAssets {
235 bootstrap,
236 output_root,
237 entrypoint,
238 } = &*assets;
239
240 let (Some(cwd), Some(entrypoint)) = (
241 to_sys_path(cwd.clone()).await?,
242 to_sys_path(entrypoint.clone()).await?,
243 ) else {
244 panic!("can only evaluate from a disk filesystem");
245 };
246
247 content_changed(Vc::upcast(**bootstrap)).await?;
249 let assets_for_source_mapping =
250 internal_assets_for_source_mapping(**bootstrap, output_root.clone())
251 .to_resolved()
252 .await?;
253 let env = match env_var_tracking {
254 EnvVarTracking::WholeEnvTracked => env.read_all().await?,
255 EnvVarTracking::Untracked => {
256 common_node_env(*env).await?;
258 for name in ["FORCE_COLOR", "NO_COLOR", "OPENSSL_CONF", "TZ"] {
259 env.read(name.into()).await?;
260 }
261
262 env.read_all().untracked().await?
263 }
264 };
265
266 let node_backend = node_backend.into_trait_ref().await?;
267 let pool = node_backend
268 .create_pool(CreatePoolOptions {
269 cwd,
270 entrypoint,
271 env: env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
272 assets_for_source_mapping,
273 assets_root: output_root.clone(),
274 project_dir: chunking_context.root_path().owned().await?,
275 concurrency: available_parallelism().map_or(1, |v| v.get()),
276 debug,
277 })
278 .await?;
279 pool.pre_warm();
280 additional_invalidation.await?;
281 Ok(pool.cell())
282}
283
284#[turbo_tasks::function]
285async fn common_node_env(env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<EnvMap>> {
286 let mut filtered = FxIndexMap::default();
287 let env = env.read_all().await?;
288 for (key, value) in &*env {
289 let uppercase = key.to_uppercase();
290 for filter in &["NODE_", "UV_", "SSL_"] {
291 if uppercase.starts_with(filter) {
292 filtered.insert(key.clone(), value.clone());
293 break;
294 }
295 }
296 }
297 Ok(Vc::cell(filtered))
298}
299
300#[derive(Debug)]
306pub struct NodeJsConnectError {
307 pub stderr: String,
310}
311
312impl std::fmt::Display for NodeJsConnectError {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 write!(
315 f,
316 "The Node.js child process could not connect to the parent process\n{}",
317 self.stderr
318 )
319 }
320}
321
322impl std::error::Error for NodeJsConnectError {}
323
324impl NodeJsConnectError {
325 pub fn find(err: &anyhow::Error) -> Option<&Self> {
327 err.chain().find_map(|cause| cause.downcast_ref::<Self>())
328 }
329}
330
331struct PoolErrorHandler;
332
333const MAX_FAST_ATTEMPTS: usize = 5;
335const MAX_ATTEMPTS: usize = MAX_FAST_ATTEMPTS * 2;
337
338impl futures_retry::ErrorHandler<anyhow::Error> for PoolErrorHandler {
339 type OutError = anyhow::Error;
340
341 fn handle(&mut self, attempt: usize, err: anyhow::Error) -> RetryPolicy<Self::OutError> {
342 if attempt >= MAX_ATTEMPTS || NodeJsConnectError::find(&err).is_some() {
344 RetryPolicy::ForwardError(err)
345 } else if attempt >= MAX_FAST_ATTEMPTS {
346 RetryPolicy::WaitRetry(Duration::from_secs(1))
347 } else {
348 RetryPolicy::Repeat
349 }
350 }
351}
352
353pub trait EvaluateContext {
354 type InfoMessage: DeserializeOwned;
355 type RequestMessage: DeserializeOwned;
356 type ResponseMessage: Serialize;
357 type State: Default;
358
359 fn pool(&self) -> OperationVc<EvaluatePool>;
360 fn keep_alive(&self) -> bool {
361 false
362 }
363 fn args(&self) -> &[ResolvedVc<JsonValue>];
364 fn cwd(&self) -> Vc<FileSystemPath>;
365 fn context_source_for_issue(&self) -> ResolvedVc<Box<dyn Source>>;
367 fn emit_error(
368 &self,
369 error: StructuredError,
370 pool: &EvaluatePool,
371 ) -> impl Future<Output = Result<()>> + Send;
372 fn info(
373 &self,
374 state: &mut Self::State,
375 data: Self::InfoMessage,
376 pool: &EvaluatePool,
377 ) -> impl Future<Output = Result<()>> + Send;
378 fn request(
379 &self,
380 state: &mut Self::State,
381 data: Self::RequestMessage,
382 pool: &EvaluatePool,
383 ) -> impl Future<Output = Result<Self::ResponseMessage>> + Send;
384 fn finish(
385 &self,
386 state: Self::State,
387 pool: &EvaluatePool,
388 ) -> impl Future<Output = Result<()>> + Send;
389
390 fn crash_context_prefix(&self) -> Option<RcStr> {
396 None
397 }
398}
399
400pub async fn custom_evaluate(evaluate_context: impl EvaluateContext) -> Result<Vc<Option<RcStr>>> {
401 let pool_op = evaluate_context.pool();
402 let mut state = Default::default();
403
404 let pool = pool_op.read_strongly_consistent().await?;
407
408 let args = evaluate_context.args().iter().try_join().await?;
409 let kill = !evaluate_context.keep_alive();
412
413 let operation = FutureRetry::new(
419 async || {
420 let mut operation = pool.operation().await?;
421 operation
422 .send(Bytes::from(serde_json::to_vec(
423 &EvalJavaScriptOutgoingMessage::Evaluate {
424 args: args.iter().map(|v| &**v).collect(),
425 },
426 )?))
427 .await?;
428 Ok(operation)
429 },
430 PoolErrorHandler,
431 )
432 .await;
433 let mut operation = match operation {
434 Ok((operation, _)) => operation,
435 Err((err, _)) => {
436 if let Some(connect_error) = NodeJsConnectError::find(&err) {
437 NodeJsConnectIssue {
438 source: IssueSource::from_source_only(
439 evaluate_context.context_source_for_issue(),
440 ),
441 crash_context: evaluate_context.crash_context_prefix(),
442 stderr: connect_error.stderr.as_str().trim().into(),
445 }
446 .resolved_cell()
447 .emit();
448 return Ok(Vc::cell(None));
449 }
450 return Err(err);
451 }
452 };
453
454 let result = pull_operation(&mut operation, &pool, &evaluate_context, &mut state).await?;
458
459 evaluate_context.finish(state, &pool).await?;
460
461 if kill {
462 operation.wait_or_kill().await?;
463 }
464
465 Ok(Vc::cell(result.map(RcStr::from)))
466}
467
468#[turbo_tasks::value]
469pub struct EvaluateEntries {
470 entries: Vec<ResolvedVc<Box<dyn EvaluatableAsset + 'static>>>,
471 main_entry_ident: ResolvedVc<AssetIdent>,
472}
473
474#[turbo_tasks::value_impl]
475impl EvaluateEntries {
476 #[turbo_tasks::function]
477 pub async fn graph_entries(self: Vc<Self>) -> Result<Vc<GraphEntries>> {
478 Ok(
479 GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
480 modules: self
481 .await?
482 .entries
483 .iter()
484 .cloned()
485 .map(ResolvedVc::upcast)
486 .collect(),
487 heuristics: EntryHeuristics::default(),
488 }])
489 .cell(),
490 )
491 }
492}
493
494#[turbo_tasks::function]
495pub async fn get_evaluate_entries(
496 module_asset: ResolvedVc<Box<dyn Module>>,
497 asset_context: ResolvedVc<Box<dyn AssetContext>>,
498 node_backend: ResolvedVc<Box<dyn NodeBackend>>,
499 runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
500) -> Result<Vc<EvaluateEntries>> {
501 let node_backend = node_backend.into_trait_ref().await?;
502 let runtime_module_path = node_backend.runtime_module_path();
503
504 let runtime_asset = asset_context
505 .process(
506 Vc::upcast(FileSource::new(
507 embed_file_path(runtime_module_path).owned().await?,
508 )),
509 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
510 )
511 .module()
512 .to_resolved()
513 .await?;
514
515 let entry_module = asset_context
516 .process(
517 Vc::upcast(VirtualSource::new(
518 runtime_asset.ident().await?.path.join("evaluate.js")?,
519 AssetContent::file(
520 FileContent::Content(File::from(
521 "import {run} from 'RUNTIME'; run(() => import('INNER'))",
522 ))
523 .cell(),
524 ),
525 )),
526 ReferenceType::Internal(ResolvedVc::cell(
527 fxindexmap! {rcstr!("INNER") => module_asset,
528 rcstr!("RUNTIME") => runtime_asset},
529 )),
530 )
531 .module()
532 .to_resolved()
533 .await?;
534
535 let runtime_entries = {
536 let mut entries = vec![];
537 let global_module_path = node_backend.globals_module_path();
538
539 let globals_module = asset_context
540 .process(
541 Vc::upcast(FileSource::new(
542 embed_file_path(global_module_path).owned().await?,
543 )),
544 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
545 )
546 .module();
547
548 let Some(globals_module) = ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(
549 globals_module.to_resolved().await?,
550 ) else {
551 bail!("Internal module is not evaluatable");
552 };
553
554 entries.push(globals_module);
555
556 if let Some(runtime_entries) = runtime_entries {
557 for &entry in &*runtime_entries.await? {
558 entries.push(entry)
559 }
560 }
561 entries
562 };
563
564 Ok(EvaluateEntries {
565 entries: runtime_entries
566 .iter()
567 .copied()
568 .chain(iter::once(ResolvedVc::try_downcast(entry_module).unwrap()))
569 .collect(),
570 main_entry_ident: module_asset.ident().to_resolved().await?,
571 }
572 .cell())
573}
574
575#[turbo_tasks::function]
578pub async fn evaluate(
579 entries: ResolvedVc<EvaluateEntries>,
580 cwd: FileSystemPath,
581 env: ResolvedVc<Box<dyn ProcessEnv>>,
582 node_backend: ResolvedVc<Box<dyn NodeBackend>>,
583 context_source_for_issue: ResolvedVc<Box<dyn Source>>,
584 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
585 module_graph: ResolvedVc<ModuleGraph>,
586 args: Vec<ResolvedVc<JsonValue>>,
587 additional_invalidation: ResolvedVc<Completion>,
588 debug: bool,
589) -> Result<Vc<Option<RcStr>>> {
590 custom_evaluate(BasicEvaluateContext {
591 entries,
592 cwd,
593 env,
594 node_backend,
595 context_source_for_issue,
596 chunking_context,
597 module_graph,
598 args,
599 additional_invalidation,
600 debug,
601 })
602 .await
603}
604
605async fn pull_operation<T: EvaluateContext>(
608 operation: &mut Box<dyn Operation>,
609 pool: &EvaluatePool,
610 evaluate_context: &T,
611 state: &mut T::State,
612) -> Result<Option<String>> {
613 let _guard = duration_span!("Node.js evaluation", blocking = false);
614
615 loop {
616 let recv_result = operation.recv().await;
617 let bytes = match recv_result {
618 Ok(bytes) => bytes,
619 Err(err) => {
620 let message = match evaluate_context.crash_context_prefix() {
627 Some(prefix) => format!(
628 "Node.js subprocess crashed while evaluating {}: {}",
629 prefix,
630 PrettyPrintError(&err)
631 ),
632 None => format!(
633 "Node.js subprocess crashed while evaluating: {}",
634 PrettyPrintError(&err)
635 ),
636 };
637 let synthetic = StructuredError::from_message("Error".to_string(), message);
638 evaluate_context.emit_error(synthetic, pool).await?;
639 operation.disallow_reuse();
640 return Ok(None);
641 }
642 };
643 let message = serde_json::from_slice(&bytes)?;
644
645 match message {
646 EvalJavaScriptIncomingMessage::Error(error) => {
647 evaluate_context.emit_error(error, pool).await?;
648 operation.disallow_reuse();
650 return Ok(None);
652 }
653 EvalJavaScriptIncomingMessage::End { data } => return Ok(data),
654 EvalJavaScriptIncomingMessage::Info { data } => {
655 evaluate_context
656 .info(state, serde_json::from_value(data)?, pool)
657 .await?;
658 }
659 EvalJavaScriptIncomingMessage::Request { id, data } => {
660 match evaluate_context
661 .request(state, serde_json::from_value(data)?, pool)
662 .await
663 {
664 Ok(response) => {
665 operation
666 .send(Bytes::from(serde_json::to_vec(
667 &EvalJavaScriptOutgoingMessage::Result {
668 id,
669 error: None,
670 data: Some(serde_json::to_value(response)?),
671 },
672 )?))
673 .await?;
674 }
675 Err(e) => {
676 operation
677 .send(Bytes::from(serde_json::to_vec(
678 &EvalJavaScriptOutgoingMessage::Result {
679 id,
680 error: Some(PrettyPrintError(&e).to_string()),
681 data: None,
682 },
683 )?))
684 .await?;
685 }
686 }
687 }
688 }
689 }
690}
691
692struct BasicEvaluateContext {
693 entries: ResolvedVc<EvaluateEntries>,
694 cwd: FileSystemPath,
695 env: ResolvedVc<Box<dyn ProcessEnv>>,
696 node_backend: ResolvedVc<Box<dyn NodeBackend>>,
697 context_source_for_issue: ResolvedVc<Box<dyn Source>>,
698 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
699 module_graph: ResolvedVc<ModuleGraph>,
700 args: Vec<ResolvedVc<JsonValue>>,
701 additional_invalidation: ResolvedVc<Completion>,
702 debug: bool,
703}
704
705impl EvaluateContext for BasicEvaluateContext {
706 type InfoMessage = ();
707 type RequestMessage = ();
708 type ResponseMessage = ();
709 type State = ();
710
711 fn pool(&self) -> OperationVc<EvaluatePool> {
712 get_evaluate_pool(
713 self.entries,
714 self.cwd.clone(),
715 self.env,
716 self.node_backend,
717 self.chunking_context,
718 self.module_graph,
719 self.additional_invalidation,
720 self.debug,
721 EnvVarTracking::WholeEnvTracked,
722 )
723 }
724
725 fn args(&self) -> &[ResolvedVc<serde_json::Value>] {
726 &self.args
727 }
728
729 fn cwd(&self) -> Vc<turbo_tasks_fs::FileSystemPath> {
730 self.cwd.clone().cell()
731 }
732
733 fn context_source_for_issue(&self) -> ResolvedVc<Box<dyn Source>> {
734 self.context_source_for_issue
735 }
736
737 fn keep_alive(&self) -> bool {
738 !self.args.is_empty()
739 }
740
741 async fn emit_error(&self, error: StructuredError, pool: &EvaluatePool) -> Result<()> {
742 EvaluationIssue {
743 error,
744 source: IssueSource::from_source_only(self.context_source_for_issue),
745 assets_for_source_mapping: pool.assets_for_source_mapping,
746 assets_root: pool.assets_root.clone(),
747 root_path: self.chunking_context.root_path().owned().await?,
748 detail: None,
749 }
750 .resolved_cell()
751 .emit();
752 Ok(())
753 }
754
755 async fn info(
756 &self,
757 _state: &mut Self::State,
758 _data: Self::InfoMessage,
759 _pool: &EvaluatePool,
760 ) -> Result<()> {
761 bail!("BasicEvaluateContext does not support info messages")
762 }
763
764 async fn request(
765 &self,
766 _state: &mut Self::State,
767 _data: Self::RequestMessage,
768 _pool: &EvaluatePool,
769 ) -> Result<Self::ResponseMessage> {
770 bail!("BasicEvaluateContext does not support request messages")
771 }
772
773 async fn finish(&self, _state: Self::State, _pool: &EvaluatePool) -> Result<()> {
774 Ok(())
775 }
776}
777
778#[turbo_tasks::value(shared)]
780pub struct EvaluationIssue {
781 pub source: IssueSource,
782 pub error: StructuredError,
783 pub assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
784 pub assets_root: FileSystemPath,
785 pub root_path: FileSystemPath,
786 pub detail: Option<RcStr>,
790}
791
792#[async_trait]
793#[turbo_tasks::value_impl]
794impl Issue for EvaluationIssue {
795 async fn title(&self) -> Result<StyledString> {
796 Ok(StyledString::Text(rcstr!("Error evaluating Node.js code")))
797 }
798
799 fn stage(&self) -> IssueStage {
800 IssueStage::Transform
801 }
802
803 async fn file_path(&self) -> Result<FileSystemPath> {
804 self.source.file_path().await
805 }
806
807 async fn description(&self) -> Result<Option<StyledString>> {
808 Ok(Some(StyledString::Text(
809 self.error
810 .print(
811 *self.assets_for_source_mapping,
812 self.assets_root.clone(),
813 self.root_path.clone(),
814 FormattingMode::Plain,
815 )
816 .await?
817 .into(),
818 )))
819 }
820
821 async fn detail(&self) -> Result<Option<StyledString>> {
822 Ok(self.detail.clone().map(StyledString::Text))
823 }
824
825 fn source(&self) -> Option<IssueSource> {
826 Some(self.source)
827 }
828}
829
830#[turbo_tasks::value(shared)]
833pub struct NodeJsConnectIssue {
834 pub source: IssueSource,
837 pub crash_context: Option<RcStr>,
839 pub stderr: RcStr,
841}
842
843#[async_trait]
844#[turbo_tasks::value_impl]
845impl Issue for NodeJsConnectIssue {
846 fn severity(&self) -> IssueSeverity {
847 IssueSeverity::Error
848 }
849
850 async fn title(&self) -> Result<StyledString> {
851 Ok(StyledString::Text(rcstr!(
852 "Node.js worker processes could not connect to Turbopack"
853 )))
854 }
855
856 fn stage(&self) -> IssueStage {
857 IssueStage::Transform
858 }
859
860 async fn file_path(&self) -> Result<FileSystemPath> {
861 self.source.file_path().await
862 }
863
864 fn source(&self) -> Option<IssueSource> {
865 Some(self.source)
866 }
867
868 async fn description(&self) -> Result<Option<StyledString>> {
869 let mut first_line = vec![StyledString::Text(rcstr!(
870 "Turbopack is configured to run webpack loaders and PostCSS transforms in child \
871 processes that communicate with it over a loopback TCP connection, but the child \
872 process was unable to connect"
873 ))];
874
875 if let Some(crash_context) = &self.crash_context {
876 first_line.push(StyledString::Text(rcstr!(" while evaluating ")));
877 first_line.push(StyledString::Code(crash_context.clone()));
878 };
879 first_line.push(StyledString::Text(rcstr!(":")));
880 Ok(Some(StyledString::Stack(vec![
881 StyledString::Line(first_line),
882 StyledString::Code(self.stderr.clone()),
883 StyledString::Line(vec![
884 StyledString::Text(rcstr!(
885 "This is likely caused by a sandbox or firewall that blocks connections. \
886 Allow local connections, or run this code using worker threads by setting "
887 )),
888 StyledString::Code(rcstr!(
889 "experimental.turbopackPluginRuntimeStrategy: 'workerThreads'"
890 )),
891 StyledString::Text(rcstr!(" in your Next.js config.")),
892 ]),
893 ])))
894 }
895
896 fn documentation_link(&self) -> RcStr {
897 rcstr!(
898 "https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopackPluginRuntimeStrategy"
899 )
900 }
901}