turbopack_node/
evaluate.rs

1use std::{
2    borrow::Cow, iter, ops::ControlFlow, sync::Arc, thread::available_parallelism, time::Duration,
3};
4
5use anyhow::{Result, anyhow, bail};
6use async_stream::try_stream as generator;
7use futures::{
8    SinkExt, StreamExt,
9    channel::mpsc::{UnboundedSender, unbounded},
10    pin_mut,
11};
12use futures_retry::{FutureRetry, RetryPolicy};
13use parking_lot::Mutex;
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15use serde_json::Value as JsonValue;
16use turbo_rcstr::rcstr;
17use turbo_tasks::{
18    Completion, Effects, FxIndexMap, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
19    TaskInput, TryJoinIterExt, Vc, VcValueType, duration_span, fxindexmap, get_effects,
20    mark_finished, prevent_gc, trace::TraceRawVcs, util::SharedError,
21};
22use turbo_tasks_bytes::{Bytes, Stream};
23use turbo_tasks_env::{EnvMap, ProcessEnv};
24use turbo_tasks_fs::{File, FileSystemPath, to_sys_path};
25use turbopack_core::{
26    asset::AssetContent,
27    changed::content_changed,
28    chunk::{ChunkingContext, ChunkingContextExt, EvaluatableAsset, EvaluatableAssets},
29    context::AssetContext,
30    error::PrettyPrintError,
31    file_source::FileSource,
32    issue::{
33        Issue, IssueExt, IssueSource, IssueStage, OptionIssueSource, OptionStyledString,
34        StyledString,
35    },
36    module::Module,
37    module_graph::{ModuleGraph, chunk_group_info::ChunkGroupEntry},
38    output::{OutputAsset, OutputAssets},
39    reference_type::{InnerAssets, ReferenceType},
40    source::Source,
41    virtual_source::VirtualSource,
42};
43
44use crate::{
45    AssetsForSourceMapping,
46    embed_js::embed_file_path,
47    emit, emit_package_json, internal_assets_for_source_mapping,
48    pool::{FormattingMode, NodeJsOperation, NodeJsPool},
49    source_map::StructuredError,
50};
51
52#[derive(Serialize)]
53#[serde(tag = "type", rename_all = "camelCase")]
54enum EvalJavaScriptOutgoingMessage<'a> {
55    #[serde(rename_all = "camelCase")]
56    Evaluate { args: Vec<&'a JsonValue> },
57    Result {
58        id: u64,
59        data: Option<JsonValue>,
60        error: Option<String>,
61    },
62}
63
64#[derive(Deserialize, Debug)]
65#[serde(tag = "type", rename_all = "camelCase")]
66enum EvalJavaScriptIncomingMessage {
67    Info { data: JsonValue },
68    Request { id: u64, data: JsonValue },
69    End { data: Option<String> },
70    Error(StructuredError),
71}
72
73type LoopResult = ControlFlow<Result<Option<String>, StructuredError>, String>;
74
75type EvaluationItem = Result<Bytes, SharedError>;
76type JavaScriptStream = Stream<EvaluationItem>;
77
78#[turbo_tasks::value(eq = "manual", cell = "new", serialization = "none")]
79pub struct JavaScriptStreamSender {
80    #[turbo_tasks(trace_ignore, debug_ignore)]
81    get: Box<dyn Fn() -> UnboundedSender<Result<Bytes, SharedError>> + Send + Sync>,
82}
83
84#[turbo_tasks::value(transparent)]
85#[derive(Clone, Debug)]
86pub struct JavaScriptEvaluation(#[turbo_tasks(trace_ignore)] JavaScriptStream);
87
88#[turbo_tasks::value]
89struct EmittedEvaluatePoolAssets {
90    bootstrap: ResolvedVc<Box<dyn OutputAsset>>,
91    output_root: FileSystemPath,
92    entrypoint: FileSystemPath,
93}
94
95#[turbo_tasks::function(operation)]
96async fn emit_evaluate_pool_assets_operation(
97    module_asset: ResolvedVc<Box<dyn Module>>,
98    asset_context: ResolvedVc<Box<dyn AssetContext>>,
99    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
100    runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
101) -> Result<Vc<EmittedEvaluatePoolAssets>> {
102    let runtime_asset = asset_context
103        .process(
104            Vc::upcast(FileSource::new(
105                embed_file_path(rcstr!("ipc/evaluate.ts")).owned().await?,
106            )),
107            ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
108        )
109        .module()
110        .to_resolved()
111        .await?;
112
113    let module_path = module_asset.ident().path().await?;
114    let file_name = module_path.file_name();
115    let file_name = if file_name.ends_with(".js") {
116        Cow::Borrowed(file_name)
117    } else if let Some(file_name) = file_name.strip_suffix(".ts") {
118        Cow::Owned(format!("{file_name}.js"))
119    } else {
120        Cow::Owned(format!("{file_name}.js"))
121    };
122    let entrypoint = chunking_context.output_root().await?.join(&file_name)?;
123    let entry_module = asset_context
124        .process(
125            Vc::upcast(VirtualSource::new(
126                runtime_asset.ident().path().await?.join("evaluate.js")?,
127                AssetContent::file(
128                    File::from("import { run } from 'RUNTIME'; run(() => import('INNER'))").into(),
129                ),
130            )),
131            ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
132                rcstr!("INNER") => module_asset,
133                rcstr!("RUNTIME") => runtime_asset
134            })),
135        )
136        .module()
137        .to_resolved()
138        .await?;
139
140    let runtime_entries = {
141        let globals_module = asset_context
142            .process(
143                Vc::upcast(FileSource::new(
144                    embed_file_path(rcstr!("globals.ts")).owned().await?,
145                )),
146                ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
147            )
148            .module();
149
150        let Some(globals_module) =
151            Vc::try_resolve_sidecast::<Box<dyn EvaluatableAsset>>(globals_module).await?
152        else {
153            bail!("Internal module is not evaluatable");
154        };
155
156        let mut entries = vec![globals_module.to_resolved().await?];
157        if let Some(runtime_entries) = runtime_entries {
158            for &entry in &*runtime_entries.await? {
159                entries.push(entry)
160            }
161        }
162        entries
163    };
164
165    let module_graph = ModuleGraph::from_modules(
166        Vc::cell(vec![ChunkGroupEntry::Entry(
167            iter::once(entry_module)
168                .chain(runtime_entries.iter().copied().map(ResolvedVc::upcast))
169                .collect(),
170        )]),
171        false,
172    );
173
174    let bootstrap = chunking_context.root_entry_chunk_group_asset(
175        entrypoint.clone(),
176        Vc::<EvaluatableAssets>::cell(runtime_entries)
177            .with_entry(*ResolvedVc::try_downcast(entry_module).unwrap()),
178        module_graph,
179        OutputAssets::empty(),
180    );
181
182    let output_root = chunking_context.output_root().owned().await?;
183    emit_package_json(output_root.clone())?
184        .as_side_effect()
185        .await?;
186    emit(bootstrap, output_root.clone())
187        .as_side_effect()
188        .await?;
189
190    Ok(EmittedEvaluatePoolAssets {
191        bootstrap: bootstrap.to_resolved().await?,
192        output_root,
193        entrypoint: entrypoint.clone(),
194    }
195    .cell())
196}
197
198#[turbo_tasks::value(serialization = "none")]
199struct EmittedEvaluatePoolAssetsWithEffects {
200    assets: ReadRef<EmittedEvaluatePoolAssets>,
201    effects: Arc<Effects>,
202}
203
204#[turbo_tasks::function(operation)]
205async fn emit_evaluate_pool_assets_with_effects_operation(
206    module_asset: ResolvedVc<Box<dyn Module>>,
207    asset_context: ResolvedVc<Box<dyn AssetContext>>,
208    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
209    runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
210) -> Result<Vc<EmittedEvaluatePoolAssetsWithEffects>> {
211    let operation = emit_evaluate_pool_assets_operation(
212        module_asset,
213        asset_context,
214        chunking_context,
215        runtime_entries,
216    );
217    let assets = operation.read_strongly_consistent().await?;
218    let effects = Arc::new(get_effects(operation).await?);
219    Ok(EmittedEvaluatePoolAssetsWithEffects { assets, effects }.cell())
220}
221
222#[derive(
223    Clone,
224    Copy,
225    Hash,
226    Debug,
227    PartialEq,
228    Eq,
229    Serialize,
230    Deserialize,
231    TaskInput,
232    NonLocalValue,
233    TraceRawVcs,
234)]
235pub enum EnvVarTracking {
236    WholeEnvTracked,
237    Untracked,
238}
239
240#[turbo_tasks::function(operation)]
241/// Pass the file you cared as `runtime_entries` to invalidate and reload the
242/// evaluated result automatically.
243pub async fn get_evaluate_pool(
244    module_asset: ResolvedVc<Box<dyn Module>>,
245    cwd: FileSystemPath,
246    env: ResolvedVc<Box<dyn ProcessEnv>>,
247    asset_context: ResolvedVc<Box<dyn AssetContext>>,
248    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
249    runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
250    additional_invalidation: ResolvedVc<Completion>,
251    debug: bool,
252    env_var_tracking: EnvVarTracking,
253) -> Result<Vc<NodeJsPool>> {
254    let operation = emit_evaluate_pool_assets_with_effects_operation(
255        module_asset,
256        asset_context,
257        chunking_context,
258        runtime_entries,
259    );
260    let EmittedEvaluatePoolAssetsWithEffects { assets, effects } =
261        &*operation.read_strongly_consistent().await?;
262    effects.apply().await?;
263
264    let EmittedEvaluatePoolAssets {
265        bootstrap,
266        output_root,
267        entrypoint,
268    } = &**assets;
269
270    let (Some(cwd), Some(entrypoint)) = (
271        to_sys_path(cwd.clone()).await?,
272        to_sys_path(entrypoint.clone()).await?,
273    ) else {
274        panic!("can only evaluate from a disk filesystem");
275    };
276
277    // Invalidate pool when code content changes
278    content_changed(Vc::upcast(**bootstrap)).await?;
279    let assets_for_source_mapping =
280        internal_assets_for_source_mapping(**bootstrap, output_root.clone())
281            .to_resolved()
282            .await?;
283    let env = match env_var_tracking {
284        EnvVarTracking::WholeEnvTracked => env.read_all().await?,
285        EnvVarTracking::Untracked => {
286            // We always depend on some known env vars that are used by Node.js
287            common_node_env(*env).await?;
288            for name in ["FORCE_COLOR", "NO_COLOR", "OPENSSL_CONF", "TZ"] {
289                env.read(name.into()).await?;
290            }
291
292            env.read_all().untracked().await?
293        }
294    };
295    let pool = NodeJsPool::new(
296        cwd,
297        entrypoint,
298        env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
299        assets_for_source_mapping,
300        output_root.clone(),
301        chunking_context.root_path().owned().await?,
302        available_parallelism().map_or(1, |v| v.get()),
303        debug,
304    );
305    additional_invalidation.await?;
306    Ok(pool.cell())
307}
308
309#[turbo_tasks::function]
310async fn common_node_env(env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<EnvMap>> {
311    let mut filtered = FxIndexMap::default();
312    let env = env.read_all().await?;
313    for (key, value) in &*env {
314        let uppercase = key.to_uppercase();
315        for filter in &["NODE_", "UV_", "SSL_"] {
316            if uppercase.starts_with(filter) {
317                filtered.insert(key.clone(), value.clone());
318                break;
319            }
320        }
321    }
322    Ok(Vc::cell(filtered))
323}
324
325struct PoolErrorHandler;
326
327/// Number of attempts before we start slowing down the retry.
328const MAX_FAST_ATTEMPTS: usize = 5;
329/// Total number of attempts.
330const MAX_ATTEMPTS: usize = MAX_FAST_ATTEMPTS * 2;
331
332impl futures_retry::ErrorHandler<anyhow::Error> for PoolErrorHandler {
333    type OutError = anyhow::Error;
334
335    fn handle(&mut self, attempt: usize, err: anyhow::Error) -> RetryPolicy<Self::OutError> {
336        if attempt >= MAX_ATTEMPTS {
337            RetryPolicy::ForwardError(err)
338        } else if attempt >= MAX_FAST_ATTEMPTS {
339            RetryPolicy::WaitRetry(Duration::from_secs(1))
340        } else {
341            RetryPolicy::Repeat
342        }
343    }
344}
345
346pub trait EvaluateContext {
347    type InfoMessage: DeserializeOwned;
348    type RequestMessage: DeserializeOwned;
349    type ResponseMessage: Serialize;
350    type State: Default;
351
352    fn compute(self, sender: Vc<JavaScriptStreamSender>)
353    -> impl Future<Output = Result<()>> + Send;
354    fn pool(&self) -> OperationVc<NodeJsPool>;
355    fn keep_alive(&self) -> bool {
356        false
357    }
358    fn args(&self) -> &[ResolvedVc<JsonValue>];
359    fn cwd(&self) -> Vc<FileSystemPath>;
360    fn emit_error(
361        &self,
362        error: StructuredError,
363        pool: &NodeJsPool,
364    ) -> impl Future<Output = Result<()>> + Send;
365    fn info(
366        &self,
367        state: &mut Self::State,
368        data: Self::InfoMessage,
369        pool: &NodeJsPool,
370    ) -> impl Future<Output = Result<()>> + Send;
371    fn request(
372        &self,
373        state: &mut Self::State,
374        data: Self::RequestMessage,
375        pool: &NodeJsPool,
376    ) -> impl Future<Output = Result<Self::ResponseMessage>> + Send;
377    fn finish(
378        &self,
379        state: Self::State,
380        pool: &NodeJsPool,
381    ) -> impl Future<Output = Result<()>> + Send;
382}
383
384pub async fn custom_evaluate(
385    evaluate_context: impl EvaluateContext,
386) -> Result<Vc<JavaScriptEvaluation>> {
387    // TODO: The way we invoke compute_evaluate_stream as side effect is not
388    // GC-safe, so we disable GC for this task.
389    prevent_gc();
390
391    // Note the following code uses some hacks to create a child task that produces
392    // a stream that is returned by this task.
393
394    // We create a new cell in this task, which will be updated from the
395    // [compute_evaluate_stream] task.
396    let cell = turbo_tasks::macro_helpers::find_cell_by_type(
397        <JavaScriptEvaluation as VcValueType>::get_value_type_id(),
398    );
399
400    // We initialize the cell with a stream that is open, but has no values.
401    // The first [compute_evaluate_stream] pipe call will pick up that stream.
402    let (sender, receiver) = unbounded();
403    cell.update(JavaScriptEvaluation(JavaScriptStream::new_open(
404        vec![],
405        Box::new(receiver),
406    )));
407    let initial = Mutex::new(Some(sender));
408
409    // run the evaluation as side effect
410    evaluate_context
411        .compute(
412            JavaScriptStreamSender {
413                get: Box::new(move || {
414                    if let Some(sender) = initial.lock().take() {
415                        sender
416                    } else {
417                        // In cases when only [compute_evaluate_stream] is (re)executed, we need to
418                        // update the old stream with a new value.
419                        let (sender, receiver) = unbounded();
420                        cell.update(JavaScriptEvaluation(JavaScriptStream::new_open(
421                            vec![],
422                            Box::new(receiver),
423                        )));
424                        sender
425                    }
426                }),
427            }
428            .cell(),
429        )
430        .await?;
431
432    let raw: RawVc = cell.into();
433    Ok(raw.into())
434}
435
436/// Pass the file you cared as `runtime_entries` to invalidate and reload the
437/// evaluated result automatically.
438#[turbo_tasks::function]
439pub async fn evaluate(
440    module_asset: ResolvedVc<Box<dyn Module>>,
441    cwd: FileSystemPath,
442    env: ResolvedVc<Box<dyn ProcessEnv>>,
443    context_source_for_issue: ResolvedVc<Box<dyn Source>>,
444    asset_context: ResolvedVc<Box<dyn AssetContext>>,
445    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
446    runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
447    args: Vec<ResolvedVc<JsonValue>>,
448    additional_invalidation: ResolvedVc<Completion>,
449    debug: bool,
450) -> Result<Vc<JavaScriptEvaluation>> {
451    custom_evaluate(BasicEvaluateContext {
452        module_asset,
453        cwd,
454        env,
455        context_source_for_issue,
456        asset_context,
457        chunking_context,
458        runtime_entries,
459        args,
460        additional_invalidation,
461        debug,
462    })
463    .await
464}
465
466pub async fn compute(
467    evaluate_context: impl EvaluateContext,
468    sender: Vc<JavaScriptStreamSender>,
469) -> Result<Vc<()>> {
470    mark_finished();
471    let Ok(sender) = sender.await else {
472        // Impossible to handle the error in a good way.
473        return Ok(Default::default());
474    };
475
476    let stream = generator! {
477        let pool_op = evaluate_context.pool();
478        let mut state = Default::default();
479
480        // Read this strongly consistent, since we don't want to run inconsistent
481        // node.js code.
482        let pool = pool_op.read_strongly_consistent().await?;
483
484        let args = evaluate_context.args().iter().try_join().await?;
485        // Assume this is a one-off operation, so we can kill the process
486        // TODO use a better way to decide that.
487        let kill = !evaluate_context.keep_alive();
488
489        // Workers in the pool could be in a bad state that we didn't detect yet.
490        // The bad state might even be unnoticeable until we actually send the job to the
491        // worker. So we retry picking workers from the pools until we succeed
492        // sending the job.
493
494        let (mut operation, _) = FutureRetry::new(
495            || async {
496                let mut operation = pool.operation().await?;
497                operation
498                    .send(EvalJavaScriptOutgoingMessage::Evaluate {
499                        args: args.iter().map(|v| &**v).collect(),
500                    })
501                    .await?;
502                Ok(operation)
503            },
504            PoolErrorHandler,
505        )
506        .await
507        .map_err(|(e, _)| e)?;
508
509        // The evaluation sent an initial intermediate value without completing. We'll
510        // need to spawn a new thread to continually pull data out of the process,
511        // and ferry that along.
512        loop {
513            let output = pull_operation(&mut operation, &pool, &evaluate_context, &mut state).await?;
514
515            match output {
516                LoopResult::Continue(data) => {
517                    yield data.into();
518                }
519                LoopResult::Break(Ok(Some(data))) => {
520                    yield data.into();
521                    break;
522                }
523                LoopResult::Break(Err(e)) => {
524                    let error = print_error(e, &pool, &evaluate_context).await?;
525                    Err(anyhow!("Node.js evaluation failed: {}", error))?;
526                    break;
527                }
528                LoopResult::Break(Ok(None)) => {
529                    break;
530                }
531            }
532        }
533
534        evaluate_context.finish(state, &pool).await?;
535
536        if kill {
537            operation.wait_or_kill().await?;
538        }
539    };
540
541    let mut sender = (sender.get)();
542    pin_mut!(stream);
543    while let Some(value) = stream.next().await {
544        if sender.send(value).await.is_err() {
545            return Ok(Default::default());
546        }
547        if sender.flush().await.is_err() {
548            return Ok(Default::default());
549        }
550    }
551
552    Ok(Default::default())
553}
554
555/// Repeatedly pulls from the NodeJsOperation until we receive a
556/// value/error/end.
557async fn pull_operation<T: EvaluateContext>(
558    operation: &mut NodeJsOperation,
559    pool: &NodeJsPool,
560    evaluate_context: &T,
561    state: &mut T::State,
562) -> Result<LoopResult> {
563    let guard = duration_span!("Node.js evaluation");
564
565    let output = loop {
566        match operation.recv().await? {
567            EvalJavaScriptIncomingMessage::Error(error) => {
568                evaluate_context.emit_error(error, pool).await?;
569                // Do not reuse the process in case of error
570                operation.disallow_reuse();
571                // Issue emitted, we want to break but don't want to return an error
572                break ControlFlow::Break(Ok(None));
573            }
574            EvalJavaScriptIncomingMessage::End { data } => break ControlFlow::Break(Ok(data)),
575            EvalJavaScriptIncomingMessage::Info { data } => {
576                evaluate_context
577                    .info(state, serde_json::from_value(data)?, pool)
578                    .await?;
579            }
580            EvalJavaScriptIncomingMessage::Request { id, data } => {
581                match evaluate_context
582                    .request(state, serde_json::from_value(data)?, pool)
583                    .await
584                {
585                    Ok(response) => {
586                        operation
587                            .send(EvalJavaScriptOutgoingMessage::Result {
588                                id,
589                                error: None,
590                                data: Some(serde_json::to_value(response)?),
591                            })
592                            .await?;
593                    }
594                    Err(e) => {
595                        operation
596                            .send(EvalJavaScriptOutgoingMessage::Result {
597                                id,
598                                error: Some(PrettyPrintError(&e).to_string()),
599                                data: None,
600                            })
601                            .await?;
602                    }
603                }
604            }
605        }
606    };
607    drop(guard);
608
609    Ok(output)
610}
611
612#[turbo_tasks::function]
613async fn basic_compute(
614    evaluate_context: BasicEvaluateContext,
615    sender: Vc<JavaScriptStreamSender>,
616) -> Result<Vc<()>> {
617    compute(evaluate_context, sender).await
618}
619
620#[derive(Clone, PartialEq, Eq, Hash, TaskInput, Debug, Serialize, Deserialize, TraceRawVcs)]
621struct BasicEvaluateContext {
622    module_asset: ResolvedVc<Box<dyn Module>>,
623    cwd: FileSystemPath,
624    env: ResolvedVc<Box<dyn ProcessEnv>>,
625    context_source_for_issue: ResolvedVc<Box<dyn Source>>,
626    asset_context: ResolvedVc<Box<dyn AssetContext>>,
627    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
628    runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
629    args: Vec<ResolvedVc<JsonValue>>,
630    additional_invalidation: ResolvedVc<Completion>,
631    debug: bool,
632}
633
634impl EvaluateContext for BasicEvaluateContext {
635    type InfoMessage = ();
636    type RequestMessage = ();
637    type ResponseMessage = ();
638    type State = ();
639
640    async fn compute(self, sender: Vc<JavaScriptStreamSender>) -> Result<()> {
641        basic_compute(self, sender).as_side_effect().await
642    }
643
644    fn pool(&self) -> OperationVc<crate::pool::NodeJsPool> {
645        get_evaluate_pool(
646            self.module_asset,
647            self.cwd.clone(),
648            self.env,
649            self.asset_context,
650            self.chunking_context,
651            self.runtime_entries,
652            self.additional_invalidation,
653            self.debug,
654            EnvVarTracking::WholeEnvTracked,
655        )
656    }
657
658    fn args(&self) -> &[ResolvedVc<serde_json::Value>] {
659        &self.args
660    }
661
662    fn cwd(&self) -> Vc<turbo_tasks_fs::FileSystemPath> {
663        self.cwd.clone().cell()
664    }
665
666    fn keep_alive(&self) -> bool {
667        !self.args.is_empty()
668    }
669
670    async fn emit_error(&self, error: StructuredError, pool: &NodeJsPool) -> Result<()> {
671        EvaluationIssue {
672            error,
673            source: IssueSource::from_source_only(self.context_source_for_issue),
674            assets_for_source_mapping: pool.assets_for_source_mapping,
675            assets_root: pool.assets_root.clone(),
676            root_path: self.chunking_context.root_path().owned().await?,
677        }
678        .resolved_cell()
679        .emit();
680        Ok(())
681    }
682
683    async fn info(
684        &self,
685        _state: &mut Self::State,
686        _data: Self::InfoMessage,
687        _pool: &NodeJsPool,
688    ) -> Result<()> {
689        bail!("BasicEvaluateContext does not support info messages")
690    }
691
692    async fn request(
693        &self,
694        _state: &mut Self::State,
695        _data: Self::RequestMessage,
696        _pool: &NodeJsPool,
697    ) -> Result<Self::ResponseMessage> {
698        bail!("BasicEvaluateContext does not support request messages")
699    }
700
701    async fn finish(&self, _state: Self::State, _pool: &NodeJsPool) -> Result<()> {
702        Ok(())
703    }
704}
705
706pub fn scale_zero() {
707    NodeJsPool::scale_zero();
708}
709
710pub fn scale_down() {
711    NodeJsPool::scale_down();
712}
713
714async fn print_error(
715    error: StructuredError,
716    pool: &NodeJsPool,
717    evaluate_context: &impl EvaluateContext,
718) -> Result<String> {
719    error
720        .print(
721            *pool.assets_for_source_mapping,
722            pool.assets_root.clone(),
723            evaluate_context.cwd().owned().await?,
724            FormattingMode::Plain,
725        )
726        .await
727}
728/// An issue that occurred while evaluating node code.
729#[turbo_tasks::value(shared)]
730pub struct EvaluationIssue {
731    pub source: IssueSource,
732    pub error: StructuredError,
733    pub assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
734    pub assets_root: FileSystemPath,
735    pub root_path: FileSystemPath,
736}
737
738#[turbo_tasks::value_impl]
739impl Issue for EvaluationIssue {
740    #[turbo_tasks::function]
741    fn title(&self) -> Vc<StyledString> {
742        StyledString::Text(rcstr!("Error evaluating Node.js code")).cell()
743    }
744
745    #[turbo_tasks::function]
746    fn stage(&self) -> Vc<IssueStage> {
747        IssueStage::Transform.into()
748    }
749
750    #[turbo_tasks::function]
751    fn file_path(&self) -> Vc<FileSystemPath> {
752        self.source.file_path()
753    }
754
755    #[turbo_tasks::function]
756    async fn description(&self) -> Result<Vc<OptionStyledString>> {
757        Ok(Vc::cell(Some(
758            StyledString::Text(
759                self.error
760                    .print(
761                        *self.assets_for_source_mapping,
762                        self.assets_root.clone(),
763                        self.root_path.clone(),
764                        FormattingMode::Plain,
765                    )
766                    .await?
767                    .into(),
768            )
769            .resolved_cell(),
770        )))
771    }
772
773    #[turbo_tasks::function]
774    fn source(&self) -> Vc<OptionIssueSource> {
775        Vc::cell(Some(self.source))
776    }
777}