1use convert_case::{Case, Casing};
2use proc_macro::TokenStream;
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::{format_ident, quote};
5use syn::{Ident, ItemFn, LitStr, Result, parse::Parser, parse_macro_input, spanned::Spanned};
6
7#[proc_macro_attribute]
8pub fn ts_macro_derive(attr: TokenStream, item: TokenStream) -> TokenStream {
9 let options = match parse_macro_options(TokenStream2::from(attr)) {
10 Ok(opts) => opts,
11 Err(err) => return err.to_compile_error().into(),
12 };
13
14 let mut function = parse_macro_input!(item as ItemFn);
15 function
16 .attrs
17 .retain(|attr| !attr.path().is_ident("ts_macro_derive"));
18
19 let fn_ident = function.sig.ident.clone();
20 let struct_ident = pascal_case_ident(&fn_ident);
21
22 let macro_name = LitStr::new(&options.name.to_string(), options.name.span());
24 let description = options
25 .description
26 .clone()
27 .unwrap_or_else(|| LitStr::new("", Span::call_site()));
28
29 let package_expr = quote! { env!("CARGO_PKG_NAME") };
31
32 let module_expr = quote! { "__DYNAMIC_MODULE__" };
34
35 let runtime_values = [LitStr::new("native", Span::call_site())];
37 let runtime_exprs = runtime_values.iter().map(|lit| quote! { #lit });
38
39 let kind_expr = options.kind.as_tokens();
40
41 let decorator_exprs = options
43 .attributes
44 .iter()
45 .map(|attr_name| generate_decorator_descriptor(attr_name, &package_expr));
46 let decorator_stubs = options.attributes.iter().map(|attr_name| {
47 generate_decorator_stub(attr_name, &struct_ident, options.description.as_ref())
48 });
49
50 let descriptor_ident = format_ident!(
51 "__TS_MACRO_DESCRIPTOR_{}",
52 struct_ident.to_string().to_uppercase()
53 );
54 let decorator_array_ident = format_ident!(
55 "__TS_MACRO_DECORATORS_{}",
56 struct_ident.to_string().to_uppercase()
57 );
58 let ctor_ident = format_ident!(
59 "__ts_macro_ctor_{}",
60 struct_ident.to_string().trim_start_matches("r#")
61 );
62
63 let main_macro_stub_fn_ident = format_ident!(
64 "__ts_macro_runtime_stub_{}",
65 struct_ident.to_string().to_case(Case::Snake)
66 );
67 let features_args_type = features_args_type_literal();
68 let main_macro_napi_stub = quote! {
69 #[macroforge_ts::napi_derive::napi(
70 js_name = #macro_name,
71 ts_return_type = "ClassDecorator",
72 ts_args_type = #features_args_type
73 )]
74 pub fn #main_macro_stub_fn_ident() -> macroforge_ts::napi::Result<()> {
75 Ok(())
77 }
78 };
79
80 let run_macro_fn_ident = format_ident!(
83 "__ts_macro_run_{}",
84 options.name.to_string().to_case(Case::Snake)
85 );
86 let run_macro_js_name = format!("__macroforgeRun{}", options.name);
87 let run_macro_js_name_lit = LitStr::new(&run_macro_js_name, Span::call_site());
88
89 let run_macro_napi = quote! {
90 #[macroforge_ts::napi_derive::napi(js_name = #run_macro_js_name_lit)]
93 pub fn #run_macro_fn_ident(context_json: String) -> macroforge_ts::napi::Result<String> {
94 use macroforge_ts::host::Macroforge;
95
96 let ctx: macroforge_ts::ts_syn::MacroContextIR = macroforge_ts::serde_json::from_str(&context_json)
98 .map_err(|e| macroforge_ts::napi::Error::new(macroforge_ts::napi::Status::InvalidArg, format!("Invalid context JSON: {}", e)))?;
99
100 let input = macroforge_ts::ts_syn::TsStream::with_context(&ctx.target_source, &ctx.file_name, ctx.clone())
102 .map_err(|e| macroforge_ts::napi::Error::new(macroforge_ts::napi::Status::GenericFailure, format!("Failed to create TsStream: {:?}", e)))?;
103
104 let macro_impl = #struct_ident;
106 let result = macro_impl.run(input);
107
108 macroforge_ts::serde_json::to_string(&result)
110 .map_err(|e| macroforge_ts::napi::Error::new(macroforge_ts::napi::Status::GenericFailure, format!("Failed to serialize result: {}", e)))
111 }
112 };
113
114 let output = quote! {
115 #function
116
117 pub struct #struct_ident;
118
119 impl macroforge_ts::host::Macroforge for #struct_ident {
120 fn name(&self) -> &str {
121 #macro_name
122 }
123
124 fn kind(&self) -> macroforge_ts::ts_syn::MacroKind {
125 #kind_expr
126 }
127
128 fn run(&self, input: macroforge_ts::ts_syn::TsStream) -> macroforge_ts::ts_syn::MacroResult {
129 match #fn_ident(input) {
130 Ok(stream) => macroforge_ts::ts_syn::TsStream::into_result(stream),
131 Err(err) => err.into(),
132 }
133 }
134
135 fn description(&self) -> &str {
136 #description
137 }
138 }
139
140 #[allow(non_upper_case_globals)]
141 const #ctor_ident: fn() -> std::sync::Arc<dyn macroforge_ts::host::Macroforge> = || {
142 std::sync::Arc::new(#struct_ident)
143 };
144
145 #[allow(non_upper_case_globals)]
146 static #decorator_array_ident: &[macroforge_ts::host::derived::DecoratorDescriptor] = &[
147 #(#decorator_exprs),*
148 ];
149
150 #[allow(non_upper_case_globals)]
151 static #descriptor_ident: macroforge_ts::host::derived::DerivedMacroDescriptor =
152 macroforge_ts::host::derived::DerivedMacroDescriptor {
153 package: #package_expr,
154 module: #module_expr,
155 runtime: &[#(#runtime_exprs),*],
156 name: #macro_name,
157 kind: #kind_expr,
158 description: #description,
159 constructor: #ctor_ident,
160 decorators: #decorator_array_ident,
161 };
162
163 macroforge_ts::inventory::submit! {
164 macroforge_ts::host::derived::DerivedMacroRegistration {
165 descriptor: &#descriptor_ident
166 }
167 }
168
169 #(#decorator_stubs)*
170
171 #main_macro_napi_stub
172
173 #run_macro_napi
174 };
175
176 output.into()
177}
178
179fn pascal_case_ident(ident: &Ident) -> Ident {
180 let raw = ident.to_string();
181 let trimmed = raw.trim_start_matches("r#");
182 let pascal = trimmed.to_case(Case::Pascal);
183 format_ident!("{}", pascal)
184}
185
186fn parse_macro_options(tokens: TokenStream2) -> Result<MacroOptions> {
187 if tokens.is_empty() {
188 return Err(syn::Error::new(
189 Span::call_site(),
190 "ts_macro_derive requires a macro name as the first argument",
191 ));
192 }
193
194 let mut opts = MacroOptions::default();
195 let mut tokens_iter = tokens.into_iter().peekable();
196
197 let first = tokens_iter
199 .next()
200 .ok_or_else(|| syn::Error::new(Span::call_site(), "expected macro name"))?;
201
202 opts.name = match first {
203 proc_macro2::TokenTree::Ident(ident) => ident,
204 _ => {
205 return Err(syn::Error::new(
206 first.span(),
207 "macro name must be an identifier",
208 ));
209 }
210 };
211
212 if let Some(proc_macro2::TokenTree::Punct(p)) = tokens_iter.peek()
214 && p.as_char() == ','
215 {
216 tokens_iter.next();
217 }
218
219 let remaining: TokenStream2 = tokens_iter.collect();
221
222 if !remaining.is_empty() {
223 let parser = syn::meta::parser(|meta| {
224 if meta.path.is_ident("description") {
225 opts.description = Some(meta.value()?.parse()?);
226 } else if meta.path.is_ident("kind") {
227 let lit: LitStr = meta.value()?.parse()?;
228 opts.kind = MacroKindOption::from_lit(&lit)?;
229 } else if meta.path.is_ident("attributes") {
230 meta.parse_nested_meta(|attr_meta| {
231 if let Some(ident) = attr_meta.path.get_ident() {
232 opts.attributes.push(ident.clone());
233 } else {
234 return Err(syn::Error::new(
235 attr_meta.path.span(),
236 "attribute name must be an identifier",
237 ));
238 }
239 Ok(())
240 })?;
241 } else {
242 return Err(syn::Error::new(
243 meta.path.span(),
244 "unknown ts_macro_derive option",
245 ));
246 }
247 Ok(())
248 });
249
250 parser.parse2(remaining)?;
251 }
252
253 Ok(opts)
254}
255
256struct MacroOptions {
257 name: Ident,
258 description: Option<LitStr>,
259 kind: MacroKindOption,
260 attributes: Vec<Ident>,
261}
262
263impl Default for MacroOptions {
264 fn default() -> Self {
265 MacroOptions {
266 name: Ident::new("Unknown", Span::call_site()),
267 description: None,
268 kind: MacroKindOption::Derive,
269 attributes: Vec::new(),
270 }
271 }
272}
273
274fn features_args_type_literal() -> LitStr {
275 LitStr::new(
277 "...features: Array<string | ClassDecorator | PropertyDecorator | ((...args:\n any[]) => unknown) | Record<string, unknown>>",
278 Span::call_site(),
279 )
280}
281
282fn generate_decorator_descriptor(attr_name: &Ident, package_expr: &TokenStream2) -> TokenStream2 {
284 let attr_str = LitStr::new(&attr_name.to_string(), attr_name.span());
285 let kind = quote! { macroforge_ts::host::derived::DecoratorKind::Property };
286 let docs = LitStr::new("", Span::call_site());
287
288 quote! {
289 macroforge_ts::host::derived::DecoratorDescriptor {
290 module: #package_expr,
291 export: #attr_str,
292 kind: #kind,
293 docs: #docs,
294 }
295 }
296}
297
298fn generate_decorator_stub(
300 attr_name: &Ident,
301 owner_ident: &Ident,
302 description: Option<&LitStr>,
303) -> TokenStream2 {
304 let owner_snake = owner_ident.to_string().to_case(Case::Snake);
305 let decorator_snake = attr_name.to_string().to_case(Case::Snake);
306 let fn_ident = format_ident!("__ts_macro_stub_{}_{}", owner_snake, decorator_snake);
307 let js_name = LitStr::new(&attr_name.to_string(), attr_name.span());
308
309 let doc_comment = description.map(|desc| {
310 let desc_str = desc.value();
311 quote! {
312 #[doc = #desc_str]
313 }
314 });
315
316 let decorator_args_type = features_args_type_literal();
317
318 quote! {
319 #doc_comment
320 #[macroforge_ts::napi_derive::napi(
321 js_name = #js_name,
322 ts_args_type = #decorator_args_type,
323 ts_return_type = "PropertyDecorator"
324 )]
325 pub fn #fn_ident() -> macroforge_ts::napi::Result<()> {
326 Ok(())
327 }
328 }
329}
330
331#[derive(Clone, Default)]
332enum MacroKindOption {
333 #[default]
334 Derive,
335 Attribute,
336 Call,
337}
338
339impl MacroKindOption {
340 fn as_tokens(&self) -> TokenStream2 {
341 match self {
342 MacroKindOption::Derive => quote! { macroforge_ts::ts_syn::MacroKind::Derive },
343 MacroKindOption::Attribute => quote! { macroforge_ts::ts_syn::MacroKind::Attribute },
344 MacroKindOption::Call => quote! { macroforge_ts::ts_syn::MacroKind::Call },
345 }
346 }
347
348 fn from_lit(lit: &LitStr) -> Result<Self> {
349 match lit.value().to_ascii_lowercase().as_str() {
350 "derive" => Ok(MacroKindOption::Derive),
351 "attribute" => Ok(MacroKindOption::Attribute),
352 "function" | "call" => Ok(MacroKindOption::Call),
353 _ => Err(syn::Error::new(
354 lit.span(),
355 "kind must be one of 'derive', 'attribute', or 'function'",
356 )),
357 }
358 }
359}