Skip to main content

oag_node_client/emitters/
bundled.rs

1use oag_core::ir::IrSpec;
2
3use crate::emitters;
4
5/// Emit a single `index.ts` file that bundles types + sse + client together.
6/// Strips relative imports between modules since everything is inlined.
7pub fn emit_bundled(ir: &IrSpec, no_jsdoc: bool) -> String {
8    let types_content = emitters::types::emit_types(ir);
9    let sse_content = emitters::sse::emit_sse();
10    let client_content = emitters::client::emit_client(ir, no_jsdoc);
11
12    let mut output = String::new();
13    output.push_str("// Auto-generated by oag — do not edit (bundled)\n\n");
14
15    // Append types (already standalone, no imports to strip)
16    output.push_str("// === Types ===\n\n");
17    output.push_str(&strip_auto_generated_header(&types_content));
18    output.push('\n');
19
20    // Append SSE runtime (strip header)
21    output.push_str("// === SSE Runtime ===\n\n");
22    output.push_str(&strip_auto_generated_header(&sse_content));
23    output.push('\n');
24
25    // Append client (strip imports from ./types and ./sse since they're inlined)
26    output.push_str("// === Client ===\n\n");
27    let client_stripped = strip_relative_imports(&strip_auto_generated_header(&client_content));
28    output.push_str(&client_stripped);
29
30    output
31}
32
33/// Remove the "// Auto-generated by oag — do not edit" header line.
34fn strip_auto_generated_header(content: &str) -> String {
35    content
36        .lines()
37        .filter(|line| !line.starts_with("// Auto-generated by oag"))
38        .collect::<Vec<_>>()
39        .join("\n")
40}
41
42/// Remove import lines that reference relative modules (./types, ./sse, ./client).
43fn strip_relative_imports(content: &str) -> String {
44    let lines: Vec<&str> = content.lines().collect();
45    let mut result = Vec::new();
46    let mut i = 0;
47
48    let is_relative = |s: &str| {
49        s.contains("\"./types\"") || s.contains("\"./sse\"") || s.contains("\"./client\"")
50    };
51
52    while i < lines.len() {
53        let trimmed = lines[i].trim();
54
55        if trimmed.starts_with("import ") || trimmed.starts_with("import type ") {
56            // Single-line import ending with `;`
57            if trimmed.ends_with(';') {
58                if is_relative(trimmed) {
59                    i += 1;
60                    continue;
61                }
62                result.push(lines[i]);
63                i += 1;
64                continue;
65            }
66
67            // Multi-line import — scan ahead to the closing `} from "...";`
68            let start = i;
69            i += 1;
70            let mut found_close = false;
71            while i < lines.len() {
72                let t = lines[i].trim();
73                if t.starts_with("} from ") {
74                    found_close = true;
75                    if is_relative(t) {
76                        // Skip entire block (start..=i)
77                        i += 1;
78                    } else {
79                        // Keep entire block
80                        result.extend_from_slice(&lines[start..=i]);
81                        i += 1;
82                    }
83                    break;
84                }
85                i += 1;
86            }
87            if !found_close {
88                // Unterminated import block — keep lines as-is
89                result.extend_from_slice(&lines[start..i]);
90            }
91            continue;
92        }
93
94        result.push(lines[i]);
95        i += 1;
96    }
97
98    result.join("\n")
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_strip_relative_imports() {
107        let content = r#"import type {
108  Pet,
109  NewPet,
110} from "./types";
111import { streamSse, type SSEOptions } from "./sse";
112
113export class ApiClient {"#;
114
115        let stripped = strip_relative_imports(content);
116        assert!(!stripped.contains("from \"./types\""));
117        assert!(!stripped.contains("from \"./sse\""));
118        assert!(stripped.contains("export class ApiClient {"));
119    }
120
121    #[test]
122    fn test_strip_auto_generated_header() {
123        let content = "// Auto-generated by oag — do not edit\nexport interface Foo {}";
124        let stripped = strip_auto_generated_header(content);
125        assert!(!stripped.contains("Auto-generated"));
126        assert!(stripped.contains("export interface Foo {}"));
127    }
128}