1use std::{borrow::Cow, sync::Arc};
2
3use cow_utils::CowUtils;
4use derive_more::Debug;
5use rspack_error::{Result, ToStringResultToRspackResultExt, error};
6use rspack_fs::ReadableFileSystem;
7use rspack_hook::define_hook;
8use rspack_loader_runner::parse_resource;
9use rspack_paths::{Utf8Path, Utf8PathBuf};
10use swc_core::common::util::take::Take;
11use tracing::instrument;
12
13use self::glob::ContextModuleGlobMatcher;
14pub use self::glob::{CompiledContextModuleGlobRequest, compile_context_module_glob_request};
15use crate::{
16 CompilationId, ContextElementDependency, ContextMode, ContextModule, ContextModuleOptions,
17 ContextModulePattern, DependencyCategory, DependencyId, DependencyRef, DependencyType, ModuleExt,
18 ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult, OverrideStrict, ResolveArgs,
19 ResolveContextModuleDependencies, ResolveInnerOptions, ResolveOptionsWithDependencyType,
20 ResolveResult, Resolver, ResolverFactory, SharedPluginDriver, resolve, walk_dir,
21};
22
23mod glob;
24
25#[derive(Debug)]
26pub enum BeforeResolveResult {
27 Ignored,
28 Data(Box<BeforeResolveData>),
29}
30
31#[derive(Debug, Clone)]
32pub struct BeforeResolveData {
33 pub context: String,
36 pub request: String,
37 pub dependencies: Vec<DependencyRef>,
39 pub recursive: bool,
46 pub pattern: ContextModulePattern,
47}
48
49#[derive(Clone)]
50pub enum AfterResolveResult {
51 Ignored,
52 Data(Box<AfterResolveData>),
53}
54
55#[derive(Debug, Clone)]
56pub struct AfterResolveData {
57 pub compilation_id: CompilationId,
58 pub resource: Utf8PathBuf,
59 pub context: String,
60 pub dependencies: Vec<DependencyRef>,
61 pub request: String,
67 pub recursive: bool,
69 pub pattern: ContextModulePattern,
70 #[debug(skip)]
80 pub resolve_dependencies: ResolveContextModuleDependencies,
81}
82
83define_hook!(ContextModuleFactoryBeforeResolve: SeriesWaterfall(data: BeforeResolveResult) -> BeforeResolveResult);
84define_hook!(ContextModuleFactoryAfterResolve: SeriesWaterfall(data: AfterResolveResult) -> AfterResolveResult);
85
86#[derive(Debug, Default)]
87pub struct ContextModuleFactoryHooks {
88 pub before_resolve: ContextModuleFactoryBeforeResolveHook,
89 pub after_resolve: ContextModuleFactoryAfterResolveHook,
90}
91
92#[derive(Debug)]
93pub struct ContextModuleFactory {
94 loader_resolver_factory: Arc<ResolverFactory>,
95 plugin_driver: SharedPluginDriver,
96 #[debug(skip)]
97 resolve_dependencies: ResolveContextModuleDependencies,
98}
99
100struct ContextModuleAfterResolveInput {
101 options: ContextModuleOptions,
102 resolve_context: String,
103}
104
105#[async_trait::async_trait]
106impl ModuleFactory for ContextModuleFactory {
107 #[instrument("context_module_factory:create", skip_all)]
108 async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result<ModuleFactoryResult> {
109 match self.before_resolve(data).await? {
110 BeforeResolveResult::Ignored => return Ok(ModuleFactoryResult::default()),
111 BeforeResolveResult::Data(before_resolve_result) => {
112 let (factorize_result, after_resolve_input) =
113 self.resolve(data, before_resolve_result).await?;
114 if let Some(after_resolve_input) = after_resolve_input
115 && let Some(factorize_result) = self.after_resolve(data, after_resolve_input).await?
116 {
117 return Ok(factorize_result);
118 }
119
120 Ok(factorize_result)
121 }
122 }
123 }
124}
125
126impl ContextModuleFactory {
127 pub fn new(
128 resolver_factory: Arc<ResolverFactory>,
129 loader_resolver_factory: Arc<ResolverFactory>,
130 plugin_driver: SharedPluginDriver,
131 ) -> Self {
132 let resolve_dependencies: ResolveContextModuleDependencies = Arc::new(move |options| {
133 let resolver_factory = resolver_factory.clone();
134 Box::pin(async move {
135 tracing::trace!("resolving context module path {}", options.resource);
136 if options.resource.as_str().is_empty() {
137 return Ok(vec![]);
138 }
139
140 let resolver = &resolver_factory.get(ResolveOptionsWithDependencyType {
141 resolve_options: options
142 .resolve_options
143 .clone()
144 .map(|r| Box::new(Arc::unwrap_or_clone(r))),
145 resolve_to_context: false,
146 dependency_category: options.context_options.category,
147 });
148 let mut context_element_dependencies = vec![];
149 visit_dirs(
150 options.resource.as_str(),
151 &options.resource,
152 &mut context_element_dependencies,
153 &options,
154 &resolver.options(),
155 resolver.inner_fs(),
156 )
157 .await?;
158 context_element_dependencies.sort_by_cached_key(|d| d.user_request.clone());
159
160 tracing::trace!(
161 "resolving dependencies for {:?}",
162 context_element_dependencies
163 );
164
165 Ok(context_element_dependencies)
166 })
167 });
168
169 Self {
170 loader_resolver_factory,
171 plugin_driver,
172 resolve_dependencies,
173 }
174 }
175
176 async fn before_resolve(
177 &self,
178 data: &mut ModuleFactoryCreateData,
179 ) -> Result<BeforeResolveResult> {
180 let dependency = data.dependencies[0]
181 .as_context_dependency()
182 .expect("should be context dependency");
183 let dependency_options = dependency.options();
184
185 let before_resolve_data = BeforeResolveData {
186 context: data.context.to_string(),
187 request: dependency.request().to_string(),
188 recursive: dependency_options.recursive,
189 pattern: dependency_options.pattern.clone(),
190 dependencies: data.dependencies.clone(),
191 };
192
193 match self
194 .plugin_driver
195 .context_module_factory_hooks
196 .before_resolve
197 .call(BeforeResolveResult::Data(Box::new(before_resolve_data)))
198 .await?
199 {
200 BeforeResolveResult::Ignored => Ok(BeforeResolveResult::Ignored),
201 BeforeResolveResult::Data(mut result) => {
202 data.dependencies = result.dependencies.take();
204 Ok(BeforeResolveResult::Data(result))
205 }
206 }
207 }
208
209 fn get_loader_resolver(&self) -> Arc<Resolver> {
210 self
211 .loader_resolver_factory
212 .get(ResolveOptionsWithDependencyType {
213 resolve_options: None,
214 resolve_to_context: false,
215 dependency_category: DependencyCategory::CommonJS,
216 })
217 }
218
219 async fn resolve(
220 &self,
221 data: &mut ModuleFactoryCreateData,
222 before_resolve_data: Box<BeforeResolveData>,
223 ) -> Result<(ModuleFactoryResult, Option<ContextModuleAfterResolveInput>)> {
224 let plugin_driver = &self.plugin_driver;
225 let strict = self.global_override_strict();
226 let dependency = data.dependencies[0]
227 .as_context_dependency()
228 .expect("should be context dependency");
229 let hook_request = before_resolve_data.request.clone();
230 let request = before_resolve_data.request;
231 let (loader_request, specifier) = match request.rfind('!') {
232 Some(idx) => {
233 let mut loaders_prefix = String::new();
234 let mut i = 0;
235
236 let loaders_request = Cow::Borrowed(&request[..idx + 1]);
237 while i < loaders_request.len() && loaders_request.chars().nth(i) == Some('!') {
238 loaders_prefix.push('!');
239 i += 1;
240 }
241 let loaders_request = loaders_request.as_ref()[i..]
242 .trim_end_matches('!')
243 .cow_replace("!!", "!");
244
245 let loaders = if loaders_request.is_empty() {
246 vec![]
247 } else {
248 loaders_request.split('!').collect()
249 };
250 let resource = request[idx + 1..].to_string();
251
252 let mut loader_result = Vec::with_capacity(loaders.len());
253 let loader_resolver = self.get_loader_resolver();
254 for loader_request in loaders {
255 let resolve_result = loader_resolver
256 .resolve(data.context.as_ref(), loader_request)
257 .await
258 .to_rspack_result_with_message(|e| {
259 format!(
260 "Failed to resolve loader: {loader_request} in {} {e}",
261 data.context
262 )
263 })?;
264 match resolve_result {
265 ResolveResult::Resource(resource) => {
266 let resource = resource.full_path();
267 loader_result.push(resource);
268 }
269 ResolveResult::Ignored => {
270 let context = data.context.to_string();
271 return Err(error!(
272 "Failed to resolve loader: loader_request={loader_request}, context={context}"
273 ));
274 }
275 }
276 }
277 let request = format!(
278 "{}{}{}",
279 loaders_prefix,
280 loader_result.join("!"),
281 if loader_result.is_empty() { "" } else { "!" }
282 );
283 (request, resource)
284 }
285 None => (String::new(), request),
286 };
287
288 let context = before_resolve_data.context;
289 let recursive = before_resolve_data.recursive;
290 let is_glob = matches!(&before_resolve_data.pattern, ContextModulePattern::Glob(_));
291 let resolve_args = ResolveArgs {
292 context: context.clone().into(),
293 importer: data.issuer_identifier.as_ref(),
294 issuer: data.issuer.as_deref(),
295 specifier: specifier.as_str(),
296 dependency_type: dependency.dependency_type(),
297 dependency_category: dependency.category(),
298 span: dependency.range(),
299 resolve_options: data.resolve_options.clone(),
300 resolve_to_context: true,
301 optional: dependency.get_optional(),
302 };
303
304 let (resource_data, resolve_dependencies) = resolve(resolve_args, plugin_driver).await;
305 let file_dependencies = resolve_dependencies.file_dependencies;
306 let missing_dependencies = resolve_dependencies.missing_dependencies;
307
308 let (module, context_module_options) = match resource_data {
309 Ok(ResolveResult::Resource(resource)) => {
310 let mut dependency_options = dependency.options().clone();
311 dependency_options.request = hook_request.clone();
312 dependency_options.recursive = recursive;
313 dependency_options.pattern = before_resolve_data.pattern.clone();
314 if !is_glob {
315 dependency_options.context = context.clone();
316 }
317
318 let options = ContextModuleOptions {
319 addon: loader_request.clone(),
320 resource: resource.path,
321 resource_query: resource.query,
322 resource_fragment: resource.fragment,
323 layer: data.issuer_layer.clone(),
324 resolve_options: data.resolve_options.clone(),
325 context_options: dependency_options,
326 type_prefix: dependency.type_prefix(),
327 };
328 let module = ContextModule::new_with_strict(
329 self.resolve_dependencies.clone(),
330 options.clone(),
331 strict,
332 )
333 .boxed();
334 (module, Some(options))
335 }
336 Ok(ResolveResult::Ignored) => {
337 let mut dependency_options = dependency.options().clone();
339 dependency_options.request = hook_request;
340 dependency_options.recursive = recursive;
341 dependency_options.pattern = before_resolve_data.pattern.clone();
342 if !is_glob {
343 dependency_options.context = context.clone();
344 }
345
346 let options = ContextModuleOptions {
347 addon: loader_request.clone(),
348 resource: Default::default(),
349 resource_query: Default::default(),
350 resource_fragment: Default::default(),
351 layer: data.issuer_layer.clone(),
352 resolve_options: data.resolve_options.clone(),
353 context_options: dependency_options,
354 type_prefix: dependency.type_prefix(),
355 };
356 let module = ContextModule::new_with_strict(
357 self.resolve_dependencies.clone(),
358 options.clone(),
359 strict,
360 )
361 .boxed();
362 (module, Some(options))
363 }
364 Err(err) => {
365 data.add_file_dependencies(file_dependencies);
366 data.add_missing_dependencies(missing_dependencies);
367 return Err(err);
368 }
369 };
370
371 data.add_file_dependencies(file_dependencies);
372 data.add_missing_dependencies(missing_dependencies);
373 let module_factory_result = ModuleFactoryResult {
376 module: Some(module),
377 };
378 Ok((
379 module_factory_result,
380 context_module_options.map(|options| ContextModuleAfterResolveInput {
381 options,
382 resolve_context: context,
383 }),
384 ))
385 }
386
387 async fn after_resolve(
388 &self,
389 data: &mut ModuleFactoryCreateData,
390 input: ContextModuleAfterResolveInput,
391 ) -> Result<Option<ModuleFactoryResult>> {
392 let ContextModuleAfterResolveInput {
393 mut options,
394 resolve_context,
395 } = input;
396 let context_module_options = &mut options;
397 let context_options = &context_module_options.context_options;
398 let after_resolve_data = AfterResolveData {
399 compilation_id: data.compilation_id,
400 resource: context_module_options.resource.clone(),
401 context: resolve_context,
402 dependencies: data.dependencies.clone(),
403 request: context_options.request.clone(),
404 pattern: context_options.pattern.clone(),
405 recursive: context_options.recursive,
406 resolve_dependencies: self.resolve_dependencies.clone(),
407 };
408
409 match self
410 .plugin_driver
411 .context_module_factory_hooks
412 .after_resolve
413 .call(AfterResolveResult::Data(Box::new(after_resolve_data)))
414 .await?
415 {
416 AfterResolveResult::Ignored => Ok(Some(ModuleFactoryResult::default())),
417 AfterResolveResult::Data(mut after_resolve_data) => {
418 data.dependencies = after_resolve_data.dependencies.take();
420
421 let parsed_resource = parse_resource(after_resolve_data.resource.as_str());
422 if let Some(parsed_resource) = parsed_resource {
423 if let Some(query) = &parsed_resource.query {
424 context_module_options.resource_query.clone_from(query);
425 }
426 if let Some(fragment) = &parsed_resource.fragment {
427 context_module_options
428 .resource_fragment
429 .clone_from(fragment);
430 }
431 }
432
433 context_module_options.resource = after_resolve_data.resource;
434 if !matches!(&after_resolve_data.pattern, ContextModulePattern::Glob(_)) {
435 context_module_options.context_options.context = after_resolve_data.context;
436 }
437 context_module_options.context_options.pattern = after_resolve_data.pattern.clone();
438 context_module_options.context_options.recursive = after_resolve_data.recursive;
439
440 let module = ContextModule::new_with_strict(
441 after_resolve_data.resolve_dependencies,
442 context_module_options.clone(),
443 self.global_override_strict(),
444 )
445 .boxed();
446
447 Ok(Some(ModuleFactoryResult::new_with_module(module)))
448 }
449 }
450 }
451
452 fn global_override_strict(&self) -> Option<bool> {
453 self
454 .plugin_driver
455 .options
456 .module
457 .parser
458 .as_ref()
459 .and_then(|parser| parser.get("javascript"))
460 .and_then(|parser| parser.get_javascript())
461 .and_then(|options| options.override_strict)
462 .map(|strict| matches!(strict, OverrideStrict::Strict))
463 }
464}
465
466async fn visit_dirs(
467 ctx: &str,
468 dir: &Utf8Path,
469 dependencies: &mut Vec<ContextElementDependency>,
470 options: &ContextModuleOptions,
471 resolve_options: &ResolveInnerOptions<'_>,
472 fs: Arc<dyn ReadableFileSystem>,
473) -> Result<()> {
474 let include = &options.context_options.include;
475 let exclude = &options.context_options.exclude;
476 let matcher = ContextModuleMatcher::new(options);
477 if matcher.is_empty() {
478 return Ok(());
479 }
480 let is_import_meta_glob = matcher.is_glob();
481 let skip_dotfiles = !is_import_meta_glob;
482 let glob_exhaustive = options.context_options.glob_exhaustive;
483
484 walk_dir(
485 dir,
486 fs,
487 options.context_options.recursive,
488 skip_dotfiles,
489 &mut |path, dirname| {
490 if is_import_meta_glob && !matcher.should_visit_dir(path.as_str()) {
491 return false;
492 }
493 if is_import_meta_glob
494 && !glob_exhaustive
495 && is_non_exhaustive_import_meta_glob_skipped_dir(dirname)
496 && !matcher.should_visit_skipped_dir(path.as_str())
497 {
498 return false;
499 }
500 exclude
501 .as_ref()
502 .is_none_or(|exclude| !exclude.test(path.as_str()))
503 },
504 &mut |path, _filename| {
505 let path_str = path.as_str();
506
507 if let Some(exclude) = exclude
508 && exclude.test(path_str)
509 {
510 return;
511 }
512
513 if let Some(include) = include
514 && !include.test(path_str)
515 {
516 return;
517 }
518
519 let relative_path = {
521 let path_str = &path_str[ctx.len()..];
522 let p = path_str.cow_replace('\\', "/");
523 if p.as_ref().starts_with('/') {
524 format!(".{p}")
525 } else {
526 format!("./{p}")
527 }
528 };
529
530 if is_import_meta_glob {
531 if let Some(user_request) = matcher.match_request(&relative_path, path_str)
535 && !dependencies.iter().any(|d| d.user_request == user_request)
536 {
537 push_context_element_dependency(dependencies, options, &relative_path, &user_request);
538 }
539 } else {
540 let requests = alternative_requests(
541 resolve_options,
542 vec![AlternativeRequest::new(ctx.to_string(), relative_path)],
543 );
544 for r in &requests {
545 if let Some(user_request) = matcher.match_request(&r.request, path_str) {
546 push_context_element_dependency(dependencies, options, &r.request, &user_request);
547 }
548 }
549 }
550 },
551 )
552 .await
553}
554
555fn push_context_element_dependency(
556 dependencies: &mut Vec<ContextElementDependency>,
557 options: &ContextModuleOptions,
558 user_request: &str,
559 exposed_user_request: &str,
560) {
561 let request = format!(
562 "{}{}{}{}",
563 options.addon, user_request, options.resource_query, options.resource_fragment,
564 );
565 let resource_identifier = ContextElementDependency::create_resource_identifier(
566 options.resource.as_str(),
567 &request,
568 options.context_options.attributes.as_ref(),
569 );
570
571 dependencies.push(ContextElementDependency {
572 id: DependencyId::new(),
573 weak: matches!(
574 options.context_options.mode,
575 ContextMode::AsyncWeak | ContextMode::Weak
576 ),
577 request,
578 user_request: exposed_user_request.to_string(),
579 category: options.context_options.category,
580 context: options.resource.clone().into(),
581 layer: options.layer.clone(),
582 resource_identifier,
583 attributes: options.context_options.attributes.clone(),
584 referenced_specifiers: options.context_options.referenced_specifiers.clone(),
585 dependency_type: DependencyType::ContextElement(options.type_prefix),
586 });
587}
588
589fn is_non_exhaustive_import_meta_glob_skipped_dir(dirname: &str) -> bool {
590 dirname == "node_modules" || dirname.starts_with('.')
591}
592
593struct ContextModuleMatcher<'a> {
594 pattern: &'a ContextModulePattern,
595 glob: Option<ContextModuleGlobMatcher<'a>>,
596}
597
598impl<'a> ContextModuleMatcher<'a> {
599 fn new(options: &'a ContextModuleOptions) -> Self {
600 let context_options = &options.context_options;
601 Self {
602 pattern: &context_options.pattern,
603 glob: ContextModuleGlobMatcher::new(options),
604 }
605 }
606
607 fn is_empty(&self) -> bool {
608 match self.pattern {
609 ContextModulePattern::Glob(_) => self
610 .glob
611 .as_ref()
612 .is_none_or(ContextModuleGlobMatcher::is_empty),
613 _ => self.pattern.is_empty(),
614 }
615 }
616
617 fn is_glob(&self) -> bool {
618 self.glob.is_some()
619 }
620
621 fn match_request(&self, request: &str, resource_path: &str) -> Option<String> {
622 if let Some(glob) = &self.glob {
623 return glob.match_request(resource_path);
624 }
625
626 if let Some(reg_exp) = self.pattern.reg_exp() {
627 reg_exp.test(request).then(|| request.to_string())
628 } else {
629 None
630 }
631 }
632
633 fn should_visit_skipped_dir(&self, path: &str) -> bool {
634 self
635 .glob
636 .as_ref()
637 .is_some_and(|glob| glob.should_visit_skipped_dir(path))
638 }
639
640 fn should_visit_dir(&self, path: &str) -> bool {
641 self
642 .glob
643 .as_ref()
644 .is_none_or(|glob| glob.should_visit_dir(path))
645 }
646}
647
648#[derive(Debug, Clone)]
649pub struct AlternativeRequest {
650 pub context: String,
651 pub request: String,
652}
653
654impl AlternativeRequest {
655 pub fn new(context: String, request: String) -> Self {
656 Self { context, request }
657 }
658}
659
660fn alternative_requests(
661 resolve_options: &ResolveInnerOptions,
662 mut items: Vec<AlternativeRequest>,
663) -> Vec<AlternativeRequest> {
664 for item in std::mem::take(&mut items) {
666 if !resolve_options.is_enforce_extension_enabled() {
667 items.push(item.clone());
668 }
669 for ext in resolve_options.extensions() {
670 if item.request.ends_with(ext) {
671 items.push(AlternativeRequest::new(
672 item.context.clone(),
673 item.request[..(item.request.len() - ext.len())].to_string(),
674 ));
675 }
676 }
677 }
678
679 for item in std::mem::take(&mut items) {
680 items.push(item.clone());
681 for main_file in resolve_options.main_files() {
682 if item.request.ends_with(&format!("/{main_file}")) {
683 items.push(AlternativeRequest::new(
684 item.context.clone(),
685 item.request[..(item.request.len() - main_file.len())].to_string(),
686 ));
687 items.push(AlternativeRequest::new(
688 item.context.clone(),
689 item.request[..(item.request.len() - main_file.len() - 1)].to_string(),
690 ));
691 }
692 }
693 }
694
695 for item in std::mem::take(&mut items) {
696 let mut hide_original = false;
703 for module in resolve_options.modules() {
704 let dir = module.cow_replace('\\', "/");
705 if item.request.starts_with(&format!("./{dir}/")) {
706 items.push(AlternativeRequest::new(
707 item.context.clone(),
708 item.request[dir.len() + 3..].to_string(),
709 ));
710 hide_original = true;
711 }
712 }
713 if !hide_original {
714 items.push(item);
715 }
716 }
717
718 items
719}