#[cfg(feature = "swc")]
use swc_core::common::{FileName, SourceMap, sync::Lrc};
#[cfg(feature = "swc")]
use swc_core::ecma::ast::*;
#[cfg(feature = "swc")]
use swc_core::ecma::codegen::{Config, Emitter, text_writer::JsWriter};
#[cfg(feature = "swc")]
use swc_core::ecma::parser::{PResult, Parser, StringInput, Syntax, TsSyntax, lexer::Lexer};
use crate::TsSynError;
#[derive(Debug, Clone, Copy)]
pub struct ImportConfig {
pub name: &'static str,
pub alias: &'static str,
pub module: &'static str,
pub is_type: bool,
}
impl ImportConfig {
pub const fn value(name: &'static str, alias: &'static str, module: &'static str) -> Self {
Self {
name,
alias,
module,
is_type: false,
}
}
pub const fn type_only(name: &'static str, alias: &'static str, module: &'static str) -> Self {
Self {
name,
alias,
module,
is_type: true,
}
}
}
#[cfg(feature = "swc")]
pub struct TsStream {
source_map: Lrc<SourceMap>,
source: String,
file_name: String,
pub ctx: Option<crate::abi::MacroContextIR>,
pub runtime_patches: Vec<crate::abi::Patch>,
pub insert_pos: crate::abi::InsertPos,
pub cross_module_suffixes: Vec<String>,
pub cross_module_type_suffixes: Vec<String>,
}
#[cfg(feature = "swc")]
pub fn format_ts_source(source: &str) -> String {
let cm = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(FileName::Custom("fmt.ts".into()).into(), source.to_string());
let syntax = Syntax::Typescript(TsSyntax {
tsx: true,
decorators: true,
..Default::default()
});
let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None);
let mut parser = Parser::new_from(lexer);
if let Ok(module) = parser.parse_module() {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Config::default().with_minify(false),
cm: cm.clone(),
comments: None,
wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
};
if emitter.emit_module(&module).is_ok() {
return String::from_utf8(buf).unwrap_or_else(|_| source.to_string());
}
}
}
let wrapped_source = format!("class __FmtWrapper {{ {} }}", source);
let fm_wrapped = cm.new_source_file(
FileName::Custom("fmt_wrapped.ts".into()).into(),
wrapped_source,
);
let lexer = Lexer::new(
syntax,
EsVersion::latest(),
StringInput::from(&*fm_wrapped),
None,
);
let mut parser = Parser::new_from(lexer);
if let Ok(module) = parser.parse_module() {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: Config::default().with_minify(false),
cm: cm.clone(),
comments: None,
wr: JsWriter::new(cm.clone(), "\n", &mut buf, None),
};
if emitter.emit_module(&module).is_ok() {
let full_output = String::from_utf8(buf).unwrap_or_default();
if let (Some(start), Some(end)) = (full_output.find('{'), full_output.rfind('}')) {
let content = &full_output[start + 1..end];
let lines: Vec<&str> = content.lines().collect();
let mut result = String::new();
for line in lines {
let trimmed = line.strip_prefix(" ").unwrap_or(line);
if !trimmed.trim().is_empty() {
result.push_str(trimmed);
result.push('\n');
}
}
return result.trim().to_string();
}
}
}
}
source.to_string()
}
#[cfg(feature = "swc")]
impl TsStream {
pub fn new(source: &str, file_name: &str) -> Result<Self, TsSynError> {
Ok(TsStream {
source_map: Lrc::new(Default::default()),
source: source.to_string(),
file_name: file_name.to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
})
}
pub fn from_string(source: String) -> Self {
TsStream {
source_map: Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn with_insert_pos(source: String, insert_pos: crate::abi::InsertPos) -> Self {
TsStream {
source_map: Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches: vec![],
insert_pos,
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn with_insert_pos_and_patches(
source: String,
insert_pos: crate::abi::InsertPos,
runtime_patches: Vec<crate::abi::Patch>,
) -> Self {
TsStream {
source_map: Lrc::new(Default::default()),
source,
file_name: "macro_output.ts".to_string(),
ctx: None,
runtime_patches,
insert_pos,
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
}
}
pub fn source(&self) -> &str {
&self.source
}
pub fn with_context(
source: &str,
file_name: &str,
ctx: crate::abi::MacroContextIR,
) -> Result<Self, TsSynError> {
Ok(TsStream {
source_map: Lrc::new(Default::default()),
source: source.to_string(),
file_name: file_name.to_string(),
ctx: Some(ctx),
runtime_patches: vec![],
insert_pos: crate::abi::InsertPos::default(),
cross_module_suffixes: vec![],
cross_module_type_suffixes: vec![],
})
}
pub fn context(&self) -> Option<&crate::abi::MacroContextIR> {
self.ctx.as_ref()
}
pub fn into_result(self) -> crate::abi::MacroResult {
let imports = crate::import_registry::with_registry_mut(|r| r.take_generated_imports());
crate::abi::MacroResult {
runtime_patches: self.runtime_patches,
type_patches: vec![],
diagnostics: vec![],
tokens: Some(self.source),
insert_pos: self.insert_pos,
debug: None,
cross_module_suffixes: self.cross_module_suffixes,
cross_module_type_suffixes: self.cross_module_type_suffixes,
imports,
}
}
pub fn add_import(&mut self, specifier: &str, module: &str) {
let (original, local) = parse_import_specifier(specifier);
crate::import_registry::with_registry_mut(|r| {
r.request_import(&local, original.as_deref(), module, false);
});
}
pub fn add_type_import(&mut self, specifier: &str, module: &str) {
let (original, local) = parse_import_specifier(specifier);
crate::import_registry::with_registry_mut(|r| {
r.request_import(&local, original.as_deref(), module, true);
});
}
pub fn add_aliased_import(&mut self, name: &str, module: &str) {
let alias = format!("__mf_{name}");
crate::import_registry::with_registry_mut(|r| {
r.request_import(&alias, Some(name), module, false);
});
}
pub fn add_aliased_type_import(&mut self, name: &str, module: &str) {
let alias = format!("__mf_{name}");
crate::import_registry::with_registry_mut(|r| {
r.request_import(&alias, Some(name), module, true);
});
}
pub fn add_import_as(&mut self, name: &str, alias: &str, module: &str) {
crate::import_registry::with_registry_mut(|r| {
r.request_import(alias, Some(name), module, false);
});
}
pub fn add_type_import_as(&mut self, name: &str, alias: &str, module: &str) {
crate::import_registry::with_registry_mut(|r| {
r.request_import(alias, Some(name), module, true);
});
}
pub fn add_imports(&mut self, imports: &[ImportConfig]) {
crate::import_registry::with_registry_mut(|r| {
for import in imports {
r.request_import(
import.alias,
Some(import.name),
import.module,
import.is_type,
);
}
});
}
pub fn add_cross_module_suffix(&mut self, suffix: &str) {
self.cross_module_suffixes.push(suffix.to_string());
}
pub fn add_cross_module_type_suffix(&mut self, suffix: &str) {
self.cross_module_type_suffixes.push(suffix.to_string());
}
pub fn merge(mut self, other: Self) -> Self {
if !self.source.is_empty() && !other.source.is_empty() {
let left_ends_ws = self
.source
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(false);
let right_starts_ws = other
.source
.chars()
.next()
.map(|c| c.is_whitespace())
.unwrap_or(false);
if !(left_ends_ws || right_starts_ws) {
self.source.push('\n');
}
}
self.source.push_str(&other.source);
self.runtime_patches.extend(other.runtime_patches);
self.cross_module_suffixes
.extend(other.cross_module_suffixes);
self.cross_module_type_suffixes
.extend(other.cross_module_type_suffixes);
self
}
pub fn merge_all(streams: impl IntoIterator<Item = Self>) -> Self {
let mut iter = streams.into_iter();
match iter.next() {
Some(first) => iter.fold(first, |acc, stream| acc.merge(stream)),
None => Self::from_string(String::new()),
}
}
fn with_parser<F, T>(&self, f: F) -> Result<T, TsSynError>
where
F: for<'a> FnOnce(&mut Parser<Lexer<'a>>) -> PResult<T>,
{
let fm = self.source_map.new_source_file(
FileName::Custom(self.file_name.clone()).into(),
self.source.clone(),
);
let syntax = Syntax::Typescript(TsSyntax {
tsx: self.file_name.ends_with(".tsx"),
decorators: true,
..Default::default()
});
let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None);
let mut parser = Parser::new_from(lexer);
f(&mut parser).map_err(|e| TsSynError::Parse(format!("{:?}", e)))
}
pub fn parse<T: ParseTs>(&mut self) -> Result<T, TsSynError> {
T::parse(self)
}
pub fn parse_ident(&self) -> Result<Ident, TsSynError> {
self.with_parser(|parser| {
use swc_core::common::DUMMY_SP;
use swc_core::ecma::parser::error::{Error, SyntaxError};
parser.parse_expr().and_then(|expr| match *expr {
Expr::Ident(ident) => Ok(ident),
_ => Err(Error::new(DUMMY_SP, SyntaxError::TS1003)),
})
})
}
pub fn parse_stmt(&self) -> Result<Stmt, TsSynError> {
self.with_parser(|parser| parser.parse_stmt_list_item())
}
pub fn parse_expr(&self) -> Result<Box<Expr>, TsSynError> {
self.with_parser(|parser| parser.parse_expr())
}
pub fn parse_module(&self) -> Result<Module, TsSynError> {
self.with_parser(|parser| parser.parse_module())
}
}
pub trait ParseTs: Sized {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError>;
}
#[cfg(feature = "swc")]
impl ParseTs for Ident {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_ident()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Stmt {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_stmt()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Box<Expr> {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_expr()
}
}
#[cfg(feature = "swc")]
impl ParseTs for Module {
fn parse(input: &mut TsStream) -> Result<Self, TsSynError> {
input.parse_module()
}
}
#[cfg(feature = "swc")]
pub fn parse_ts_str<T: ParseTs>(code: &str) -> Result<T, TsSynError> {
let mut stream = TsStream::new(code, "input.ts")?;
stream.parse()
}
#[cfg(feature = "swc")]
pub fn parse_ts_expr(code: &str) -> Result<Box<Expr>, TsSynError> {
parse_ts_str(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_stmt(code: &str) -> Result<Stmt, TsSynError> {
parse_ts_str(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_module(code: &str) -> Result<Module, TsSynError> {
parse_ts_str(code)
}
#[cfg(feature = "swc")]
pub fn parse_ts_type(code: &str) -> Result<TsType, TsSynError> {
use swc_core::ecma::ast::{Decl, ModuleItem, Stmt};
let wrapped = format!("type __T = {};", code);
let module: Module = parse_ts_str(&wrapped)?;
for item in module.body {
if let ModuleItem::Stmt(Stmt::Decl(Decl::TsTypeAlias(alias))) = item {
return Ok(*alias.type_ann);
}
}
Err(TsSynError::Parse(format!("Failed to parse type: {}", code)))
}
fn parse_import_specifier(specifier: &str) -> (Option<String>, String) {
if let Some(idx) = specifier.find(" as ") {
let original = specifier[..idx].trim().to_string();
let local = specifier[idx + 4..].trim().to_string();
(Some(original), local)
} else {
(None, specifier.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "swc")]
#[test]
fn test_parse_ident() {
let result: Result<Ident, _> = parse_ts_str("myVariable");
assert!(result.is_ok());
assert_eq!(result.unwrap().sym.as_ref(), "myVariable");
}
#[cfg(feature = "swc")]
#[test]
fn test_parse_expr() {
let result = parse_ts_expr("1 + 2");
assert!(result.is_ok());
}
#[cfg(feature = "swc")]
#[test]
fn test_parse_stmt() {
let result = parse_ts_stmt("const x = 5;");
assert!(result.is_ok(), "parse_ts_stmt failed: {:?}", result.err());
}
#[test]
fn test_add_cross_module_suffix() {
let mut stream = TsStream::from_string("export function foo() {}".to_string());
assert!(stream.cross_module_suffixes.is_empty());
stream.add_cross_module_suffix("GetFields");
assert_eq!(stream.cross_module_suffixes, vec!["GetFields"]);
stream.add_cross_module_suffix("CustomSuffix");
assert_eq!(
stream.cross_module_suffixes,
vec!["GetFields", "CustomSuffix"]
);
}
#[test]
fn test_cross_module_suffixes_propagate_to_result() {
let mut stream = TsStream::from_string("export function foo() {}".to_string());
stream.add_cross_module_suffix("GetFields");
stream.add_cross_module_suffix("OtherSuffix");
let result = stream.into_result();
assert_eq!(
result.cross_module_suffixes,
vec!["GetFields", "OtherSuffix"]
);
}
#[test]
fn test_merge_combines_cross_module_suffixes() {
let mut a = TsStream::from_string("export function a() {}".to_string());
a.add_cross_module_suffix("GetFields");
let mut b = TsStream::from_string("export function b() {}".to_string());
b.add_cross_module_suffix("CustomSuffix");
let merged = a.merge(b);
assert_eq!(
merged.cross_module_suffixes,
vec!["GetFields", "CustomSuffix"]
);
}
#[test]
fn test_empty_cross_module_suffixes_by_default() {
let stream = TsStream::from_string("const x = 1;".to_string());
assert!(stream.cross_module_suffixes.is_empty());
let result = stream.into_result();
assert!(result.cross_module_suffixes.is_empty());
}
}