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