Skip to main content

turbopack_node/transforms/
webpack.rs

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