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