#![cfg(feature = "swc")]
#[path = "template/ir_declarations.rs"]
mod ir_declarations;
#[path = "template/ir_expressions.rs"]
mod ir_expressions;
#[path = "template/ir_patterns.rs"]
mod ir_patterns;
#[path = "template/ir_statements.rs"]
mod ir_statements;
#[path = "template/ir_types.rs"]
mod ir_types;
#[path = "template/test_single.rs"]
mod test_single;
use macroforge_ts::macros::ts_template;
use macroforge_ts::swc_core::common::{FileName, GLOBALS, Globals, SourceMap, sync::Lrc};
use macroforge_ts::swc_core::ecma::parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer};
use macroforge_ts::ts_syn::abi::{MacroContextIR, SpanIR};
use macroforge_ts::ts_syn::{
Data, DeriveInput, ParseTs, TsStream, lower_classes, lower_interfaces,
};
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn create_test_stream(source: &str) -> TsStream {
GLOBALS.set(&Globals::new(), || {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom("test.ts".into()).into(),
source.to_string(),
);
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax {
tsx: false,
decorators: true,
..Default::default()
}),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().expect("Failed to parse test source");
let classes = lower_classes(&module, source, None).expect("Failed to lower classes");
let class = classes
.first()
.expect("Expected at least one class in test source")
.clone();
let ctx = MacroContextIR::new_derive_class(
"TestMacro".to_string(),
"test-macro".to_string(),
SpanIR::new(0, 0), class.span,
"test.ts".to_string(),
class,
source.to_string(),
);
TsStream::with_context(source, "test.ts", ctx).unwrap()
})
}
fn create_test_stream_interface(source: &str) -> TsStream {
GLOBALS.set(&Globals::new(), || {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom("test.ts".into()).into(),
source.to_string(),
);
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax {
tsx: false,
decorators: true,
..Default::default()
}),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().expect("Failed to parse test source");
let interfaces =
lower_interfaces(&module, source, None).expect("Failed to lower interfaces");
let interface = interfaces
.first()
.expect("Expected at least one interface in test source")
.clone();
let ctx = MacroContextIR::new_derive_interface(
"TestMacro".to_string(),
"test-macro".to_string(),
SpanIR::new(0, 0), interface.span,
"test.ts".to_string(),
interface,
source.to_string(),
);
TsStream::with_context(source, "test.ts", ctx).unwrap()
})
}
#[test]
pub fn derive_json_macro() {
let raw = include_str!("./fixtures/macro-user.ts");
let mut stream = create_test_stream(raw);
let input = DeriveInput::parse(&mut stream).unwrap();
match &input.data {
Data::Class(class) => {
let stream = ts_template! {
toJSON(): Record<string, unknown> {
const result: Record<string, unknown> = {};
{#for field in class.field_names()}
result.@{field} = this.@{field};
{/for}
return result;
}
};
let source = stream.source();
println!("Generated JSON Source:\n{}", source);
assert!(source.contains("toJSON(): Record<string, unknown>"));
assert!(source.contains("result.id = this.id"));
assert!(source.contains("result.name = this.name"));
assert!(source.contains("result.role = this.role"));
}
_ => panic!("Expected class data in macro-user.ts"),
}
}
#[test]
pub fn field_controller_macro() {
let raw = include_str!("./fixtures/field-controller.fixture.ts");
let mut stream = create_test_stream_interface(raw);
let input = DeriveInput::parse(&mut stream).unwrap();
match &input.data {
Data::Interface(interface) => {
let decorated_fields: Vec<_> = interface
.fields()
.iter()
.filter(|field| {
field
.decorators
.iter()
.any(|d| d.name == "fieldController" || d.name == "textAreaController")
})
.collect();
let class_name = input.name();
let base_props_method = format!("make{}BaseProps", class_name);
let field_data: Vec<_> = decorated_fields
.iter()
.map(|field| {
let field_name = &field.name;
(
format!("\"{}\"", capitalize(field_name)), format!("\"{}\"", field_name), format!("{}FieldPath", field_name), format!("{}FieldController", field_name), &field.ts_type,
)
})
.collect();
let stream = ts_template! {
make@{class_name}BaseProps<D extends number, const P extends DeepPath<@{class_name}, D>, V = DeepValue<@{class_name}, P, never, D>>(
superForm: SuperForm<@{class_name}>,
path: P,
overrides?: BasePropsOverrides<@{class_name}, V, D>
): BaseFieldProps<@{class_name}, V, D> {
const proxy = formFieldProxy(superForm, path);
const baseProps = {
fieldPath: path,
...(overrides ?? {}),
value: proxy.value,
errors: proxy.errors,
superForm
};
return baseProps;
};
{#for (label_text, field_path_literal, field_path_prop, field_controller_prop, field_type) in field_data}
{$let controller_type = format!("{}FieldController", label_text.replace("\"", ""))}
static {
this.prototype.@{field_path_prop} = [@{field_path_literal}];
}
@{field_controller_prop}(superForm: SuperForm<@{class_name}>): @{controller_type}<@{class_name}, @{field_type}, 1> {
const fieldPath = this.@{field_path_prop};
return {
fieldPath,
baseProps: this.@{base_props_method}(
superForm,
fieldPath,
{
labelText: @{label_text}
}
)
};
};
{/for}
};
let source = stream.source();
println!("Generated FieldController Source:\n{}", source);
assert!(source.contains("makeFormModelBaseProps"));
assert!(source.contains("memoFieldController(superForm: SuperForm<FormModel>"));
assert!(source.contains("descriptionFieldController(superForm: SuperForm<FormModel>"));
assert!(source.contains("MemoFieldController<FormModel, string | null, 1>"));
assert!(source.contains("this.prototype.memoFieldPath = ["));
assert!(source.contains("\"memo\""));
}
_ => panic!("Expected interface data in field-controller.fixture.ts"),
}
}
#[test]
fn test_json_macro_pattern() {
let field_name_str = "id";
let field_name = field_name_str.to_string();
let fields = vec![field_name];
let stream: TsStream = ts_template! {
toJSON(): Record<string, unknown> {
const result: Record<string, unknown> = {};
{#for field in fields}
result.@{field} = this.@{field};
{/for}
return result;
}
};
let s = stream.source();
println!("Generated JSON Source:\n{}", s);
assert!(
s.contains(&format!("result.{} =", field_name_str)),
"Expected result.field to be concatenated. Found: {}",
s
);
assert!(
s.contains(&format!("this.{};", field_name_str)),
"Expected this.field to be concatenated. Found: {}",
s
);
}
#[test]
fn test_field_controller_template_spacing() {
let class_name_str = "FormModel";
let class_name = class_name_str.to_string();
let field_name_str = "memo";
let field_type = "string | null";
let field_data_tuple = (
format!("\"{}\"", capitalize(field_name_str)),
format!("\"{}\"", field_name_str),
format!("{}FieldPath", field_name_str),
format!("{}FieldController", field_name_str),
field_type,
);
let field_data: Vec<(_, _, _, _, _)> = vec![field_data_tuple];
let base_props_method = format!("make{}BaseProps", class_name_str);
let stream: TsStream = ts_template! {
make@{class_name}BaseProps<D extends number, const P extends DeepPath<@{class_name}, D>, V = DeepValue<@{class_name}, P, never, D>>(
superForm: SuperForm<@{class_name}>,
path: P,
overrides?: BasePropsOverrides<@{class_name}, V, D>
): BaseFieldProps<@{class_name}, V, D> {
const proxy = formFieldProxy(superForm, path);
const baseProps = {
fieldPath: path,
...(overrides ?? {}),
value: proxy.value,
errors: proxy.errors,
superForm
};
return baseProps;
};
{#for (label_text, field_path_literal, field_path_prop, field_controller_prop, field_type) in field_data}
{$let controller_type_ident = label_text.replace("\"", "") + "FieldController"}
static {
this.prototype.@{field_path_prop} = [@{field_path_literal}];
}
@{field_controller_prop}(superForm: SuperForm<@{class_name}>): @{controller_type_ident}<@{class_name}, @{field_type}, 1> {
const fieldPath = this.@{field_path_prop};
return {
fieldPath,
baseProps: this.@{base_props_method}(
superForm,
fieldPath,
{
labelText: @{label_text}
}
)
};
};
{/for}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains(&format!("make{}BaseProps<", class_name_str)),
"Expected 'make' and class name to be concatenated, but found: {}",
s
);
assert!(
s.contains("return baseProps"),
"Expected 'return' to have a space before 'baseProps', but found: {}",
s
);
assert!(
s.contains(&format!("this.prototype.{}FieldPath", field_name_str)),
"Expected 'this.prototype.' and field_path_prop to be concatenated, but found: {}",
s
);
assert!(
s.contains(&format!("this.{}(", base_props_method)),
"Expected 'this.' and base_props_method to be concatenated, but found: {}",
s
);
}
#[test]
fn test_ident_block_basic_concatenation() {
let suffix = "Status";
let stream: TsStream = ts_template! {
const namespace@{suffix} = "value";
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("namespaceStatus"),
"Expected 'namespace' and 'Status' to be concatenated without space, but found: {}",
s
);
}
#[test]
fn test_ident_block_function_name() {
let type_name = "User";
let stream: TsStream = ts_template! {
function get@{type_name}(): @{type_name} {
return {} as @{type_name};
}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("getUser()"),
"Expected 'get' and 'User' to form 'getUser()', but found: {}",
s
);
assert!(
s.contains("getUser(): User"),
"Expected proper spacing around return type, but found: {}",
s
);
}
#[test]
fn test_ident_block_multiple_interpolations() {
let prefix = "get";
let middle = "User";
let suffix = "ById";
let stream: TsStream = ts_template! {
function @{prefix}@{middle}@{suffix}(id: string) { }
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("getUserById("),
"Expected all parts to be concatenated into 'getUserById', but found: {}",
s
);
}
#[test]
fn test_ident_block_preserves_external_spacing() {
let name = "Handler";
let stream: TsStream = ts_template! {
export class Event@{name} { }
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("class EventHandler"),
"Expected 'class EventHandler' with space, but found: {}",
s
);
}
#[test]
fn test_ident_block_vs_regular_interpolation() {
let type_name = "User";
let with_block: TsStream = ts_template! {
create@{type_name}
};
let regular: TsStream = ts_template! {
create@{type_name}
};
let block_s = with_block.source();
let regular_s = regular.source();
println!("With block: {}", block_s);
println!("Regular: {}", regular_s);
assert!(
block_s.contains("createUser"),
"Ident block should produce 'createUser', but found: {}",
block_s
);
}
#[test]
fn test_ident_block_in_method_chain() {
let prop = "Status";
let stream: TsStream = ts_template! {
const value = obj.get@{prop}();
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("obj.getStatus()"),
"Expected 'obj.getStatus()', but found: {}",
s
);
}
#[test]
fn test_ident_block_empty() {
let stream: TsStream = ts_template! {
const prefixsuffix = 1;
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("prefix") && s.contains("suffix"),
"Expected prefix and suffix in output, but found: {}",
s
);
}
#[test]
fn test_ident_block_with_underscore_separator() {
let entity = "user";
let action = "create";
let stream: TsStream = ts_template! {
function @{entity}_@{action}() { }
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("user_create()"),
"Expected 'user_create()', but found: {}",
s
);
}
#[test]
fn test_union_type_for_loop_basic() {
let type_refs: Vec<String> = vec!["User".to_string(), "Admin".to_string(), "Guest".to_string()];
let stream: TsStream = ts_template! {
function dispatch(value: any) {
{#for type_ref in type_refs}
if (value.__type === "@{type_ref}") {
return @{type_ref}.deserializeWithContext(value);
}
{/for}
}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("User.deserializeWithContext"),
"Expected User.deserializeWithContext, found: {}",
s
);
assert!(
s.contains("Admin.deserializeWithContext"),
"Expected Admin.deserializeWithContext, found: {}",
s
);
assert!(
s.contains("Guest.deserializeWithContext"),
"Expected Guest.deserializeWithContext, found: {}",
s
);
}
#[test]
fn test_union_type_for_loop_with_conditionals() {
let type_refs: Vec<String> = vec!["Success".to_string(), "Failure".to_string()];
let literals: Vec<String> = vec!["\"pending\"".to_string(), "\"active\"".to_string()];
let is_literal_only = false;
let is_type_ref_only = true;
let stream: TsStream = ts_template! {
function deserializeWithContext(value: any) {
{#if is_literal_only}
const allowedValues = [{#for lit in literals}@{lit}, {/for}] as const;
return value;
{:else if is_type_ref_only}
const typeName = value.__type;
{#for type_ref in type_refs}
if (typeName === "@{type_ref}") {
return @{type_ref}.deserializeWithContext(value);
}
{/for}
throw new Error("Unknown type");
{:else}
return value;
{/if}
}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("Success.deserializeWithContext"),
"Expected Success.deserializeWithContext, found: {}",
s
);
assert!(
s.contains("Failure.deserializeWithContext"),
"Expected Failure.deserializeWithContext, found: {}",
s
);
}
#[test]
fn test_union_type_for_loop_reuse() {
let type_refs: Vec<String> = vec!["TypeA".to_string(), "TypeB".to_string()];
let stream: TsStream = ts_template! {
function dispatch(value: any) {
{#for type_ref in type_refs.clone()}
console.log("@{type_ref}");
{/for}
{#for type_ref in type_refs}
return @{type_ref};
{/for}
}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.matches("TypeA").count() >= 2,
"Expected TypeA to appear at least twice, found: {}",
s
);
assert!(
s.matches("TypeB").count() >= 2,
"Expected TypeB to appear at least twice, found: {}",
s
);
}
#[test]
fn test_union_type_nested_if_for() {
let type_refs: Vec<String> = vec!["Option1".to_string(), "Option2".to_string()];
let has_type_refs = !type_refs.is_empty();
let stream: TsStream = ts_template! {
function deserializeWithContext(value: any) {
{#if has_type_refs}
if (typeof value === "object" && value !== null) {
const __typeName = value.__type;
if (typeof __typeName === "string") {
{#for type_ref in type_refs}
if (__typeName === "@{type_ref}") {
return @{type_ref}.deserializeWithContext(value);
}
{/for}
}
}
{/if}
return value;
}
};
let s = stream.source();
println!("Generated Source:\n{}", s);
assert!(
s.contains("Option1.deserializeWithContext"),
"Expected Option1.deserializeWithContext, found: {}",
s
);
assert!(
s.contains("Option2.deserializeWithContext"),
"Expected Option2.deserializeWithContext, found: {}",
s
);
}