use std::{borrow::Cow, sync::Arc};
use cow_utils::CowUtils;
use derive_more::Debug;
use rspack_error::{Result, ToStringResultToRspackResultExt, error};
use rspack_fs::ReadableFileSystem;
use rspack_hook::define_hook;
use rspack_loader_runner::parse_resource;
use rspack_paths::{Utf8Path, Utf8PathBuf};
use rspack_util::node_path::NodePath;
use swc_core::common::util::take::Take;
use tracing::instrument;
use crate::{
BoxDependency, CompilationId, ContextElementDependency, ContextModule, ContextModuleOptions,
ContextModulePattern, DependencyCategory, DependencyId, DependencyType, GlobMatchOptions,
ModuleExt, ModuleFactory, ModuleFactoryCreateData, ModuleFactoryResult, ResolveArgs,
ResolveContextModuleDependencies, ResolveInnerOptions, ResolveOptionsWithDependencyType,
ResolveResult, Resolver, ResolverFactory, SharedPluginDriver, escape_glob_pattern,
extract_glob_base_dir, glob_match_normalized_with_explicit_dot, normalize_path_separators,
normalize_path_separators_for_path, resolve, unescape_glob_path, walk_dir,
};
#[derive(Debug)]
pub enum BeforeResolveResult {
Ignored,
Data(Box<BeforeResolveData>),
}
#[derive(Debug, Clone)]
pub struct BeforeResolveData {
pub context: String,
pub request: String,
pub dependencies: Vec<BoxDependency>,
pub recursive: bool,
pub pattern: ContextModulePattern,
}
#[derive(Clone)]
pub enum AfterResolveResult {
Ignored,
Data(Box<AfterResolveData>),
}
#[derive(Debug, Clone)]
pub struct AfterResolveData {
pub compilation_id: CompilationId,
pub resource: Utf8PathBuf,
pub context: String,
pub dependencies: Vec<BoxDependency>,
pub request: String,
pub recursive: bool,
pub pattern: ContextModulePattern,
#[debug(skip)]
pub resolve_dependencies: ResolveContextModuleDependencies,
}
define_hook!(ContextModuleFactoryBeforeResolve: SeriesWaterfall(data: BeforeResolveResult) -> BeforeResolveResult);
define_hook!(ContextModuleFactoryAfterResolve: SeriesWaterfall(data: AfterResolveResult) -> AfterResolveResult);
#[derive(Debug, Default)]
pub struct ContextModuleFactoryHooks {
pub before_resolve: ContextModuleFactoryBeforeResolveHook,
pub after_resolve: ContextModuleFactoryAfterResolveHook,
}
#[derive(Debug)]
pub struct ContextModuleFactory {
loader_resolver_factory: Arc<ResolverFactory>,
plugin_driver: SharedPluginDriver,
#[debug(skip)]
resolve_dependencies: ResolveContextModuleDependencies,
}
#[async_trait::async_trait]
impl ModuleFactory for ContextModuleFactory {
#[instrument("context_module_factory:create", skip_all)]
async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result<ModuleFactoryResult> {
match self.before_resolve(data).await? {
BeforeResolveResult::Ignored => return Ok(ModuleFactoryResult::default()),
BeforeResolveResult::Data(before_resolve_result) => {
let (factorize_result, context_module_options) =
self.resolve(data, before_resolve_result).await?;
if let Some(context_module_options) = context_module_options
&& let Some(factorize_result) = self.after_resolve(data, context_module_options).await?
{
return Ok(factorize_result);
}
Ok(factorize_result)
}
}
}
}
impl ContextModuleFactory {
pub fn new(
resolver_factory: Arc<ResolverFactory>,
loader_resolver_factory: Arc<ResolverFactory>,
plugin_driver: SharedPluginDriver,
) -> Self {
let resolve_dependencies: ResolveContextModuleDependencies = Arc::new(move |options| {
let resolver_factory = resolver_factory.clone();
Box::pin(async move {
tracing::trace!("resolving context module path {}", options.resource);
if options.resource.as_str().is_empty() {
return Ok(vec![]);
}
let resolver = &resolver_factory.get(ResolveOptionsWithDependencyType {
resolve_options: options
.resolve_options
.clone()
.map(|r| Box::new(Arc::unwrap_or_clone(r))),
resolve_to_context: false,
dependency_category: options.context_options.category,
});
let mut context_element_dependencies = vec![];
visit_dirs(
options.resource.as_str(),
&options.resource,
&mut context_element_dependencies,
&options,
&resolver.options(),
resolver.inner_fs(),
)
.await?;
context_element_dependencies.sort_by_cached_key(|d| d.user_request.clone());
tracing::trace!(
"resolving dependencies for {:?}",
context_element_dependencies
);
Ok(context_element_dependencies)
})
});
Self {
loader_resolver_factory,
plugin_driver,
resolve_dependencies,
}
}
async fn before_resolve(
&self,
data: &mut ModuleFactoryCreateData,
) -> Result<BeforeResolveResult> {
let dependency = data.dependencies[0]
.as_context_dependency_mut()
.expect("should be context dependency");
let dependency_options = dependency.options();
let before_resolve_data = BeforeResolveData {
context: data.context.to_string(),
request: dependency.request().to_string(),
recursive: dependency_options.recursive,
pattern: dependency_options.pattern.clone(),
dependencies: data.dependencies.clone(),
};
match self
.plugin_driver
.context_module_factory_hooks
.before_resolve
.call(BeforeResolveResult::Data(Box::new(before_resolve_data)))
.await?
{
BeforeResolveResult::Ignored => Ok(BeforeResolveResult::Ignored),
BeforeResolveResult::Data(mut result) => {
data.dependencies = result.dependencies.take();
Ok(BeforeResolveResult::Data(result))
}
}
}
fn get_loader_resolver(&self) -> Arc<Resolver> {
self
.loader_resolver_factory
.get(ResolveOptionsWithDependencyType {
resolve_options: None,
resolve_to_context: false,
dependency_category: DependencyCategory::CommonJS,
})
}
async fn resolve(
&self,
data: &mut ModuleFactoryCreateData,
before_resolve_data: Box<BeforeResolveData>,
) -> Result<(ModuleFactoryResult, Option<ContextModuleOptions>)> {
let plugin_driver = &self.plugin_driver;
let dependency = data.dependencies[0]
.as_context_dependency()
.expect("should be context dependency");
let request = before_resolve_data.request;
let (loader_request, specifier) = match request.rfind('!') {
Some(idx) => {
let mut loaders_prefix = String::new();
let mut i = 0;
let loaders_request = Cow::Borrowed(&request[..idx + 1]);
while i < loaders_request.len() && loaders_request.chars().nth(i) == Some('!') {
loaders_prefix.push('!');
i += 1;
}
let loaders_request = loaders_request.as_ref()[i..]
.trim_end_matches('!')
.cow_replace("!!", "!");
let loaders = if loaders_request.is_empty() {
vec![]
} else {
loaders_request.split('!').collect()
};
let resource = request[idx + 1..].to_string();
let mut loader_result = Vec::with_capacity(loaders.len());
let loader_resolver = self.get_loader_resolver();
for loader_request in loaders {
let resolve_result = loader_resolver
.resolve(data.context.as_ref(), loader_request)
.await
.to_rspack_result_with_message(|e| {
format!(
"Failed to resolve loader: {loader_request} in {} {e}",
data.context
)
})?;
match resolve_result {
ResolveResult::Resource(resource) => {
let resource = resource.full_path();
loader_result.push(resource);
}
ResolveResult::Ignored => {
let context = data.context.to_string();
return Err(error!(
"Failed to resolve loader: loader_request={loader_request}, context={context}"
));
}
}
}
let request = format!(
"{}{}{}",
loaders_prefix,
loader_result.join("!"),
if loader_result.is_empty() { "" } else { "!" }
);
(request, resource)
}
None => (String::new(), request),
};
let resolve_args = ResolveArgs {
context: before_resolve_data.context.into(),
importer: data.issuer_identifier.as_ref(),
issuer: data.issuer.as_deref(),
specifier: specifier.as_str(),
dependency_type: dependency.dependency_type(),
dependency_category: dependency.category(),
span: dependency.range(),
resolve_options: data.resolve_options.clone(),
resolve_to_context: true,
optional: dependency.get_optional(),
};
let (resource_data, resolve_dependencies) = resolve(resolve_args, plugin_driver).await;
let file_dependencies = resolve_dependencies.file_dependencies;
let missing_dependencies = resolve_dependencies.missing_dependencies;
let (module, context_module_options) = match resource_data {
Ok(ResolveResult::Resource(resource)) => {
let mut dependency_options = dependency.options().clone();
dependency_options.recursive = before_resolve_data.recursive;
dependency_options.pattern = before_resolve_data.pattern.clone();
let options = ContextModuleOptions {
addon: loader_request.clone(),
resource: resource.path,
resource_query: resource.query,
resource_fragment: resource.fragment,
layer: data.issuer_layer.clone(),
resolve_options: data.resolve_options.clone(),
context_options: dependency_options,
type_prefix: dependency.type_prefix(),
};
let module = ContextModule::new(self.resolve_dependencies.clone(), options.clone()).boxed();
(module, Some(options))
}
Ok(ResolveResult::Ignored) => {
let mut dependency_options = dependency.options().clone();
dependency_options.recursive = before_resolve_data.recursive;
dependency_options.pattern = before_resolve_data.pattern.clone();
let options = ContextModuleOptions {
addon: loader_request.clone(),
resource: Default::default(),
resource_query: Default::default(),
resource_fragment: Default::default(),
layer: data.issuer_layer.clone(),
resolve_options: data.resolve_options.clone(),
context_options: dependency_options,
type_prefix: dependency.type_prefix(),
};
let module = ContextModule::new(self.resolve_dependencies.clone(), options.clone()).boxed();
(module, Some(options))
}
Err(err) => {
data.add_file_dependencies(file_dependencies);
data.add_missing_dependencies(missing_dependencies);
return Err(err);
}
};
data.add_file_dependencies(file_dependencies);
data.add_missing_dependencies(missing_dependencies);
let module_factory_result = ModuleFactoryResult {
module: Some(module),
};
Ok((module_factory_result, context_module_options))
}
async fn after_resolve(
&self,
data: &mut ModuleFactoryCreateData,
mut context_module_options: ContextModuleOptions,
) -> Result<Option<ModuleFactoryResult>> {
let context_options = &context_module_options.context_options;
let after_resolve_data = AfterResolveData {
compilation_id: data.compilation_id,
resource: context_module_options.resource.clone(),
context: context_options.context.clone(),
dependencies: data.dependencies.clone(),
request: context_options.request.clone(),
pattern: context_options.pattern.clone(),
recursive: context_options.recursive,
resolve_dependencies: self.resolve_dependencies.clone(),
};
match self
.plugin_driver
.context_module_factory_hooks
.after_resolve
.call(AfterResolveResult::Data(Box::new(after_resolve_data)))
.await?
{
AfterResolveResult::Ignored => Ok(Some(ModuleFactoryResult::default())),
AfterResolveResult::Data(mut after_resolve_data) => {
data.dependencies = after_resolve_data.dependencies.take();
let parsed_resource = parse_resource(after_resolve_data.resource.as_str());
if let Some(parsed_resource) = parsed_resource {
if let Some(query) = &parsed_resource.query {
context_module_options.resource_query.clone_from(query);
}
if let Some(fragment) = &parsed_resource.fragment {
context_module_options
.resource_fragment
.clone_from(fragment);
}
}
context_module_options.resource = after_resolve_data.resource;
context_module_options.context_options.context = after_resolve_data.context;
context_module_options.context_options.pattern = after_resolve_data.pattern.clone();
context_module_options.context_options.recursive = after_resolve_data.recursive;
let module = ContextModule::new(
after_resolve_data.resolve_dependencies,
context_module_options.clone(),
)
.boxed();
Ok(Some(ModuleFactoryResult::new_with_module(module)))
}
}
}
}
async fn visit_dirs(
ctx: &str,
dir: &Utf8Path,
dependencies: &mut Vec<ContextElementDependency>,
options: &ContextModuleOptions,
resolve_options: &ResolveInnerOptions<'_>,
fs: Arc<dyn ReadableFileSystem>,
) -> Result<()> {
let include = &options.context_options.include;
let exclude = &options.context_options.exclude;
let matcher = ContextModuleMatcher::new(&options.context_options.pattern);
if matcher.is_empty() {
return Ok(());
}
let skip_dotfiles = !matches!(
options.context_options.pattern,
ContextModulePattern::Glob(_)
);
let resolved_glob_patterns =
if let ContextModulePattern::Glob(patterns) = &options.context_options.pattern {
Some(resolve_context_module_glob_patterns(
patterns,
&options.context_options.context,
ctx,
))
} else {
None
};
let is_import_meta_glob = resolved_glob_patterns.is_some();
let glob_exhaustive = options.context_options.glob_exhaustive;
walk_dir(
dir,
fs,
options.context_options.recursive,
skip_dotfiles,
&mut |path, dirname| {
if is_import_meta_glob
&& !glob_exhaustive
&& is_non_exhaustive_import_meta_glob_skipped_dir(dirname)
{
return false;
}
exclude
.as_ref()
.is_none_or(|exclude| !exclude.test(path.as_str()))
},
&mut |path, _filename| {
let path_str = path.as_str();
if let Some(exclude) = exclude
&& exclude.test(path_str)
{
return;
}
if let Some(include) = include
&& !include.test(path_str)
{
return;
}
let relative_path = {
let path_str = &path_str[ctx.len()..];
let p = path_str.cow_replace('\\', "/");
if p.as_ref().starts_with('/') {
format!(".{p}")
} else {
format!("./{p}")
}
};
if let Some(patterns) = &resolved_glob_patterns {
if let Some(user_request) = glob_user_request(patterns, path_str, glob_exhaustive)
&& !dependencies.iter().any(|d| d.user_request == user_request)
{
push_context_element_dependency(dependencies, options, &relative_path, &user_request);
}
} else {
let requests = alternative_requests(
resolve_options,
vec![AlternativeRequest::new(ctx.to_string(), relative_path)],
);
for r in &requests {
if matcher.matches(&r.request) {
push_context_element_dependency(dependencies, options, &r.request, &r.request);
}
}
}
},
)
.await
}
fn push_context_element_dependency(
dependencies: &mut Vec<ContextElementDependency>,
options: &ContextModuleOptions,
user_request: &str,
exposed_user_request: &str,
) {
let request = format!(
"{}{}{}{}",
options.addon, user_request, options.resource_query, options.resource_fragment,
);
let resource_identifier = ContextElementDependency::create_resource_identifier(
options.resource.as_str(),
&request,
options.context_options.attributes.as_ref(),
);
dependencies.push(ContextElementDependency {
id: DependencyId::new(),
request,
user_request: exposed_user_request.to_string(),
category: options.context_options.category,
context: options.resource.clone().into(),
layer: options.layer.clone(),
options: options.context_options.clone(),
resource_identifier,
attributes: options.context_options.attributes.clone(),
referenced_specifiers: options.context_options.referenced_specifiers.clone(),
dependency_type: DependencyType::ContextElement(options.type_prefix),
factorize_info: Default::default(),
});
}
#[derive(Debug)]
struct ResolvedContextModuleGlobPattern {
absolute_pattern: String,
base: String,
absolute_base: String,
negative: bool,
}
fn resolve_context_module_glob_patterns(
patterns: &[String],
context: &str,
common_base: &str,
) -> Vec<ResolvedContextModuleGlobPattern> {
patterns
.iter()
.map(|pattern| resolve_context_module_glob_pattern(pattern, context, common_base))
.collect()
}
fn resolve_context_module_glob_pattern(
pattern: &str,
context: &str,
common_base: &str,
) -> ResolvedContextModuleGlobPattern {
let (pattern, negative) = if let Some(pattern) = pattern.strip_prefix('!') {
(pattern, true)
} else {
(pattern, false)
};
let pattern = normalize_path_separators(pattern);
let (base, pattern_to_join) = if let Some(pattern_to_join) = pattern.strip_prefix('/') {
(
infer_glob_root_context(common_base, extract_glob_base_dir(&pattern)),
pattern_to_join,
)
} else {
(
if context.is_empty() {
common_base.to_string()
} else {
context.to_string()
},
pattern.as_str(),
)
};
let base = normalize_path_separators_for_path(&base);
let escaped_base = escape_glob_pattern(&base);
let absolute_pattern = Utf8Path::new(&escaped_base)
.node_join_posix(pattern_to_join)
.node_normalize_posix()
.to_string();
let absolute_pattern = normalize_path_separators(&absolute_pattern);
let base = extract_glob_base_dir(&pattern).to_string();
let absolute_base = unescape_glob_path(extract_glob_base_dir(&absolute_pattern));
ResolvedContextModuleGlobPattern {
absolute_pattern,
base,
absolute_base,
negative,
}
}
fn infer_glob_root_context(common_base: &str, pattern_base: &str) -> String {
let mut common_base = normalize_path_separators_for_path(common_base);
if !common_base.ends_with('/') {
common_base.push('/');
}
let pattern_base = pattern_base.trim_start_matches('/');
let mut matched_len = 0;
for idx in pattern_base
.char_indices()
.map(|(idx, _)| idx)
.chain(std::iter::once(pattern_base.len()))
{
if !pattern_base[..idx].ends_with('/') && idx != pattern_base.len() {
continue;
}
if common_base.ends_with(&pattern_base[..idx]) {
matched_len = idx;
}
}
common_base[..common_base.len() - matched_len].to_string()
}
fn is_non_exhaustive_import_meta_glob_skipped_dir(dirname: &str) -> bool {
dirname == "node_modules" || dirname.starts_with('.')
}
fn glob_user_request(
patterns: &[ResolvedContextModuleGlobPattern],
path: &str,
exhaustive: bool,
) -> Option<String> {
let normalized_path = normalize_path_separators_for_path(path);
let matched = patterns
.iter()
.filter(|pattern| !pattern.negative)
.find(|pattern| glob_pattern_matches(pattern, &normalized_path, exhaustive))?;
if patterns
.iter()
.filter(|pattern| pattern.negative)
.any(|pattern| glob_pattern_matches(pattern, &normalized_path, exhaustive))
{
return None;
}
let suffix = normalized_path
.strip_prefix(&matched.absolute_base)
.unwrap_or(normalized_path.as_str())
.trim_start_matches('/');
Some(
Utf8Path::new(&matched.base)
.node_join_posix(suffix)
.to_string(),
)
}
fn glob_pattern_matches(
pattern: &ResolvedContextModuleGlobPattern,
normalized_path: &str,
exhaustive: bool,
) -> bool {
glob_match_normalized_with_explicit_dot(
&pattern.absolute_pattern,
normalized_path,
&pattern.absolute_base,
&GlobMatchOptions {
require_literal_leading_dot: !exhaustive,
..Default::default()
},
)
}
struct ContextModuleMatcher<'a> {
pattern: &'a ContextModulePattern,
}
impl<'a> ContextModuleMatcher<'a> {
fn new(pattern: &'a ContextModulePattern) -> Self {
Self { pattern }
}
fn is_empty(&self) -> bool {
!matches!(self.pattern, ContextModulePattern::Glob(_)) && self.pattern.is_empty()
}
fn matches(&self, request: &str) -> bool {
if let Some(reg_exp) = self.pattern.reg_exp() {
reg_exp.test(request)
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct AlternativeRequest {
pub context: String,
pub request: String,
}
impl AlternativeRequest {
pub fn new(context: String, request: String) -> Self {
Self { context, request }
}
}
fn alternative_requests(
resolve_options: &ResolveInnerOptions,
mut items: Vec<AlternativeRequest>,
) -> Vec<AlternativeRequest> {
for item in std::mem::take(&mut items) {
if !resolve_options.is_enforce_extension_enabled() {
items.push(item.clone());
}
for ext in resolve_options.extensions() {
if item.request.ends_with(ext) {
items.push(AlternativeRequest::new(
item.context.clone(),
item.request[..(item.request.len() - ext.len())].to_string(),
));
}
}
}
for item in std::mem::take(&mut items) {
items.push(item.clone());
for main_file in resolve_options.main_files() {
if item.request.ends_with(&format!("/{main_file}")) {
items.push(AlternativeRequest::new(
item.context.clone(),
item.request[..(item.request.len() - main_file.len())].to_string(),
));
items.push(AlternativeRequest::new(
item.context.clone(),
item.request[..(item.request.len() - main_file.len() - 1)].to_string(),
));
}
}
}
for item in std::mem::take(&mut items) {
items.push(item.clone());
for module in resolve_options.modules() {
let dir = module.cow_replace('\\', "/");
if item.request.starts_with(&format!("./{dir}/")) {
items.push(AlternativeRequest::new(
item.context.clone(),
item.request[dir.len() + 3..].to_string(),
));
}
}
}
items
}