Skip to main content

turbopack_node/transforms/
webpack.rs

1use std::mem::take;
2
3use anyhow::{Context, Result, bail};
4use async_trait::async_trait;
5use base64::Engine;
6use bincode::{Decode, Encode};
7use either::Either;
8use futures::try_join;
9use serde::{Deserialize, Serialize};
10use serde_json::{Map as JsonMap, Value as JsonValue, json};
11use serde_with::serde_as;
12use tracing::Instrument;
13use turbo_rcstr::{RcStr, rcstr};
14use turbo_tasks::{
15    Completion, OperationVc, ReadRef, ResolvedVc, TryJoinIterExt, ValueToString, ValueToStringRef,
16    Vc, trace::TraceRawVcs,
17};
18use turbo_tasks_env::ProcessEnv;
19use turbo_tasks_fs::{
20    File, FileContent, FileSystemPath,
21    glob::{Glob, GlobOptions},
22    json::parse_json_with_source_context,
23    rope::Rope,
24};
25use turbopack_core::{
26    asset::{Asset, AssetContent},
27    chunk::{ChunkingContext, ChunkingContextExt, EvaluatableAsset},
28    context::{AssetContext, ProcessResult},
29    file_source::FileSource,
30    ident::AssetIdent,
31    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
32    module::Module,
33    module_graph::{
34        ModuleGraph, SingleModuleGraph,
35        chunk_group_info::{ChunkGroup, ChunkGroupEntry},
36    },
37    output::{ExpandOutputAssetsInput, OutputAsset, OutputAssets, expand_output_assets},
38    reference_type::{EcmaScriptModulesReferenceSubType, InnerAssets, ReferenceType},
39    resolve::{
40        ResolveErrorMode,
41        options::{ConditionValue, ResolveInPackage, ResolveIntoPackage, ResolveOptions},
42        origin::PlainResolveOrigin,
43        parse::Request,
44        pattern::Pattern,
45        resolve,
46    },
47    source::Source,
48    source_map::{GenerateSourceMap, utils::resolve_source_map_sources},
49    source_transform::SourceTransform,
50    virtual_source::VirtualSource,
51};
52use turbopack_resolve::{
53    ecmascript::{esm_resolve, get_condition_maps},
54    resolve::resolve_options,
55    resolve_options_context::ResolveOptionsContext,
56};
57
58use crate::{
59    AssetsForSourceMapping,
60    backend::NodeBackend,
61    debug::should_debug,
62    embed_js::embed_file_path,
63    evaluate::{
64        EnvVarTracking, EvaluateContext, EvaluateEntries, EvaluatePool, EvaluationIssue,
65        custom_evaluate, get_evaluate_entries, get_evaluate_pool,
66    },
67    execution_context::ExecutionContext,
68    format::FormattingMode,
69    source_map::{StackFrame, StructuredError},
70    transforms::util::{EmittedAsset, emitted_assets_to_virtual_sources},
71};
72
73#[serde_as]
74#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Encode, Decode)]
75struct BytesBase64 {
76    #[serde_as(as = "serde_with::base64::Base64")]
77    binary: Vec<u8>,
78}
79
80#[derive(Debug, Clone, Deserialize)]
81#[turbo_tasks::value]
82#[serde(rename_all = "camelCase")]
83struct WebpackLoadersProcessingResult {
84    #[serde(with = "either::serde_untagged")]
85    #[bincode(with = "turbo_bincode::either")]
86    #[turbo_tasks(debug_ignore, trace_ignore)]
87    source: Either<RcStr, BytesBase64>,
88    map: Option<RcStr>,
89    #[turbo_tasks(trace_ignore)]
90    assets: Option<Vec<EmittedAsset>>,
91}
92
93pub use turbopack_core::loader::{WebpackLoaderItem, WebpackLoaderItems};
94
95#[turbo_tasks::value]
96pub struct WebpackLoaders {
97    evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
98    execution_context: ResolvedVc<ExecutionContext>,
99    loaders: ResolvedVc<WebpackLoaderItems>,
100    rename_as: Option<RcStr>,
101    resolve_options_context: ResolvedVc<ResolveOptionsContext>,
102    source_maps: bool,
103}
104
105#[turbo_tasks::value_impl]
106impl WebpackLoaders {
107    #[turbo_tasks::function]
108    pub fn new(
109        evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
110        execution_context: ResolvedVc<ExecutionContext>,
111        loaders: ResolvedVc<WebpackLoaderItems>,
112        rename_as: Option<RcStr>,
113        resolve_options_context: ResolvedVc<ResolveOptionsContext>,
114        source_maps: bool,
115    ) -> Vc<Self> {
116        WebpackLoaders {
117            evaluate_context,
118            execution_context,
119            loaders,
120            rename_as,
121            resolve_options_context,
122            source_maps,
123        }
124        .cell()
125    }
126}
127
128#[turbo_tasks::value_impl]
129impl SourceTransform for WebpackLoaders {
130    #[turbo_tasks::function]
131    fn transform(
132        self: ResolvedVc<Self>,
133        source: ResolvedVc<Box<dyn Source>>,
134        asset_context: ResolvedVc<Box<dyn AssetContext>>,
135    ) -> Vc<Box<dyn Source>> {
136        Vc::upcast(
137            WebpackLoadersProcessedAsset {
138                transform: self,
139                source,
140                asset_context,
141            }
142            .cell(),
143        )
144    }
145}
146
147#[turbo_tasks::value]
148struct WebpackLoadersProcessedAsset {
149    transform: ResolvedVc<WebpackLoaders>,
150    source: ResolvedVc<Box<dyn Source>>,
151    asset_context: ResolvedVc<Box<dyn AssetContext>>,
152}
153
154#[turbo_tasks::value_impl]
155impl Source for WebpackLoadersProcessedAsset {
156    #[turbo_tasks::function]
157    async fn ident(&self) -> Result<Vc<AssetIdent>> {
158        Ok(
159            if let Some(rename_as) = self.transform.await?.rename_as.as_deref() {
160                self.source
161                    .ident()
162                    .owned()
163                    .await?
164                    .rename_as(rename_as)
165                    .into_vc()
166            } else {
167                self.source.ident()
168            },
169        )
170    }
171
172    #[turbo_tasks::function]
173    async fn description(&self) -> Result<Vc<RcStr>> {
174        let inner = self.source.description().await?;
175        let loaders = self.transform.await?.loaders.await?;
176        let loader_names: Vec<&str> = loaders.iter().map(|l| l.loader.as_str()).collect();
177        Ok(Vc::cell(
178            format!(
179                "loaders [{}] transform of {}",
180                loader_names.join(", "),
181                inner
182            )
183            .into(),
184        ))
185    }
186}
187
188#[turbo_tasks::value_impl]
189impl Asset for WebpackLoadersProcessedAsset {
190    #[turbo_tasks::function]
191    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
192        Ok(*self.process().await?.content)
193    }
194}
195
196#[turbo_tasks::value_impl]
197impl GenerateSourceMap for WebpackLoadersProcessedAsset {
198    #[turbo_tasks::function]
199    async fn generate_source_map(self: Vc<Self>) -> Result<Vc<FileContent>> {
200        Ok(*self.process().await?.source_map)
201    }
202}
203
204#[turbo_tasks::value]
205struct ProcessWebpackLoadersResult {
206    content: ResolvedVc<AssetContent>,
207    source_map: ResolvedVc<FileContent>,
208    assets: Vec<ResolvedVc<VirtualSource>>,
209}
210
211#[turbo_tasks::function]
212async fn webpack_loaders_executor(
213    evaluate_context: Vc<Box<dyn AssetContext>>,
214) -> Result<Vc<ProcessResult>> {
215    Ok(evaluate_context.process(
216        Vc::upcast(FileSource::new(
217            embed_file_path(rcstr!("transforms/webpack-loaders.ts"))
218                .owned()
219                .await?,
220        )),
221        ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
222    ))
223}
224
225#[turbo_tasks::value_impl]
226impl WebpackLoadersProcessedAsset {
227    #[turbo_tasks::function]
228    async fn process(&self) -> Result<Vc<ProcessWebpackLoadersResult>> {
229        let transform = self.transform.await?;
230        let loaders = transform.loaders.await?;
231
232        let webpack_span = tracing::info_span!(
233            "webpack loader",
234            name = display(ReadRef::<WebpackLoaderItems>::as_raw_ref(&loaders))
235        );
236
237        async {
238            let ExecutionContext {
239                project_path,
240                chunking_context,
241                env,
242                node_backend,
243            } = &*transform.execution_context.await?;
244            let source_content = self.source.content();
245            let AssetContent::File(file) = *source_content.await? else {
246                bail!("Webpack Loaders transform only support transforming files");
247            };
248            let FileContent::Content(file_content) = &*file.await? else {
249                return Ok(ProcessWebpackLoadersResult {
250                    content: AssetContent::File(FileContent::NotFound.resolved_cell())
251                        .resolved_cell(),
252                    assets: Vec::new(),
253                    source_map: FileContent::NotFound.resolved_cell(),
254                }
255                .cell());
256            };
257
258            // If the content is not a valid string (e.g. binary file), handle the error and pass a
259            // Buffer to Webpack instead of a Base64 string so the build process doesn't crash.
260            let content: JsonValue = match file_content.content().to_str() {
261                Ok(utf8_str) => utf8_str.to_string().into(),
262                Err(_) => JsonValue::Object(JsonMap::from_iter(std::iter::once((
263                    "binary".to_string(),
264                    JsonValue::from(
265                        base64::engine::general_purpose::STANDARD
266                            .encode(file_content.content().to_bytes()),
267                    ),
268                )))),
269            };
270            let evaluate_context = transform.evaluate_context;
271
272            let webpack_loaders_executor = webpack_loaders_executor(*evaluate_context).module();
273
274            let entries = get_evaluate_entries(
275                webpack_loaders_executor,
276                *evaluate_context,
277                **node_backend,
278                None,
279            )
280            .to_resolved()
281            .await?;
282
283            let module_graph = ModuleGraph::from_graphs(
284                vec![SingleModuleGraph::new_with_entries(
285                    entries.graph_entries().to_resolved().await?,
286                    false,
287                    false,
288                )],
289                None,
290            )
291            .connect()
292            .to_resolved()
293            .await?;
294
295            let source_ident = self.source.ident().await?;
296            let resource_fs_path = &source_ident.path;
297            let Some(resource_path) = project_path.get_relative_path_to(resource_fs_path) else {
298                bail!(
299                    "Resource path \"{}\" needs to be on project filesystem \"{}\"",
300                    resource_fs_path,
301                    project_path
302                );
303            };
304            let loader_names: Vec<RcStr> = loaders.iter().map(|l| l.loader.clone()).collect();
305            let config_value = evaluate_webpack_loader(WebpackLoaderContext {
306                entries,
307                cwd: project_path.clone(),
308                env: *env,
309                node_backend: *node_backend,
310                context_source_for_issue: self.source,
311                chunking_context: *chunking_context,
312                evaluate_context: transform.evaluate_context,
313                module_graph,
314                resolve_options_context: Some(transform.resolve_options_context),
315                asset_context: self.asset_context,
316                args: vec![
317                    ResolvedVc::cell(content),
318                    // We need to pass the query string to the loader
319                    ResolvedVc::cell(resource_path.to_string().into()),
320                    ResolvedVc::cell(self.source.ident().await?.query.to_string().into()),
321                    ResolvedVc::cell(json!(*loaders)),
322                    ResolvedVc::cell(transform.source_maps.into()),
323                ],
324                additional_invalidation: Completion::immutable().to_resolved().await?,
325                loader_names,
326            })
327            .await?;
328
329            let Some(val) = &*config_value else {
330                // An error happened, which has already been converted into an issue.
331                return Ok(ProcessWebpackLoadersResult {
332                    content: AssetContent::File(FileContent::NotFound.resolved_cell())
333                        .resolved_cell(),
334                    assets: Vec::new(),
335                    source_map: FileContent::NotFound.resolved_cell(),
336                }
337                .cell());
338            };
339            let processed: WebpackLoadersProcessingResult = parse_json_with_source_context(val)
340                .context(
341                    "Unable to deserializate response from webpack loaders transform operation",
342                )?;
343
344            // handle SourceMap
345            let source_map = if !transform.source_maps {
346                None
347            } else {
348                processed
349                    .map
350                    .map(|source_map| Rope::from(source_map.into_owned()))
351            };
352            let source_map =
353                resolve_source_map_sources(source_map.as_ref(), resource_fs_path).await?;
354
355            let file = match processed.source {
356                Either::Left(str) => File::from(str),
357                Either::Right(bytes) => File::from(bytes.binary),
358            };
359            let assets = emitted_assets_to_virtual_sources(processed.assets).await?;
360
361            let content =
362                AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
363            Ok(ProcessWebpackLoadersResult {
364                content,
365                assets,
366                source_map: if let Some(source_map) = source_map {
367                    FileContent::Content(File::from(source_map)).resolved_cell()
368                } else {
369                    FileContent::NotFound.resolved_cell()
370                },
371            }
372            .cell())
373        }
374        .instrument(webpack_span)
375        .await
376    }
377}
378
379#[turbo_tasks::function]
380pub(crate) async fn evaluate_webpack_loader(
381    webpack_loader_context: WebpackLoaderContext,
382) -> Result<Vc<Option<RcStr>>> {
383    custom_evaluate(webpack_loader_context).await
384}
385
386#[derive(Deserialize, Debug, PartialEq, Eq, Encode, Decode)]
387#[serde(rename_all = "camelCase")]
388enum LogType {
389    Error,
390    Warn,
391    Info,
392    Log,
393    Debug,
394    Trace,
395    Group,
396    GroupCollapsed,
397    GroupEnd,
398    Profile,
399    ProfileEnd,
400    Time,
401    Clear,
402    Status,
403}
404
405#[derive(Deserialize, Debug, PartialEq, Eq, Encode, Decode)]
406#[serde(rename_all = "camelCase")]
407pub struct LogInfo {
408    time: u64,
409    log_type: LogType,
410    #[bincode(with = "turbo_bincode::serde_self_describing")]
411    args: Vec<JsonValue>,
412    trace: Option<Vec<StackFrame<'static>>>,
413}
414
415#[derive(Deserialize, Debug)]
416#[serde(tag = "type", rename_all = "camelCase")]
417pub enum InfoMessage {
418    // Sent to inform Turbopack about the dependencies of the task.
419    // All fields are `default` since it is ok for the client to
420    // simply omit instead of sending empty arrays.
421    #[serde(rename_all = "camelCase")]
422    Dependencies {
423        #[serde(default)]
424        env_variables: Vec<RcStr>,
425        #[serde(default)]
426        file_paths: Vec<RcStr>,
427        #[serde(default)]
428        directories: Vec<(RcStr, RcStr)>,
429        #[serde(default)]
430        build_file_paths: Vec<RcStr>,
431    },
432    EmittedError {
433        severity: IssueSeverity,
434        error: StructuredError,
435    },
436    Log {
437        logs: Vec<LogInfo>,
438    },
439}
440
441#[turbo_tasks::task_input]
442#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, TraceRawVcs, Encode, Decode)]
443#[serde(rename_all = "camelCase")]
444pub struct WebpackResolveOptions {
445    alias_fields: Option<Vec<RcStr>>,
446    condition_names: Option<Vec<RcStr>>,
447    no_package_json: bool,
448    extensions: Option<Vec<RcStr>>,
449    main_fields: Option<Vec<RcStr>>,
450    no_exports_field: bool,
451    main_files: Option<Vec<RcStr>>,
452    no_modules: bool,
453    prefer_relative: bool,
454}
455
456#[derive(Deserialize, Debug)]
457#[serde(tag = "type", rename_all = "camelCase")]
458pub enum RequestMessage {
459    #[serde(rename_all = "camelCase")]
460    Resolve {
461        options: WebpackResolveOptions,
462        lookup_path: RcStr,
463        request: RcStr,
464    },
465    #[serde(rename_all = "camelCase")]
466    TrackFileRead { file: RcStr },
467    #[serde(rename_all = "camelCase")]
468    ImportModule { lookup_path: RcStr, request: RcStr },
469}
470
471#[derive(Serialize, Debug)]
472#[serde(rename_all = "camelCase")]
473pub struct ImportModuleChunk {
474    path: RcStr,
475    #[serde(skip_serializing_if = "Option::is_none")]
476    code: Option<RcStr>,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    binary: Option<String>,
479    #[serde(skip_serializing_if = "Option::is_none")]
480    source_map: Option<RcStr>,
481}
482
483#[derive(Serialize, Debug)]
484#[serde(untagged)]
485pub enum ResponseMessage {
486    Resolve {
487        path: RcStr,
488    },
489    // Only used for tracking invalidations, no content is returned.
490    TrackFileRead {},
491    #[serde(rename_all = "camelCase")]
492    ImportModule {
493        entry_path: RcStr,
494        chunks: Vec<ImportModuleChunk>,
495    },
496}
497
498#[turbo_tasks::task_input]
499#[derive(Clone, PartialEq, Eq, Hash, Debug, TraceRawVcs, Encode, Decode)]
500pub struct WebpackLoaderContext {
501    pub entries: ResolvedVc<EvaluateEntries>,
502    pub cwd: FileSystemPath,
503    pub env: ResolvedVc<Box<dyn ProcessEnv>>,
504    pub node_backend: ResolvedVc<Box<dyn NodeBackend>>,
505    pub context_source_for_issue: ResolvedVc<Box<dyn Source>>,
506    pub module_graph: ResolvedVc<ModuleGraph>,
507    pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
508    pub evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
509    pub resolve_options_context: Option<ResolvedVc<ResolveOptionsContext>>,
510    pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
511    pub args: Vec<ResolvedVc<JsonValue>>,
512    pub additional_invalidation: ResolvedVc<Completion>,
513    /// Names of the loaders being applied to the source, in pipeline order.
514    /// Used to enrich error messages and issue details so users know which
515    /// loader chain was running when an error occurred.
516    pub loader_names: Vec<RcStr>,
517}
518
519impl WebpackLoaderContext {
520    /// Format the loader chain as "loaders [a, b, c]" for inclusion in
521    /// error messages and issue detail text. Returns `None` if there are
522    /// no loaders, which should not normally happen but keeps the helper
523    /// well-defined.
524    fn loader_chain_description(&self) -> Option<RcStr> {
525        if self.loader_names.is_empty() {
526            None
527        } else {
528            Some(
529                format!(
530                    "loaders [{}]",
531                    self.loader_names
532                        .iter()
533                        .map(|n| n.as_str())
534                        .collect::<Vec<_>>()
535                        .join(", ")
536                )
537                .into(),
538            )
539        }
540    }
541}
542
543impl EvaluateContext for WebpackLoaderContext {
544    type InfoMessage = InfoMessage;
545    type RequestMessage = RequestMessage;
546    type ResponseMessage = ResponseMessage;
547    type State = Vec<LogInfo>;
548
549    fn pool(&self) -> OperationVc<EvaluatePool> {
550        get_evaluate_pool(
551            self.entries,
552            self.cwd.clone(),
553            self.env,
554            self.node_backend,
555            self.chunking_context,
556            self.module_graph,
557            self.additional_invalidation,
558            should_debug("webpack_loader"),
559            // Env vars are read untracked, since we want a more granular dependency on certain env
560            // vars only. So the runtime code tracks which env vars are read and send a dependency
561            // message for them.
562            EnvVarTracking::Untracked,
563        )
564    }
565
566    fn args(&self) -> &[ResolvedVc<serde_json::Value>] {
567        &self.args
568    }
569
570    fn cwd(&self) -> Vc<turbo_tasks_fs::FileSystemPath> {
571        self.cwd.clone().cell()
572    }
573
574    fn keep_alive(&self) -> bool {
575        true
576    }
577
578    fn crash_context_prefix(&self) -> Option<RcStr> {
579        self.loader_chain_description()
580    }
581
582    async fn emit_error(&self, error: StructuredError, pool: &EvaluatePool) -> Result<()> {
583        EvaluationIssue {
584            error,
585            source: IssueSource::from_source_only(self.context_source_for_issue),
586            assets_for_source_mapping: pool.assets_for_source_mapping,
587            assets_root: pool.assets_root.clone(),
588            root_path: self.chunking_context.root_path().owned().await?,
589            detail: self.loader_chain_description(),
590        }
591        .resolved_cell()
592        .emit();
593        Ok(())
594    }
595
596    async fn info(
597        &self,
598        state: &mut Self::State,
599        data: Self::InfoMessage,
600        pool: &EvaluatePool,
601    ) -> Result<()> {
602        match data {
603            InfoMessage::Dependencies {
604                env_variables,
605                file_paths,
606                directories,
607                build_file_paths,
608            } => {
609                // We only process these dependencies to help with tracking, so if it is disabled
610                // dont bother.
611                if turbo_tasks::turbo_tasks().is_tracking_dependencies() {
612                    // Track dependencies of the loader task
613                    // TODO: Because these are reported _after_ the loader actually read the
614                    // dependency there is a race condition where we may miss
615                    // updates that race with the loader execution.
616
617                    // Track all the subscriptions in parallel, since certain loaders like tailwind
618                    // might add thousands of subscriptions.
619                    let env_subscriptions = env_variables
620                        .iter()
621                        .map(|e| self.env.read(e.clone()))
622                        .try_join();
623                    let file_subscriptions = file_paths
624                        .iter()
625                        .map(|p| async move { self.cwd.join(p)?.read().await })
626                        .try_join();
627                    let directory_subscriptions = directories
628                        .iter()
629                        .map(|(dir, glob)| async move {
630                            self.cwd
631                                .join(dir)?
632                                .track_glob(Glob::new(glob.clone(), GlobOptions::default()), false)
633                                .await
634                        })
635                        .try_join();
636                    try_join!(
637                        env_subscriptions,
638                        file_subscriptions,
639                        directory_subscriptions
640                    )?;
641
642                    for build_path in build_file_paths {
643                        let build_path = self.cwd.join(&build_path)?;
644                        BuildDependencyIssue {
645                            source: IssueSource::from_source_only(self.context_source_for_issue),
646                            path: build_path,
647                        }
648                        .resolved_cell()
649                        .emit();
650                    }
651                }
652            }
653            InfoMessage::EmittedError { error, severity } => {
654                EvaluateEmittedErrorIssue {
655                    source: IssueSource::from_source_only(self.context_source_for_issue),
656                    error,
657                    severity,
658                    assets_for_source_mapping: pool.assets_for_source_mapping,
659                    assets_root: pool.assets_root.clone(),
660                    project_dir: self.chunking_context.root_path().owned().await?,
661                }
662                .resolved_cell()
663                .emit();
664            }
665            InfoMessage::Log { logs } => {
666                state.extend(logs);
667            }
668        }
669        Ok(())
670    }
671
672    async fn request(
673        &self,
674        _state: &mut Self::State,
675        data: Self::RequestMessage,
676        _pool: &EvaluatePool,
677    ) -> Result<Self::ResponseMessage> {
678        match data {
679            RequestMessage::Resolve {
680                options: webpack_options,
681                lookup_path,
682                request,
683            } => {
684                let Some(resolve_options_context) = self.resolve_options_context else {
685                    bail!("Resolve options are not available in this context");
686                };
687                let lookup_path = self.cwd.join(&lookup_path)?;
688                let request = Request::parse(Pattern::Constant(request));
689                let options = resolve_options(lookup_path.clone(), *resolve_options_context);
690
691                let options = apply_webpack_resolve_options(options, webpack_options);
692
693                let resolved = resolve(
694                    lookup_path.clone(),
695                    ReferenceType::Undefined,
696                    request,
697                    options,
698                );
699
700                if let Some(source) = resolved.await?.first_source() {
701                    if let Some(path) = self.cwd.get_relative_path_to(&source.ident().await?.path) {
702                        Ok(ResponseMessage::Resolve { path })
703                    } else {
704                        bail!(
705                            "Resolving {} in {} ends up on a different filesystem",
706                            request.to_string().await?,
707                            lookup_path.to_string_ref().await?
708                        );
709                    }
710                } else {
711                    bail!(
712                        "Unable to resolve {} in {}",
713                        request.to_string().await?,
714                        lookup_path.to_string_ref().await?
715                    );
716                }
717            }
718            RequestMessage::TrackFileRead { file } => {
719                // Ignore result, we read on the JS side again to prevent some IPC overhead. Still
720                // await the read though to cover at least one class of race conditions.
721                let _ = &*self.cwd.join(&file)?.read().await?;
722                Ok(ResponseMessage::TrackFileRead {})
723            }
724            RequestMessage::ImportModule {
725                lookup_path,
726                request,
727            } => {
728                let lookup_path = self.cwd.join(&lookup_path)?;
729
730                let request_vc = Request::parse(Pattern::Constant(request.clone()));
731                let origin = PlainResolveOrigin::new(*self.asset_context, lookup_path.join("_")?);
732                let resolved = esm_resolve(
733                    Vc::upcast(origin),
734                    request_vc,
735                    EcmaScriptModulesReferenceSubType::ImportModule,
736                    ResolveErrorMode::Error,
737                    Some(IssueSource::from_source_only(self.context_source_for_issue)),
738                )
739                .await?;
740
741                let Some(module) = resolved.await?.first_module().await? else {
742                    bail!(
743                        "importModule: unable to resolve {} in {}",
744                        request,
745                        lookup_path.to_string_ref().await?
746                    );
747                };
748
749                // Cast to evaluatable asset for bundle generation
750                let evaluatable = ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(module)
751                    .context("importModule: module is not evaluatable")?;
752
753                // Build a module graph from the resolved module and its
754                // transitive dependencies
755                let single_graph = SingleModuleGraph::new_with_entry(
756                    ChunkGroupEntry::Entry(vec![module]),
757                    false,
758                    false,
759                );
760                let import_module_graph = ModuleGraph::from_graphs(vec![single_graph], None)
761                    .connect()
762                    .to_resolved()
763                    .await?;
764
765                // Generate a full Node.js bundle using the real runtime
766                let entry_path = self
767                    .chunking_context
768                    .chunk_path(
769                        None,
770                        module.ident(),
771                        Some(rcstr!("importModule")),
772                        rcstr!(".js"),
773                    )
774                    .owned()
775                    .await?;
776
777                let bootstrap = self.chunking_context.root_entry_chunk_group_asset(
778                    entry_path.clone(),
779                    ChunkGroup::Entry(vec![ResolvedVc::upcast(evaluatable)]),
780                    *import_module_graph,
781                    OutputAssets::empty(),
782                    OutputAssets::empty(),
783                );
784
785                // Collect all internal assets as {path, code} pairs
786                let bootstrap_resolved = bootstrap.to_resolved().await?;
787                let all_assets = expand_output_assets(
788                    std::iter::once(ExpandOutputAssetsInput::Asset(bootstrap_resolved)),
789                    true,
790                )
791                .await?;
792                let output_root = self.chunking_context.output_root().owned().await?;
793
794                let mut chunks = Vec::new();
795                for asset in all_assets {
796                    let asset_path = asset.path().owned().await?;
797                    if !asset_path.is_inside_ref(&output_root) {
798                        continue;
799                    }
800                    let Some(rel_path) = output_root.get_path_to(&asset_path) else {
801                        continue;
802                    };
803                    // Skip source map files
804                    if rel_path.ends_with(".map") {
805                        continue;
806                    }
807                    let content = asset.content().await?;
808                    let AssetContent::File(file_vc) = *content else {
809                        continue;
810                    };
811                    let file_content = file_vc.await?;
812                    let FileContent::Content(file) = &*file_content else {
813                        continue;
814                    };
815
816                    if rel_path.ends_with(".js") {
817                        // JavaScript chunk — send as text
818                        let code: RcStr = file.content().to_str()?.into_owned().into();
819                        chunks.push(ImportModuleChunk {
820                            path: rel_path.into(),
821                            code: Some(code),
822                            binary: None,
823                            source_map: None,
824                        });
825                    } else {
826                        // Binary asset (wasm, images, etc.) — send base64-encoded
827                        let bytes = file.content().to_bytes();
828                        let encoded = base64::engine::general_purpose::STANDARD.encode(&*bytes);
829                        chunks.push(ImportModuleChunk {
830                            path: rel_path.into(),
831                            code: None,
832                            binary: Some(encoded),
833                            source_map: None,
834                        });
835                    }
836                }
837
838                let entry_rel = output_root
839                    .get_path_to(&entry_path)
840                    .context("entry path should be inside output root")?;
841
842                Ok(ResponseMessage::ImportModule {
843                    entry_path: entry_rel.into(),
844                    chunks,
845                })
846            }
847        }
848    }
849
850    async fn finish(&self, state: Self::State, pool: &EvaluatePool) -> Result<()> {
851        let has_errors = state.iter().any(|log| log.log_type == LogType::Error);
852        let has_warnings = state.iter().any(|log| log.log_type == LogType::Warn);
853        if has_errors || has_warnings {
854            let logs = state
855                .into_iter()
856                .filter(|log| {
857                    matches!(
858                        log.log_type,
859                        LogType::Error
860                            | LogType::Warn
861                            | LogType::Info
862                            | LogType::Log
863                            | LogType::Clear,
864                    )
865                })
866                .collect();
867
868            EvaluateErrorLoggingIssue {
869                source: IssueSource::from_source_only(self.context_source_for_issue),
870                logging: logs,
871                severity: if has_errors {
872                    IssueSeverity::Error
873                } else {
874                    IssueSeverity::Warning
875                },
876                assets_for_source_mapping: pool.assets_for_source_mapping,
877                assets_root: pool.assets_root.clone(),
878                project_dir: self.chunking_context.root_path().owned().await?,
879            }
880            .resolved_cell()
881            .emit();
882        }
883        Ok(())
884    }
885}
886
887#[turbo_tasks::function]
888async fn apply_webpack_resolve_options(
889    resolve_options: Vc<ResolveOptions>,
890    webpack_resolve_options: WebpackResolveOptions,
891) -> Result<Vc<ResolveOptions>> {
892    let mut resolve_options = resolve_options.owned().await?;
893    if let Some(alias_fields) = webpack_resolve_options.alias_fields {
894        let mut old = resolve_options
895            .in_package
896            .extract_if(0.., |field| {
897                matches!(field, ResolveInPackage::AliasField(..))
898            })
899            .collect::<Vec<_>>();
900        for field in alias_fields {
901            if &*field == "..." {
902                resolve_options.in_package.extend(take(&mut old));
903            } else {
904                resolve_options
905                    .in_package
906                    .push(ResolveInPackage::AliasField(field));
907            }
908        }
909    }
910    if let Some(condition_names) = webpack_resolve_options.condition_names {
911        for conditions in get_condition_maps(&mut resolve_options) {
912            let mut old = take(conditions);
913            for name in &condition_names {
914                if name == "..." {
915                    conditions.extend(take(&mut old));
916                } else {
917                    conditions.insert(name.clone(), ConditionValue::Set);
918                }
919            }
920        }
921    }
922    if webpack_resolve_options.no_package_json {
923        resolve_options.into_package.retain(|item| {
924            !matches!(
925                item,
926                ResolveIntoPackage::ExportsField { .. } | ResolveIntoPackage::MainField { .. }
927            )
928        });
929    }
930    if let Some(mut extensions) = webpack_resolve_options.extensions {
931        if let Some(pos) = extensions.iter().position(|ext| ext == "...") {
932            extensions.splice(pos..=pos, take(&mut resolve_options.extensions));
933        }
934        resolve_options.extensions = extensions;
935    }
936    if let Some(main_fields) = webpack_resolve_options.main_fields {
937        let mut old = resolve_options
938            .into_package
939            .extract_if(0.., |field| {
940                matches!(field, ResolveIntoPackage::MainField { .. })
941            })
942            .collect::<Vec<_>>();
943        for field in main_fields {
944            if &*field == "..." {
945                resolve_options.into_package.extend(take(&mut old));
946            } else {
947                resolve_options
948                    .into_package
949                    .push(ResolveIntoPackage::MainField { field });
950            }
951        }
952    }
953    if webpack_resolve_options.no_exports_field {
954        resolve_options
955            .into_package
956            .retain(|field| !matches!(field, ResolveIntoPackage::ExportsField { .. }));
957    }
958    if let Some(main_files) = webpack_resolve_options.main_files {
959        resolve_options.default_files = main_files;
960    }
961    if webpack_resolve_options.no_modules {
962        resolve_options.modules.clear();
963    }
964    if webpack_resolve_options.prefer_relative {
965        resolve_options.prefer_relative = true;
966    }
967    Ok(resolve_options.cell())
968}
969
970/// An issue that occurred while evaluating node code.
971#[turbo_tasks::value(shared)]
972pub struct BuildDependencyIssue {
973    pub path: FileSystemPath,
974    pub source: IssueSource,
975}
976
977#[async_trait]
978#[turbo_tasks::value_impl]
979impl Issue for BuildDependencyIssue {
980    fn severity(&self) -> IssueSeverity {
981        IssueSeverity::Warning
982    }
983
984    async fn title(&self) -> Result<StyledString> {
985        Ok(StyledString::Text(rcstr!(
986            "Build dependencies are not yet supported"
987        )))
988    }
989
990    fn stage(&self) -> IssueStage {
991        IssueStage::Unsupported
992    }
993
994    async fn file_path(&self) -> Result<FileSystemPath> {
995        self.source.file_path().await
996    }
997
998    async fn description(&self) -> Result<Option<StyledString>> {
999        Ok(Some(StyledString::Line(vec![
1000            StyledString::Text(rcstr!("The file at ")),
1001            StyledString::Code(self.path.to_string().into()),
1002            StyledString::Text(
1003                " is a build dependency, which is not yet implemented.
1004    Changing this file or any dependency will not be recognized and might require restarting the \
1005                 server"
1006                    .into(),
1007            ),
1008        ])))
1009    }
1010
1011    fn source(&self) -> Option<IssueSource> {
1012        Some(self.source)
1013    }
1014}
1015
1016#[turbo_tasks::value(shared)]
1017pub struct EvaluateEmittedErrorIssue {
1018    pub source: IssueSource,
1019    pub severity: IssueSeverity,
1020    pub error: StructuredError,
1021    pub assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
1022    pub assets_root: FileSystemPath,
1023    pub project_dir: FileSystemPath,
1024}
1025
1026#[async_trait]
1027#[turbo_tasks::value_impl]
1028impl Issue for EvaluateEmittedErrorIssue {
1029    async fn file_path(&self) -> Result<FileSystemPath> {
1030        self.source.file_path().await
1031    }
1032
1033    fn stage(&self) -> IssueStage {
1034        IssueStage::Transform
1035    }
1036
1037    fn severity(&self) -> IssueSeverity {
1038        self.severity
1039    }
1040
1041    async fn title(&self) -> Result<StyledString> {
1042        Ok(StyledString::Text(rcstr!("Issue while running loader")))
1043    }
1044
1045    async fn description(&self) -> Result<Option<StyledString>> {
1046        Ok(Some(StyledString::Text(
1047            self.error
1048                .print(
1049                    *self.assets_for_source_mapping,
1050                    self.assets_root.clone(),
1051                    self.project_dir.clone(),
1052                    FormattingMode::Plain,
1053                )
1054                .await?
1055                .into(),
1056        )))
1057    }
1058
1059    fn source(&self) -> Option<IssueSource> {
1060        Some(self.source)
1061    }
1062}
1063
1064#[turbo_tasks::value(shared)]
1065pub struct EvaluateErrorLoggingIssue {
1066    pub source: IssueSource,
1067    pub severity: IssueSeverity,
1068    #[turbo_tasks(trace_ignore)]
1069    pub logging: Vec<LogInfo>,
1070    pub assets_for_source_mapping: ResolvedVc<AssetsForSourceMapping>,
1071    pub assets_root: FileSystemPath,
1072    pub project_dir: FileSystemPath,
1073}
1074
1075#[async_trait]
1076#[turbo_tasks::value_impl]
1077impl Issue for EvaluateErrorLoggingIssue {
1078    async fn file_path(&self) -> Result<FileSystemPath> {
1079        self.source.file_path().await
1080    }
1081
1082    fn stage(&self) -> IssueStage {
1083        IssueStage::Transform
1084    }
1085
1086    fn severity(&self) -> IssueSeverity {
1087        self.severity
1088    }
1089
1090    async fn title(&self) -> Result<StyledString> {
1091        Ok(StyledString::Text(rcstr!(
1092            "Error logging while running loader"
1093        )))
1094    }
1095
1096    async fn description(&self) -> Result<Option<StyledString>> {
1097        fn fmt_args(prefix: String, args: &[JsonValue]) -> String {
1098            let mut iter = args.iter();
1099            let Some(first) = iter.next() else {
1100                return "".to_string();
1101            };
1102            let mut result = prefix;
1103            if let JsonValue::String(s) = first {
1104                result.push_str(s);
1105            } else {
1106                result.push_str(&first.to_string());
1107            }
1108            for arg in iter {
1109                result.push(' ');
1110                result.push_str(&arg.to_string());
1111            }
1112            result
1113        }
1114        let lines = self
1115            .logging
1116            .iter()
1117            .map(|log| match log.log_type {
1118                LogType::Error => {
1119                    StyledString::Strong(fmt_args("<e> ".to_string(), &log.args).into())
1120                }
1121                LogType::Warn => StyledString::Text(fmt_args("<w> ".to_string(), &log.args).into()),
1122                LogType::Info => StyledString::Text(fmt_args("<i> ".to_string(), &log.args).into()),
1123                LogType::Log => StyledString::Text(fmt_args("<l> ".to_string(), &log.args).into()),
1124                LogType::Clear => StyledString::Strong(rcstr!("---")),
1125                _ => {
1126                    unimplemented!("{:?} is not implemented", log.log_type)
1127                }
1128            })
1129            .collect::<Vec<_>>();
1130        Ok(Some(StyledString::Stack(lines)))
1131    }
1132
1133    fn source(&self) -> Option<IssueSource> {
1134        Some(self.source)
1135    }
1136}