Skip to main content

rspack_core/
context_module_factory.rs

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  // context_info
34  // resolve_options
35  pub context: String,
36  pub request: String,
37  // assertions
38  pub dependencies: Vec<DependencyRef>,
39  // dependency_type
40  // file_dependencies
41  // missing_dependencies
42  // context_dependencies
43  // create_data
44  // cacheable
45  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  // layer
62  // resolve_options
63  // file_dependencies: HashSet<String>,
64  // missing_dependencies: HashSet<String>,
65  // context_dependencies: HashSet<String>,
66  pub request: String,
67  // mode
68  pub recursive: bool,
69  pub pattern: ContextModulePattern,
70  // namespace_object
71  // addon: String,
72  // chunk_name: String,
73  // include
74  // exclude
75  // group_options
76  // type_prefix: String,
77  // category: String,
78  // referenced_exports
79  #[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        // The dependencies can be modified  in the before resolve hook
203        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        // should create an empty context module when ignored
338        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    // data.add_context_dependencies(context_dependencies);
374
375    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        // The dependencies can be modified  in the after resolve hook
419        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      // FIXME: nodejs resolver return path of context, sometimes is '/a/b', sometimes is '/a/b/'
520      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        // Keep import.meta.glob Vite-compatible: expose only filesystem-matched
532        // paths, not resolver alternative requests like extensionless aliases.
533        // Revisit this branch if import.meta.glob compatibility changes.
534        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  // TODO: should respect fullySpecified resolve options
665  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    // webpack's `hideOriginal`: when the request points into a relative
697    // resolve.modules directory (e.g. `./node_modules/`), hide the original
698    // `./<dir>/...` request and emit only the bare specifier. The bare
699    // specifier can still be added under its own key if the context regExp
700    // matches it (same as webpack); but for the common `import('./dir/' + x)`
701    // the default `^\./` matcher rejects it, so the file drops out of the map.
702    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}