Skip to main content

turbopack_node/
evaluate.rs

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    /// Eagerly spawn a Node.js worker so it's ready when the first [`Self::operation`] is called.
117    /// The worker should go into the idle queue.
118    ///
119    /// If a worker request comes in while this is still initializing, it should wait on the bootup
120    /// semaphore and will resume when the worker is ready.
121    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    // Apply the effects here (inside this producing task) via the bounded-retry helper, draining
195    // them from the nested emit operation. Returning the serializable `EmittedEvaluatePoolAssets`
196    // (rather than a `serialization = "skip"` wrapper carrying the effects) keeps this task's
197    // output restorable from the persistent cache, so a warm restart does not re-run the
198    // effect-producing tasks.
199    //
200    // HACK: applying effects from inside a task means they may get re-applied if this task is
201    // invalidated. That's acceptable because the pool is created lazily; we can't move the
202    // apply to a true top-level task without eagerly reading the operation.
203    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)]
216/// Pass the file you cared as `runtime_entries` to invalidate and reload the
217/// evaluated result automatically.
218pub 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    // Effects are applied inside `create_evaluate_pool_assets_operation`; a plain strongly
231    // consistent read suffices here.
232    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    // Invalidate pool when code content changes
248    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            // We always depend on some known env vars that are used by Node.js
257            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/// A Node.js process could not connect back to us over the loopback interface.
301///
302/// Backends return this from [`EvaluateOperation::operation`] so that it can be
303/// reported as an actionable issue. This is not a bug in the evaluated code and
304/// retrying will not help, it usually means a sandbox blocks local networking.
305#[derive(Debug)]
306pub struct NodeJsConnectError {
307    /// The error output of the child process, which contains the reason the
308    /// connection failed.
309    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    /// Finds a [`NodeJsConnectError`] anywhere in the cause chain of `err`.
326    pub fn find(err: &anyhow::Error) -> Option<&Self> {
327        err.chain().find_map(|cause| cause.downcast_ref::<Self>())
328    }
329}
330
331struct PoolErrorHandler;
332
333/// Number of attempts before we start slowing down the retry.
334const MAX_FAST_ATTEMPTS: usize = 5;
335/// Total number of attempts.
336const 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        // A process that cannot connect to us will not be able to on a retry either.
343        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    /// The source that issues about this evaluation are attributed to.
366    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    /// Optional human-readable prefix describing *what was being evaluated*,
391    /// included verbatim in the message of the synthetic [`StructuredError`]
392    /// emitted when the Node.js subprocess crashes mid-evaluation. For
393    /// webpack-loader evaluations this is the loader chain ("loaders
394    /// [foo, bar]"). The default returns `None`.
395    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    // Read this strongly consistent, since we don't want to run inconsistent
405    // node.js code.
406    let pool = pool_op.read_strongly_consistent().await?;
407
408    let args = evaluate_context.args().iter().try_join().await?;
409    // Assume this is a one-off operation, so we can kill the process
410    // TODO use a better way to decide that.
411    let kill = !evaluate_context.keep_alive();
412
413    // Workers in the pool could be in a bad state that we didn't detect yet.
414    // The bad state might even be unnoticeable until we actually send the job to the
415    // worker. So we retry picking workers from the pools until we succeed
416    // sending the job.
417
418    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                    // Connection errors happen first thing on startup, so we shouldn't be too
443                    // worried about this being overly long.
444                    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    // The evaluation sent an initial intermediate value without completing. We'll
455    // need to spawn a new thread to continually pull data out of the process,
456    // and ferry that along.
457    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/// Pass the file you cared as `runtime_entries` to invalidate and reload the
576/// evaluated result automatically.
577#[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
605/// Repeatedly pulls from the Operation until we receive a
606/// value/error/end.
607async 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                // The Node.js subprocess crashed (or some other IPC failure
621                // closed the connection) before sending a response. Convert
622                // this into a synthesized issue with whatever diagnostic
623                // context the pool managed to capture, so the user sees a
624                // real error message instead of an internal turbo-tasks
625                // execution-failed cascade.
626                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                // Do not reuse the process in case of error
649                operation.disallow_reuse();
650                // Issue emitted, we want to break but don't want to return an error
651                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/// An issue that occurred while evaluating node code.
779#[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    /// Optional extra context shown only when log details are enabled — e.g.
787    /// the loader chain that was running when a webpack-loader subprocess
788    /// errored.
789    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/// Node.js child processes could not connect back to the parent process, which
831/// makes it impossible to run any Node.js code (webpack loaders, PostCSS, ...).
832#[turbo_tasks::value(shared)]
833pub struct NodeJsConnectIssue {
834    /// The source being transformed when the connection failed. The failure is
835    /// unrelated to its contents, but this tells the user which transform hit it.
836    pub source: IssueSource,
837    /// What was being evaluated, e.g. the loader chain.
838    pub crash_context: Option<RcStr>,
839    /// Error output of the child process, describing why the connection failed.
840    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}