pub mod ast;
pub mod config;
pub mod dictionary_entry;
pub mod dictionary_imports;
pub mod extra_caller;
pub mod field_rename;
pub mod imports;
pub mod logger;
pub mod optimize;
pub mod packages;
pub mod paths;
pub mod pre_pass;
#[cfg(test)]
mod tests;
pub use config::{
ExtraCallerConfig, FieldRenameMap, FieldRenameNode, LogLevel, NamespaceOptionConfig,
PluginConfig,
};
pub use paths::normalize_path;
use crate::{
dictionary_entry::build_empty_dictionaries_entry,
dictionary_imports::ImportKind,
extra_caller::ExtraCallerContext,
imports::{inject_dictionary_imports, DictionaryDirs},
logger::{Logger, TransformSummary},
optimize::TransformVisitor,
pre_pass::run_pre_pass,
};
use std::collections::HashSet;
use swc_core::ecma::{ast::Program, visit::VisitMutWith};
#[cfg(feature = "plugin")]
use swc_core::plugin::{
metadata::{TransformPluginMetadataContextKind, TransformPluginProgramMetadata},
plugin_transform,
};
fn resolve_working_filename(
files_list: &[String],
filename_raw: &str,
logger: &Logger,
) -> Option<String> {
let normalized_filename = normalize_path(filename_raw);
if files_list.is_empty() {
logger.debug(format!(
"processing {} (no filesList allowlist configured)",
filename_raw
));
return Some(normalized_filename.into_owned());
}
let Some(matched) = files_list.iter().find_map(|target| {
let normalized_target = normalize_path(target);
let is_match = normalized_filename.ends_with(normalized_target.as_ref())
|| normalized_target.ends_with(normalized_filename.as_ref());
is_match.then_some(normalized_target)
}) else {
logger.debug(format!("skipping {} (not in filesList)", filename_raw));
return None;
};
logger.debug(format!(
"processing {} (matched allowlist entry {})",
filename_raw, matched
));
Some(matched.into_owned())
}
pub fn process_transform(
mut program: Program,
mut cfg: PluginConfig,
filename_raw: String,
) -> Program {
let logger = Logger::new(if logger::DEBUG_LOG {
LogLevel::Debug
} else {
LogLevel::from_option(cfg.log_level.as_deref())
});
cfg.dictionaries_dir = normalize_path(&cfg.dictionaries_dir).into_owned();
cfg.dynamic_dictionaries_dir = normalize_path(&cfg.dynamic_dictionaries_dir).into_owned();
cfg.fetch_dictionaries_dir = normalize_path(&cfg.fetch_dictionaries_dir).into_owned();
cfg.dictionaries_entry_path = normalize_path(&cfg.dictionaries_entry_path).into_owned();
let Some(working_filename) = resolve_working_filename(&cfg.files_list, &filename_raw, &logger)
else {
return program;
};
if cfg.replace_dictionary_entry.unwrap_or(false) {
let is_main_entry = working_filename == cfg.dictionaries_entry_path
|| normalize_path(&filename_raw) == cfg.dictionaries_entry_path.as_str();
if is_main_entry {
logger.info(format!("{}: emptied dictionaries entry", filename_raw));
return build_empty_dictionaries_entry();
}
}
let mut summary = TransformSummary {
renamed_fields: field_rename::rename_field_accesses(&mut program, &cfg.field_rename_map),
..TransformSummary::default()
};
let import_mode =
ImportKind::from_option(cfg.import_mode.as_deref()).unwrap_or(ImportKind::Static);
let dictionary_mode_map = cfg.dictionary_mode_map.take().unwrap_or_default();
let pre_pass = run_pre_pass(&program, &dictionary_mode_map, &cfg.extra_callers);
let extra_caller_context = (!cfg.extra_callers.is_empty()).then(|| ExtraCallerContext {
extra_callers: &cfg.extra_callers,
dictionary_mode_map: &dictionary_mode_map,
import_mode,
use_dynamic_helpers: import_mode != ImportKind::Static || pre_pass.extra_has_dynamic_call,
});
let mut visitor = TransformVisitor::new(
import_mode,
&dictionary_mode_map,
&pre_pass.caller_map,
&pre_pass.packages_with_dynamic_call,
&pre_pass.packages_with_fetch_call,
extra_caller_context,
);
program.visit_mut_with(&mut visitor);
summary.static_imports = visitor.injected_imports.static_imports.len();
summary.dynamic_imports = visitor.injected_imports.dynamic_imports.len();
let nesting_dictionary_keys: HashSet<&str> = cfg
.nesting_dictionary_keys
.iter()
.map(String::as_str)
.collect();
inject_dictionary_imports(
&mut program,
&visitor.injected_imports,
&DictionaryDirs {
dictionaries_dir: &cfg.dictionaries_dir,
dynamic_dictionaries_dir: &cfg.dynamic_dictionaries_dir,
fetch_dictionaries_dir: &cfg.fetch_dictionaries_dir,
},
&nesting_dictionary_keys,
&working_filename,
);
logger.report_file(&filename_raw, &summary, &program);
program
}
#[cfg(feature = "plugin")]
#[plugin_transform]
pub fn transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {
let cfg: PluginConfig = match metadata
.get_transform_plugin_config()
.and_then(|raw| serde_json::from_str::<PluginConfig>(&raw).ok())
{
Some(config) => config,
None => return program,
};
let filename_raw = match metadata.get_context(&TransformPluginMetadataContextKind::Filename) {
Some(filename) => filename,
None => return program,
};
process_transform(program, cfg, filename_raw)
}