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