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