alef_e2e/codegen/typescript/
mod.rs1mod assertions;
4mod config;
5mod json;
6mod test_file;
7mod visitors;
8
9use crate::config::E2eConfig;
10use crate::field_access::FieldResolver;
11use crate::fixture::{Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use anyhow::Result;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use config::{render_file_setup, render_global_setup, render_package_json, render_tsconfig, render_vitest_config};
19pub use test_file::render_test_file;
20use test_file::resolve_node_function_name;
21
22pub struct TypeScriptCodegen;
24
25impl E2eCodegen for TypeScriptCodegen {
26 fn generate(
27 &self,
28 groups: &[FixtureGroup],
29 e2e_config: &E2eConfig,
30 config: &ResolvedCrateConfig,
31 type_defs: &[alef_core::ir::TypeDef],
32 enums: &[alef_core::ir::EnumDef],
33 ) -> Result<Vec<GeneratedFile>> {
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(self.language_name());
35 let tests_base = output_base.join("tests");
36
37 let mut files = Vec::new();
38
39 let call = &e2e_config.call;
41 let overrides = call.overrides.get("node");
42 let module_path = overrides
43 .and_then(|o| o.module.as_ref())
44 .cloned()
45 .unwrap_or_else(|| call.module.clone());
46 let function_name = overrides.and_then(|o| o.function.as_ref()).cloned().unwrap_or_else(|| {
47 let default_cc = e2e_config.resolve_call(None);
48 resolve_node_function_name(default_cc)
49 });
50 let client_factory = overrides.and_then(|o| o.client_factory.as_deref());
51
52 let node_pkg = e2e_config.resolve_package("node");
54 let pkg_path = node_pkg
55 .as_ref()
56 .and_then(|p| p.path.as_ref())
57 .cloned()
58 .unwrap_or_else(|| "../../packages/typescript".to_string());
59 let pkg_name = node_pkg
60 .as_ref()
61 .and_then(|p| p.name.as_ref())
62 .cloned()
63 .unwrap_or_else(|| module_path.clone());
64 let pkg_version = node_pkg
65 .as_ref()
66 .and_then(|p| p.version.as_ref())
67 .cloned()
68 .or_else(|| config.resolved_version())
69 .unwrap_or_else(|| "0.1.0".to_string());
70
71 let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.is_http_test());
72
73 let has_file_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
74 let cc = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
75 cc.args
76 .iter()
77 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
78 });
79
80 files.push(GeneratedFile {
81 path: output_base.join("package.json"),
82 content: render_package_json(
83 &pkg_name,
84 &pkg_path,
85 &pkg_version,
86 e2e_config.dep_mode,
87 has_http_fixtures,
88 ),
89 generated_header: false,
90 });
91
92 files.push(GeneratedFile {
93 path: output_base.join("tsconfig.json"),
94 content: render_tsconfig(),
95 generated_header: false,
96 });
97
98 let any_needs_mock_server = groups
105 .iter()
106 .flat_map(|g| g.fixtures.iter())
107 .any(|f| f.needs_mock_server());
108 let needs_global_setup = client_factory.is_some() || has_http_fixtures || any_needs_mock_server;
109
110 files.push(GeneratedFile {
111 path: output_base.join("vitest.config.ts"),
112 content: render_vitest_config(needs_global_setup, has_file_fixtures),
113 generated_header: true,
114 });
115
116 if needs_global_setup {
117 files.push(GeneratedFile {
118 path: output_base.join("globalSetup.ts"),
119 content: render_global_setup(),
120 generated_header: true,
121 });
122 }
123
124 if has_file_fixtures {
125 files.push(GeneratedFile {
126 path: output_base.join("setup.ts"),
127 content: render_file_setup(&e2e_config.test_documents_dir),
128 generated_header: true,
129 });
130 }
131
132 let options_type = overrides.and_then(|o| o.options_type.clone());
133 let field_resolver = FieldResolver::new(
134 &e2e_config.fields,
135 &e2e_config.fields_optional,
136 &e2e_config.result_fields,
137 &e2e_config.fields_array,
138 &std::collections::HashSet::new(),
139 );
140
141 for group in groups {
142 let active: Vec<_> = group
143 .fixtures
144 .iter()
145 .filter(|fixture| is_node_fixture_runnable(fixture))
146 .collect();
147
148 if active.is_empty() {
149 continue;
150 }
151
152 let filename = format!("{}.test.ts", crate::escape::sanitize_filename(&group.category));
153 let content = render_test_file(
154 "node",
155 &group.category,
156 &active,
157 &module_path,
158 &pkg_name,
159 &function_name,
160 &e2e_config.call.args,
161 options_type.as_deref(),
162 &field_resolver,
163 client_factory,
164 e2e_config,
165 type_defs,
166 enums,
167 "",
168 );
169 files.push(GeneratedFile {
170 path: tests_base.join(filename),
171 content,
172 generated_header: true,
173 });
174 }
175
176 Ok(files)
177 }
178
179 fn language_name(&self) -> &'static str {
180 "node"
181 }
182}
183
184fn is_node_fixture_runnable(fixture: &Fixture) -> bool {
185 if fixture.skip.as_ref().is_some_and(|skip| skip.should_skip("node")) {
186 return false;
187 }
188
189 if let Some(http) = &fixture.http {
190 return http.expected_response.status_code != 101;
191 }
192
193 !fixture.assertions.is_empty()
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn language_name_is_node() {
202 let codegen = TypeScriptCodegen;
203 assert_eq!(codegen.language_name(), "node");
204 }
205
206 #[test]
207 fn generate_empty_groups_produces_config_files_only() {
208 use alef_core::config::NewAlefConfig;
209 let cfg: NewAlefConfig = toml::from_str(
210 r#"
211[workspace]
212languages = ["node"]
213
214[[crates]]
215name = "my-lib"
216sources = ["src/lib.rs"]
217
218[crates.e2e]
219fixtures = "fixtures"
220output = "e2e"
221[crates.e2e.call]
222function = "process"
223module = "my-lib"
224result_var = "result"
225"#,
226 )
227 .unwrap();
228 let e2e = cfg.crates[0].e2e.clone().unwrap();
229 let resolved = cfg.resolve().unwrap().remove(0);
230 let codegen = TypeScriptCodegen;
231 let files = codegen.generate(&[], &e2e, &resolved, &[], &[]).unwrap();
232 assert!(files.len() >= 3, "got {} files", files.len());
234 }
235}