use std::{borrow::Cow, sync::Arc};
use cow_utils::CowUtils;
use oxc_allocator::Allocator;
use oxc_ast::{AstBuilder, NONE, ast::*};
use oxc_semantic::Scoping;
use oxc_span::{CompactStr, SPAN, format_compact_str};
use oxc_syntax::identifier;
use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
use super::{
DotDefineMemberExpression,
replace_global_defines::{DotDefine, ReplaceGlobalDefines},
};
#[derive(Debug, Clone)]
pub struct InjectGlobalVariablesConfig {
injects: Arc<[InjectImport]>,
}
impl InjectGlobalVariablesConfig {
pub fn new(injects: Vec<InjectImport>) -> Self {
Self { injects: Arc::from(injects) }
}
}
#[derive(Debug, Clone)]
pub struct InjectImport {
source: CompactStr,
specifier: InjectImportSpecifier,
replace_value: Option<CompactStr>,
}
impl InjectImport {
pub fn named_specifier(source: &str, imported: Option<&str>, local: &str) -> InjectImport {
InjectImport {
source: CompactStr::from(source),
specifier: InjectImportSpecifier::Specifier {
imported: imported.map(CompactStr::from),
local: CompactStr::from(local),
},
replace_value: Self::replace_name(local),
}
}
pub fn namespace_specifier(source: &str, local: &str) -> InjectImport {
InjectImport {
source: CompactStr::from(source),
specifier: InjectImportSpecifier::NamespaceSpecifier { local: CompactStr::from(local) },
replace_value: Self::replace_name(local),
}
}
pub fn default_specifier(source: &str, local: &str) -> InjectImport {
InjectImport {
source: CompactStr::from(source),
specifier: InjectImportSpecifier::DefaultSpecifier { local: CompactStr::from(local) },
replace_value: Self::replace_name(local),
}
}
fn replace_name(local: &str) -> Option<CompactStr> {
match local.cow_replace('.', "_") {
Cow::Owned(local) => Some(format_compact_str!("$inject_{local}")),
Cow::Borrowed(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub enum InjectImportSpecifier {
Specifier { imported: Option<CompactStr>, local: CompactStr },
NamespaceSpecifier { local: CompactStr },
DefaultSpecifier { local: CompactStr },
}
impl InjectImportSpecifier {
fn local(&self) -> &CompactStr {
match self {
Self::Specifier { local, .. }
| Self::NamespaceSpecifier { local, .. }
| Self::DefaultSpecifier { local, .. } => local,
}
}
}
struct DotDefineState<'a> {
dot_define: DotDefine,
value_atom: Option<Atom<'a>>,
}
impl From<&InjectImport> for DotDefineState<'_> {
fn from(inject: &InjectImport) -> Self {
let parts = inject.specifier.local().split('.').map(CompactStr::from).collect::<Vec<_>>();
let value = inject.replace_value.clone().unwrap();
let dot_define = DotDefine { parts, value };
Self { dot_define, value_atom: None }
}
}
#[must_use]
pub struct InjectGlobalVariablesReturn {
pub scoping: Scoping,
}
pub struct InjectGlobalVariables<'a> {
ast: AstBuilder<'a>,
config: InjectGlobalVariablesConfig,
dot_defines: Vec<DotDefineState<'a>>,
replaced_dot_defines:
Vec<(/* identifier of member expression */ CompactStr, /* local */ CompactStr)>,
}
impl<'a> Traverse<'a> for InjectGlobalVariables<'a> {
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
self.replace_dot_defines(expr, ctx);
}
}
impl<'a> InjectGlobalVariables<'a> {
pub fn new(allocator: &'a Allocator, config: InjectGlobalVariablesConfig) -> Self {
Self {
ast: AstBuilder::new(allocator),
config,
dot_defines: vec![],
replaced_dot_defines: vec![],
}
}
pub fn build(
&mut self,
scoping: Scoping,
program: &mut Program<'a>,
) -> InjectGlobalVariablesReturn {
let mut scoping = scoping;
let dot_defines = self
.config
.injects
.iter()
.filter(|i| i.replace_value.is_some())
.map(DotDefineState::from)
.collect::<Vec<_>>();
if !dot_defines.is_empty() {
self.dot_defines = dot_defines;
scoping = traverse_mut(self, self.ast.allocator, program, scoping);
}
let injects = self
.config
.injects
.iter()
.filter(|i| {
match &i.replace_value {
Some(replace_value) => {
self.replaced_dot_defines.iter().any(|d| d.1 == replace_value)
}
_ => {
if self.replaced_dot_defines.iter().any(|d| d.0 == i.specifier.local()) {
false
} else {
scoping
.root_unresolved_references()
.contains_key(i.specifier.local().as_str())
}
}
}
})
.cloned()
.collect::<Vec<_>>();
if injects.is_empty() {
return InjectGlobalVariablesReturn { scoping };
}
self.inject_imports(&injects, program);
InjectGlobalVariablesReturn { scoping }
}
fn inject_imports(&self, injects: &[InjectImport], program: &mut Program<'a>) {
let imports = injects.iter().map(|inject| {
let specifiers = Some(self.ast.vec1(self.inject_import_to_specifier(inject)));
let source = self.ast.string_literal(SPAN, self.ast.atom(&inject.source), None);
let kind = ImportOrExportKind::Value;
let import_decl = self
.ast
.module_declaration_import_declaration(SPAN, specifiers, source, None, NONE, kind);
Statement::from(import_decl)
});
program.body.splice(0..0, imports);
}
fn inject_import_to_specifier(&self, inject: &InjectImport) -> ImportDeclarationSpecifier<'a> {
match &inject.specifier {
InjectImportSpecifier::Specifier { imported, local } => {
let imported = match imported {
Some(imported_name) => {
let imported_name = self.ast.atom(imported_name);
if identifier::is_identifier_name(&imported_name) {
self.ast.module_export_name_identifier_name(SPAN, imported_name)
} else {
self.ast.module_export_name_string_literal(SPAN, imported_name, None)
}
}
None => self.ast.module_export_name_identifier_name(SPAN, "default"),
};
let local = inject.replace_value.as_ref().unwrap_or(local).as_str();
self.ast.import_declaration_specifier_import_specifier(
SPAN,
imported,
self.ast.binding_identifier(SPAN, self.ast.atom(local)),
ImportOrExportKind::Value,
)
}
InjectImportSpecifier::DefaultSpecifier { local } => {
let local = inject.replace_value.as_ref().unwrap_or(local).as_str();
let local = self.ast.binding_identifier(SPAN, self.ast.atom(local));
self.ast.import_declaration_specifier_import_default_specifier(SPAN, local)
}
InjectImportSpecifier::NamespaceSpecifier { local } => {
let local = inject.replace_value.as_ref().unwrap_or(local).as_str();
let local = self.ast.binding_identifier(SPAN, self.ast.atom(local));
self.ast.import_declaration_specifier_import_namespace_specifier(SPAN, local)
}
}
}
fn replace_dot_defines(&mut self, expr: &mut Expression<'a>, ctx: &TraverseCtx<'a>) {
if let Expression::StaticMemberExpression(member) = expr {
for DotDefineState { dot_define, value_atom } in &mut self.dot_defines {
if ReplaceGlobalDefines::is_dot_define(
ctx,
dot_define,
DotDefineMemberExpression::StaticMemberExpression(member),
) {
let value_atom = *value_atom.get_or_insert_with(|| {
self.replaced_dot_defines
.push((dot_define.parts[0].clone(), dot_define.value.clone()));
self.ast.atom(dot_define.value.as_str())
});
let value = self.ast.expression_identifier(SPAN, value_atom);
*expr = value;
break;
}
}
}
}
}