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