use std::{
borrow::Cow,
cell::OnceCell,
hash::{Hash, Hasher},
path::Path,
};
use cow_utils::CowUtils;
use rspack_core::{ChunkGraph, Compilation, OutputOptions, contextify};
use rspack_error::Result;
use rspack_hash::RspackHash;
use rspack_paths::Utf8Path;
use rustc_hash::FxHashMap as HashMap;
use sugar_path::SugarPath;
use crate::{ModuleFilenameTemplateFn, ModuleFilenameTemplateFnCtx, SourceReference};
fn get_before<'a>(s: &'a str, token: &str) -> &'a str {
match s.rfind(token) {
Some(idx) => &s[..idx],
None => "",
}
}
fn get_after<'a>(s: &'a str, token: &str) -> &'a str {
s.find(token).map(|idx| &s[idx..]).unwrap_or_default()
}
fn get_hash(text: &str, output_options: &OutputOptions) -> String {
let OutputOptions {
hash_function,
hash_salt,
..
} = output_options;
let mut hasher = RspackHash::with_salt(hash_function, hash_salt);
text.as_bytes().hash(&mut hasher);
let mut buf = format!("{:x}", hasher.finish());
buf.truncate(4);
buf
}
pub struct ModuleFilenameHelpers;
fn resolve_relative_resource_path(
absolute_resource_path: &str,
source_map_path: Option<&Utf8Path>,
) -> Option<String> {
if absolute_resource_path.starts_with("webpack/") {
return Some(absolute_resource_path.to_string());
}
let Some(source_map_path) = source_map_path else {
return None;
};
let Some(parent) = source_map_path.parent() else {
return Some(
absolute_resource_path
.normalize()
.to_string_lossy()
.cow_replace("\\", "/")
.to_string(),
);
};
Some(
Path::new(absolute_resource_path)
.relative(parent)
.to_string_lossy()
.cow_replace("\\", "/")
.to_string(),
)
}
impl ModuleFilenameHelpers {
fn create_module_filename_template_fn_ctx(
source_reference: &SourceReference,
compilation: &Compilation,
output_options: &OutputOptions,
namespace: &str,
unresolved_source_map_path: Option<&Utf8Path>,
) -> ModuleFilenameTemplateFnCtx {
let Compilation { options, .. } = compilation;
let context = &options.context;
match source_reference {
SourceReference::Module(module_identifier) => {
let module_graph = compilation.get_module_graph();
let module = module_graph
.module_by_identifier(module_identifier)
.unwrap_or_else(|| {
panic!("failed to find a module for the given identifier '{module_identifier}'")
});
let short_identifier = module.readable_identifier(context).to_string();
let identifier = contextify(context, module_identifier);
let module_id =
ChunkGraph::get_module_id(&compilation.module_ids_artifact, *module_identifier)
.map(|s| s.to_string())
.unwrap_or_default();
let absolute_resource_path = module
.identifier()
.split('!')
.next_back()
.unwrap_or("")
.to_string();
let hash = get_hash(&identifier, output_options);
let resource = short_identifier
.split('!')
.next_back()
.unwrap_or("")
.to_string();
let relative_resource_path = Some(resource.clone());
let loaders = get_before(&short_identifier, "!").to_string();
let all_loaders = get_before(&identifier, "!").to_string();
let query = get_after(&resource, "?").to_string();
let q = query.len();
let resource_path = if q == 0 {
resource.clone()
} else {
resource[..resource.len().saturating_sub(q)].to_string()
};
ModuleFilenameTemplateFnCtx {
short_identifier,
identifier,
module_id,
absolute_resource_path,
relative_resource_path,
hash,
resource,
loaders,
all_loaders,
query,
resource_path,
namespace: namespace.to_string(),
}
}
SourceReference::Source(source) => {
let short_identifier = contextify(context, source);
let identifier = short_identifier.clone();
let hash = get_hash(&identifier, output_options);
let resource = short_identifier
.split('!')
.next_back()
.unwrap_or("")
.to_string();
let loaders = get_before(&short_identifier, "!").to_string();
let all_loaders = get_before(&identifier, "!").to_string();
let query = get_after(&resource, "?").to_string();
let q = query.len();
let resource_path = if q == 0 {
resource.clone()
} else {
resource[..resource.len().saturating_sub(q)].to_string()
};
let absolute_resource_path = source.split('!').next_back().unwrap_or("").to_string();
let relative_resource_path =
resolve_relative_resource_path(&absolute_resource_path, unresolved_source_map_path);
ModuleFilenameTemplateFnCtx {
short_identifier,
identifier,
module_id: String::new(),
absolute_resource_path,
relative_resource_path,
hash,
resource,
loaders,
all_loaders,
query,
resource_path,
namespace: namespace.to_string(),
}
}
}
}
pub async fn create_filename_of_fn_template(
source_reference: &SourceReference,
compilation: &Compilation,
module_filename_template: &ModuleFilenameTemplateFn,
output_options: &OutputOptions,
namespace: &str,
unresolved_source_map_path: Option<&Utf8Path>,
) -> Result<String> {
let ctx = ModuleFilenameHelpers::create_module_filename_template_fn_ctx(
source_reference,
compilation,
output_options,
namespace,
unresolved_source_map_path,
);
module_filename_template(ctx).await
}
pub fn create_filename_of_string_template(
source_reference: &SourceReference,
compilation: &Compilation,
module_filename_template: &str,
output_options: &OutputOptions,
namespace: &str,
unresolved_source_map_path: Option<&Utf8Path>,
) -> String {
let ctx = ModuleFilenameHelpers::create_module_filename_template_string_ctx(
source_reference,
compilation,
output_options,
namespace,
unresolved_source_map_path,
);
template_replace(module_filename_template, &ctx)
}
pub fn replace_duplicates<F>(filenames: Vec<String>, mut fn_replace: F) -> Vec<String>
where
F: FnMut(String, usize, usize) -> String,
{
let mut count_map: HashMap<String, Vec<usize>> = HashMap::default();
let mut pos_map: HashMap<String, usize> = HashMap::default();
for (idx, item) in filenames.iter().enumerate() {
count_map.entry(item.clone()).or_default().push(idx);
pos_map.entry(item.clone()).or_insert(0);
}
filenames
.into_iter()
.enumerate()
.map(|(i, item)| {
let count = count_map
.get(&item)
.expect("should have a count entry in count_map");
if count.len() > 1 {
let pos = pos_map
.get_mut(&item)
.expect("should have a position entry in pos_map");
let result = fn_replace(item, i, *pos);
*pos += 1;
result
} else {
item
}
})
.collect()
}
fn create_module_filename_template_string_ctx<'a>(
source_reference: &'a SourceReference,
compilation: &'a Compilation,
output_options: &'a OutputOptions,
namespace: &'a str,
unresolved_source_map_path: Option<&'a Utf8Path>,
) -> ModuleFilenameTemplateStringCtx<'a> {
ModuleFilenameTemplateStringCtx {
source_reference,
compilation,
output_options,
namespace,
unresolved_source_map_path,
short_identifier: Default::default(),
identifier: Default::default(),
}
}
}
struct ModuleFilenameTemplateStringCtx<'a> {
source_reference: &'a SourceReference,
compilation: &'a Compilation,
output_options: &'a OutputOptions,
namespace: &'a str,
unresolved_source_map_path: Option<&'a Utf8Path>,
short_identifier: OnceCell<Cow<'a, str>>,
identifier: OnceCell<Cow<'a, str>>,
}
impl<'a> ModuleFilenameTemplateStringCtx<'a> {
pub fn short_identifier(&self) -> &str {
self.short_identifier.get_or_init(|| {
let Compilation { options, .. } = self.compilation;
let context = &options.context;
match &self.source_reference {
SourceReference::Module(module_identifier) => {
let module_graph = self.compilation.get_module_graph();
let module = module_graph
.module_by_identifier(module_identifier)
.unwrap_or_else(|| {
panic!("failed to find a module for the given identifier '{module_identifier}'")
});
module.readable_identifier(context)
}
SourceReference::Source(source) => Cow::Owned(contextify(context, source)),
}
})
}
pub fn identifier(&self) -> &str {
let Compilation { options, .. } = self.compilation;
let context = &options.context;
match &self.source_reference {
SourceReference::Module(module_identifier) => self
.identifier
.get_or_init(|| Cow::Owned(contextify(context, module_identifier))),
SourceReference::Source(_) => {
self.short_identifier()
}
}
}
pub fn module_id(&self) -> &str {
match &self.source_reference {
SourceReference::Module(module_identifier) => {
ChunkGraph::get_module_id(&self.compilation.module_ids_artifact, *module_identifier)
.map(|s| s.as_str())
.unwrap_or_default()
}
SourceReference::Source(_) => "",
}
}
pub fn absolute_resource_path(&self) -> &str {
match &self.source_reference {
SourceReference::Module(module_identifier) => {
module_identifier.split('!').next_back().unwrap_or("")
}
SourceReference::Source(source) => source.split('!').next_back().unwrap_or(""),
}
}
pub fn relative_resource_path(&self) -> Option<Cow<'_, str>> {
match &self.source_reference {
SourceReference::Module(_) => {
let short_identifier = self.short_identifier();
let resource = short_identifier.split('!').next_back().unwrap_or("");
Some(Cow::Borrowed(resource))
}
SourceReference::Source(_) => {
let absolute_resource_path = self.absolute_resource_path();
resolve_relative_resource_path(absolute_resource_path, self.unresolved_source_map_path)
.map(Cow::Owned)
}
}
}
pub fn hash(&self) -> String {
let identifier = self.identifier();
get_hash(identifier, self.output_options)
}
pub fn resource(&self) -> &str {
let short_identifier = self.short_identifier();
short_identifier.split('!').next_back().unwrap_or("")
}
pub fn loaders(&self) -> &str {
let short_identifier = self.short_identifier();
get_before(short_identifier, "!")
}
pub fn all_loaders(&self) -> &str {
let identifier = self.identifier();
get_before(identifier, "!")
}
pub fn query(&self) -> &str {
let resource = self.resource();
get_after(resource, "?")
}
pub fn resource_path(&self) -> &str {
let resource = self.resource();
let query = self.query();
let q = query.len();
if q == 0 {
resource
} else {
&resource[..resource.len().saturating_sub(q)]
}
}
pub fn namespace(&self) -> &str {
self.namespace
}
}
fn starts_with_ignore_ascii_case(s: &[u8], prefix: &[u8]) -> bool {
s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix)
}
fn template_replace<'a>(s: &str, ctx: &ModuleFilenameTemplateStringCtx<'a>) -> String {
let resource_tag = b"[resource]";
let sstr = s;
let s = s.as_bytes();
let mut buf = String::new();
let mut pos = 0;
let mut state = false;
macro_rules! match_ignore_case {
(
$value:expr ;
$(
$item:literal $( | $item2:literal )* => $b:expr,
)*
$name:ident => $tail:expr
) => {
$(
if $value.eq_ignore_ascii_case($item)
$( || $value.eq_ignore_ascii_case($item2) )*
{
$b
} else
)*
{
let $name = $value;
$tail
}
}
}
for i in memchr::memchr2_iter(b'[', b']', s) {
if i < pos {
continue;
}
match s[i] {
b'[' => {
let s = &sstr[pos..i];
buf.push_str(s);
pos = i;
state = true;
}
b']' if state => {
let mut next_pos = i + 1;
match_ignore_case!(&s[pos..next_pos];
b"[identifier]" => buf.push_str(ctx.identifier().as_ref()),
b"[short-identifier]" => buf.push_str(ctx.short_identifier().as_ref()),
b"[resource]" => buf.push_str(ctx.resource()),
b"[resource-path]" | b"[resourcepath]" => buf.push_str(ctx.resource_path()),
b"[absolute-resource-path]" |
b"[abs-resource-path]" |
b"[absoluteresource-path]" |
b"[absresource-path]" |
b"[absolute-resourcepath]" |
b"[abs-resourcepath]" |
b"[absoluteresourcepath]" |
b"[absresourcepath]" => buf.push_str(ctx.absolute_resource_path()),
b"[relative-resource-path]" |
b"[relativeresource-path]" |
b"[relative-resourcepath]" |
b"[relativeresourcepath]" => {
if let Some(relative_resource_path) = ctx.relative_resource_path() {
buf.push_str(relative_resource_path.as_ref())
} else {
buf.push_str(&sstr[pos..next_pos]);
}
},
b"[all-loaders]" | b"[allloaders]" => if starts_with_ignore_ascii_case(&s[next_pos..], resource_tag) {
next_pos += resource_tag.len();
buf.push_str(ctx.identifier().as_ref());
} else {
buf.push_str(ctx.all_loaders());
},
b"[loaders]" => if starts_with_ignore_ascii_case(&s[next_pos..], resource_tag) {
next_pos += resource_tag.len();
buf.push_str(ctx.short_identifier().as_ref());
} else {
buf.push_str(ctx.loaders());
},
b"[query]" => buf.push_str(ctx.query()),
b"[id]" => buf.push_str(ctx.module_id()),
b"[hash]" => buf.push_str(ctx.hash().as_ref()),
b"[namespace]" => buf.push_str(ctx.namespace()),
matched => if let Some(matched) = matched.strip_prefix(b"[\\")
.and_then(|matched| matched.strip_suffix(b"\\]"))
{
#[allow(clippy::unwrap_used)]
let s = str::from_utf8(matched).unwrap();
buf.push('[');
buf.push_str(s);
buf.push(']');
} else {
let s = &sstr[pos..next_pos];
buf.push_str(s);
}
);
pos = next_pos;
state = false;
}
_ => (),
}
}
let s = &sstr[pos..];
buf.push_str(s);
buf
}