Skip to main content

turbopack_node/transforms/
webpack.rs

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