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#[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#[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 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 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 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 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 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 #[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 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 pub loader_names: Vec<RcStr>,
719}
720
721impl WebpackLoaderContext {
722 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 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 if turbo_tasks::turbo_tasks().is_tracking_dependencies() {
819 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 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 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 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 let evaluatable = ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(module)
968 .context("importModule: module is not evaluatable")?;
969
970 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 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 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 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 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 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#[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}