fresh_plugin_runtime/
ts_export.rs

1//! TypeScript type generation using ts-rs
2//!
3//! This module collects all API types with `#[derive(TS)]` and generates
4//! TypeScript declarations that are combined with the proc macro output.
5//! The generated TypeScript is validated and formatted using oxc.
6//!
7//! Types are automatically collected based on `JSEDITORAPI_REFERENCED_TYPES`
8//! from the proc macro, so when you add a new type to method signatures,
9//! it will automatically be included if it has `#[derive(TS)]`.
10
11use oxc_allocator::Allocator;
12use oxc_codegen::Codegen;
13use oxc_parser::Parser;
14use oxc_span::SourceType;
15use ts_rs::TS;
16
17use fresh_core::api::{
18    ActionPopupAction, ActionSpec, BackgroundProcessResult, BufferInfo, BufferSavedDiff,
19    CompositeHunk, CompositeLayoutConfig, CompositePaneStyle, CompositeSourceConfig,
20    CreateVirtualBufferInExistingSplitOptions, CreateVirtualBufferInSplitOptions,
21    CreateVirtualBufferOptions, CursorInfo, JsTextPropertyEntry, LayoutHints, SpawnResult,
22    TextPropertiesAtCursor, TsHighlightSpan, ViewTokenStyle, ViewTokenWire, ViewTokenWireKind,
23    ViewportInfo,
24};
25
26/// Get the TypeScript declaration for a type by name
27///
28/// Returns None if the type is not known (not registered in this mapping).
29/// Add new types here when they're added to api.rs with `#[derive(TS)]`.
30fn get_type_decl(type_name: &str) -> Option<String> {
31    // Map TypeScript type names to their ts-rs declarations
32    // The type name should match either the Rust struct name or the ts(rename = "...") value
33    match type_name {
34        // Core types
35        "BufferInfo" => Some(BufferInfo::decl()),
36        "CursorInfo" => Some(CursorInfo::decl()),
37        "ViewportInfo" => Some(ViewportInfo::decl()),
38        "ActionSpec" => Some(ActionSpec::decl()),
39        "BufferSavedDiff" => Some(BufferSavedDiff::decl()),
40        "LayoutHints" => Some(LayoutHints::decl()),
41
42        // Process types
43        "SpawnResult" => Some(SpawnResult::decl()),
44        "BackgroundProcessResult" => Some(BackgroundProcessResult::decl()),
45
46        // Composite buffer types (ts-rs renames these with Ts prefix)
47        "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => Some(CompositeLayoutConfig::decl()),
48        "TsCompositeSourceConfig" | "CompositeSourceConfig" => Some(CompositeSourceConfig::decl()),
49        "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl()),
50        "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl()),
51
52        // View transform types
53        "ViewTokenWireKind" => Some(ViewTokenWireKind::decl()),
54        "ViewTokenStyle" => Some(ViewTokenStyle::decl()),
55        "ViewTokenWire" => Some(ViewTokenWire::decl()),
56
57        // UI types (ts-rs renames these with Ts prefix)
58        "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl()),
59        "TsHighlightSpan" => Some(TsHighlightSpan::decl()),
60
61        // Virtual buffer option types
62        "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl()),
63        "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl()),
64        "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl()),
65        "CreateVirtualBufferInExistingSplitOptions" => {
66            Some(CreateVirtualBufferInExistingSplitOptions::decl())
67        }
68
69        // Return types
70        "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl()),
71
72        _ => None,
73    }
74}
75
76/// Types that are dependencies of other types and must always be included.
77/// These are types referenced inside option structs or other complex types
78/// that aren't directly in method signatures.
79const DEPENDENCY_TYPES: &[&str] = &[
80    "TextPropertyEntry", // Used in CreateVirtualBuffer*Options.entries
81];
82
83/// Collect TypeScript type declarations based on referenced types from proc macro
84///
85/// Uses `JSEDITORAPI_REFERENCED_TYPES` to determine which types to include.
86/// Also includes dependency types that are referenced by other types.
87pub fn collect_ts_types() -> String {
88    use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
89
90    let mut types = Vec::new();
91    let mut included = std::collections::HashSet::new();
92
93    // First, include dependency types (order matters - dependencies first)
94    for type_name in DEPENDENCY_TYPES {
95        if let Some(decl) = get_type_decl(type_name) {
96            types.push(decl);
97            included.insert(*type_name);
98        }
99    }
100
101    // Collect types referenced by the API
102    for type_name in JSEDITORAPI_REFERENCED_TYPES {
103        if included.contains(*type_name) {
104            continue;
105        }
106        if let Some(decl) = get_type_decl(type_name) {
107            types.push(decl);
108            included.insert(*type_name);
109        } else {
110            // Log warning for unknown types (these need to be added to get_type_decl)
111            eprintln!(
112                "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
113                type_name
114            );
115        }
116    }
117
118    types.join("\n\n")
119}
120
121/// Validate TypeScript syntax using oxc parser
122///
123/// Returns Ok(()) if the syntax is valid, or an error with the parse errors.
124pub fn validate_typescript(source: &str) -> Result<(), String> {
125    let allocator = Allocator::default();
126    let source_type = SourceType::d_ts();
127
128    let parser_ret = Parser::new(&allocator, source, source_type).parse();
129
130    if parser_ret.errors.is_empty() {
131        Ok(())
132    } else {
133        let errors: Vec<String> = parser_ret
134            .errors
135            .iter()
136            .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
137            .collect();
138        Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
139    }
140}
141
142/// Format TypeScript source code using oxc codegen
143///
144/// Parses the TypeScript and regenerates it with consistent formatting.
145/// Returns the original source if parsing fails.
146pub fn format_typescript(source: &str) -> String {
147    let allocator = Allocator::default();
148    let source_type = SourceType::d_ts();
149
150    let parser_ret = Parser::new(&allocator, source, source_type).parse();
151
152    if !parser_ret.errors.is_empty() {
153        // Return original source if parsing fails
154        return source.to_string();
155    }
156
157    // Generate formatted code from AST
158    Codegen::new().build(&parser_ret.program).code
159}
160
161/// Generate and write the complete fresh.d.ts file
162///
163/// Combines ts-rs generated types with proc macro output,
164/// validates the syntax, formats the output, and writes to disk.
165pub fn write_fresh_dts() -> Result<(), String> {
166    use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
167
168    let ts_types = collect_ts_types();
169
170    let content = format!(
171        "{}\n{}\n{}",
172        JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
173    );
174
175    // Validate the generated TypeScript syntax
176    validate_typescript(&content)?;
177
178    // Format the TypeScript
179    let formatted = format_typescript(&content);
180
181    // Determine output path
182    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
183    let output_path = std::path::Path::new(&manifest_dir)
184        .join("plugins")
185        .join("lib")
186        .join("fresh.d.ts");
187
188    // Only write if content changed
189    let should_write = match std::fs::read_to_string(&output_path) {
190        Ok(existing) => existing != formatted,
191        Err(_) => true,
192    };
193
194    if should_write {
195        if let Some(parent) = output_path.parent() {
196            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
197        }
198        std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
199    }
200
201    Ok(())
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    /// Generate, validate, format, and write fresh.d.ts
209    /// Run with: cargo test --features plugins write_fresh_dts_file -- --ignored --nocapture
210    #[test]
211    #[ignore]
212    fn write_fresh_dts_file() {
213        // write_fresh_dts validates syntax and formats before writing
214        write_fresh_dts().expect("Failed to write fresh.d.ts");
215        println!("Successfully generated, validated, and formatted fresh.d.ts");
216    }
217}