1use std::path::{Path, PathBuf};
2
3use serde::Deserialize;
4
5#[derive(Deserialize)]
6struct Metadata {
7 packages: Vec<Package>,
8}
9
10#[derive(Deserialize)]
11struct Package {
12 name: String,
13 manifest_path: String,
14}
15
16pub struct ProjectedModule {
18 pub package: &'static str,
20 pub dts_filename: &'static str,
22 pub specifier: &'static str,
24 pub subpath: &'static str,
26 pub required: bool,
28}
29
30impl ProjectedModule {
31 pub fn marker(&self) -> String {
33 format!("superui_modules/{}", self.subpath)
34 }
35 pub fn index_path(&self) -> String {
37 format!("./superui_modules/{}/index.d.ts", self.subpath)
38 }
39}
40
41pub fn projected_modules() -> Vec<ProjectedModule> {
44 vec![
45 ProjectedModule {
46 package: "supersolid",
47 dts_filename: "supersolid.d.ts",
48 specifier: "supersolid",
49 subpath: "supersolid",
50 required: true,
51 },
52 ProjectedModule {
53 package: "superui",
54 dts_filename: "superui-test.d.ts",
55 specifier: "superui/test",
56 subpath: "superui/test",
57 required: false,
58 },
59 ]
60}
61
62pub fn find_module_dts(metadata_json: &str, package: &str, filename: &str) -> Option<PathBuf> {
65 let meta: Metadata = serde_json::from_str(metadata_json).ok()?;
66 let pkg = meta.packages.into_iter().find(|p| p.name == package)?;
67 let dir = Path::new(&pkg.manifest_path).parent()?;
68 Some(dir.join(filename))
69}
70
71pub fn find_supersolid_dts(metadata_json: &str) -> Option<PathBuf> {
73 find_module_dts(metadata_json, "supersolid", "supersolid.d.ts")
74}
75
76pub const SUPERSOLID_PATH_MARKER: &str = "superui_modules/supersolid";
78
79pub const GITIGNORE_ENTRY: &str = "superui_modules/";
81
82pub const TSCONFIG_TEMPLATE: &str = r#"{
85 // Editor-only config: it tells TypeScript / your IDE how to resolve and
86 // type-check the .tsx UI. superui's Rust transpiler is the real consumer of
87 // the source — nothing here affects the build or runtime.
88 "compilerOptions": {
89 "jsx": "preserve", // leave JSX untouched (no React runtime)
90 "module": "esnext", // use modern ES module syntax
91 "moduleResolution": "bundler", // resolve bare imports the way a bundler does
92 "target": "esnext", // don't down-level; we only type-check
93 "lib": ["ESNext", "DOM"], // std JS + web globals (document, console, fetch, timers)
94 "noEmit": true, // never emit output — editor concern only
95 "baseUrl": ".", // anchor the paths below to this folder
96 "paths": {
97 // map the bare `supersolid` import to its projected editor types
98 "supersolid": ["./superui_modules/supersolid/index.d.ts"],
99 // map the `superui/test` import to the projected test-framework types
100 "superui/test": ["./superui_modules/superui/test/index.d.ts"]
101 }
102 },
103 // files the language server should type-check: projected types, UI, and specs
104 "include": ["superui_modules/**/*.d.ts", "assets/**/*.ts", "assets/**/*.tsx", "tests/**/*.ts"]
105}
106"#;
107
108pub fn tsconfig_has_path(src: &str, marker: &str) -> bool {
111 src.contains(marker)
112}
113
114pub fn tsconfig_has_supersolid_path(src: &str) -> bool {
116 tsconfig_has_path(src, SUPERSOLID_PATH_MARKER)
117}
118
119pub fn gitignore_needs_entry(src: &str) -> bool {
123 !src.lines().any(|l| l.trim() == GITIGNORE_ENTRY.trim_end_matches('/')
124 || l.trim() == GITIGNORE_ENTRY)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn finds_supersolid_dts_from_metadata() {
133 let json = r#"{
134 "packages": [
135 { "name": "superui", "manifest_path": "/w/crates/superui/Cargo.toml" },
136 { "name": "supersolid", "manifest_path": "/w/crates/supersolid/Cargo.toml" }
137 ]
138 }"#;
139 let got = find_supersolid_dts(json).unwrap();
140 assert_eq!(got, PathBuf::from("/w/crates/supersolid/supersolid.d.ts"));
141 }
142
143 #[test]
144 fn returns_none_when_supersolid_absent() {
145 let json = r#"{ "packages": [ { "name": "bevy", "manifest_path": "/x/Cargo.toml" } ] }"#;
146 assert!(find_supersolid_dts(json).is_none());
147 }
148
149 #[test]
150 fn tsconfig_template_maps_supersolid() {
151 assert!(tsconfig_has_supersolid_path(TSCONFIG_TEMPLATE));
152 }
153
154 #[test]
155 fn detects_missing_supersolid_path() {
156 let existing = r#"{ "compilerOptions": { "jsx": "preserve" } }"#;
157 assert!(!tsconfig_has_supersolid_path(existing));
158 }
159
160 #[test]
161 fn detects_present_supersolid_path() {
162 let existing = r#"{ "compilerOptions": { "paths": {
163 "supersolid": ["./superui_modules/supersolid/index.d.ts"] } } }"#;
164 assert!(tsconfig_has_supersolid_path(existing));
165 }
166
167 #[test]
168 fn gitignore_needs_entry_when_absent() {
169 assert!(gitignore_needs_entry("/target\n"));
170 }
171
172 #[test]
173 fn gitignore_ok_when_present() {
174 assert!(!gitignore_needs_entry("/target\nsuperui_modules/\n"));
175 }
176
177 #[test]
178 fn gitignore_ok_when_present_without_slash() {
179 assert!(!gitignore_needs_entry("/target\nsuperui_modules\n"));
180 }
181
182 #[test]
183 fn tsconfig_template_maps_superui_test() {
184 let m = projected_modules().into_iter().find(|m| m.specifier == "superui/test").unwrap();
185 assert!(tsconfig_has_path(TSCONFIG_TEMPLATE, &m.marker()));
186 }
187
188 #[test]
189 fn projected_modules_marks_supersolid_required_and_test_optional() {
190 let mods = projected_modules();
191 assert!(mods.iter().find(|m| m.specifier == "supersolid").unwrap().required);
192 assert!(!mods.iter().find(|m| m.specifier == "superui/test").unwrap().required);
193 }
194
195 #[test]
196 fn superui_test_module_paths_are_correct() {
197 let m = projected_modules().into_iter().find(|m| m.specifier == "superui/test").unwrap();
198 assert_eq!(m.package, "superui");
199 assert_eq!(m.dts_filename, "superui-test.d.ts");
200 assert_eq!(m.marker(), "superui_modules/superui/test");
201 assert_eq!(m.index_path(), "./superui_modules/superui/test/index.d.ts");
202 }
203
204 #[test]
205 fn finds_superui_test_dts_from_metadata() {
206 let json = r#"{
207 "packages": [
208 { "name": "superui", "manifest_path": "/w/crates/superui/Cargo.toml" },
209 { "name": "supersolid", "manifest_path": "/w/crates/supersolid/Cargo.toml" }
210 ]
211 }"#;
212 let got = find_module_dts(json, "superui", "superui-test.d.ts").unwrap();
213 assert_eq!(got, PathBuf::from("/w/crates/superui/superui-test.d.ts"));
214 }
215
216 #[test]
217 fn find_module_dts_none_when_package_absent() {
218 let json = r#"{ "packages": [ { "name": "bevy", "manifest_path": "/x/Cargo.toml" } ] }"#;
219 assert!(find_module_dts(json, "superui", "superui-test.d.ts").is_none());
220 }
221
222 #[test]
223 fn optional_module_absent_is_skip_not_error() {
224 let json = r#"{ "packages": [
227 { "name": "supersolid", "manifest_path": "/w/crates/supersolid/Cargo.toml" }
228 ] }"#;
229 for m in projected_modules() {
230 let found = find_module_dts(json, m.package, m.dts_filename);
231 if m.required {
232 assert!(found.is_some(), "required module {} must resolve", m.specifier);
233 } else {
234 assert!(found.is_none(), "optional module {} absent -> None", m.specifier);
236 }
237 }
238 }
239}