use std::fmt::Write as _;
use arcstr::ArcStr;
use rolldown_common::OutputExports;
use rolldown_error::{BuildDiagnostic, BuildResult};
use rolldown_utils::{
concat_string,
ecmascript::{is_validate_assignee_identifier_name, is_validate_identifier_name},
};
use crate::types::generator::GenerateContext;
pub fn generate_namespace_definition(
name: &str,
global: &str,
delimiter: &str,
) -> (String, String) {
let parts: Vec<&str> = name.split('.').collect();
let mut stmts = String::new();
let mut namespace = String::from(global);
let global_len = global.len();
for (i, part) in parts.iter().enumerate() {
let property = render_property_access(part);
namespace.push_str(&property);
if i < parts.len() - 1 {
let property = &namespace[global_len..];
write!(stmts, "{global}{property} = {global}{property} || {{}}{delimiter}").unwrap();
}
}
(stmts, namespace)
}
pub fn generate_identifier(
warnings: &mut Vec<BuildDiagnostic>,
ctx: &GenerateContext<'_>,
export_mode: OutputExports,
) -> BuildResult<(String, String)> {
if ctx.options.name.as_ref().is_none_or(String::is_empty)
&& !matches!(export_mode, OutputExports::None)
{
warnings
.push(BuildDiagnostic::missing_name_option_for_iife_export(false).with_severity_warning());
}
let Some(name) = &ctx.options.name else {
return Ok((String::new(), String::new()));
};
if name.contains('.') {
let (stmts, namespace) = generate_namespace_definition(name, "this", ";\n");
let final_expr = if ctx.options.extend && matches!(export_mode, OutputExports::Named) {
format!("{namespace} = {namespace} || {{}}")
} else {
namespace
};
return Ok((stmts, final_expr));
}
if ctx.options.extend {
let property = render_property_access(name.as_str());
let final_expr = if matches!(export_mode, OutputExports::Named) {
format!("this{property} = this{property} || {{}}")
} else {
if name.is_empty() { String::new() } else { format!("this{property}") }
};
return Ok((String::new(), final_expr));
}
if is_validate_assignee_identifier_name(name) {
Ok((String::new(), format!("var {name}")))
} else {
let name = ArcStr::from(name);
Err(vec![BuildDiagnostic::illegal_identifier_as_name(name)].into())
}
}
pub fn render_property_access(name: &str) -> String {
if is_validate_identifier_name(name) {
concat_string!(".", name)
} else {
concat_string!("[\"", name, "\"]")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_namespace_definition() {
let result = generate_namespace_definition("a.b.c", "this", ";\n");
assert_eq!(result.0, "this.a = this.a || {};\nthis.a.b = this.a.b || {};\n");
assert_eq!(result.1, "this.a.b.c");
}
#[test]
fn test_non_identifier_as_name() {
let result = generate_namespace_definition("1.2.3", "this", ";\n");
assert_eq!(
result.0,
"this[\"1\"] = this[\"1\"] || {};\nthis[\"1\"][\"2\"] = this[\"1\"][\"2\"] || {};\n"
);
assert_eq!(result.1, "this[\"1\"][\"2\"][\"3\"]");
}
#[test]
fn test_reserved_identifier_as_name() {
let result = generate_namespace_definition("if.else", "this", ";\n");
assert_eq!(result.0, "this.if = this.if || {};\n");
assert_eq!(result.1, "this.if.else");
}
#[test]
fn test_invalid_identifier_as_name() {
let result = generate_namespace_definition("toString.valueOf.constructor", "this", ";\n");
assert_eq!(
result.0,
"this.toString = this.toString || {};\nthis.toString.valueOf = this.toString.valueOf || {};\n"
);
assert_eq!(result.1, "this.toString.valueOf.constructor");
}
}