Skip to main content

superui_cli/
lib.rs

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
16/// One editor-types module projected by `cargo superui install`.
17pub struct ProjectedModule {
18    /// cargo-metadata package that ships the canonical `.d.ts`.
19    pub package: &'static str,
20    /// Canonical `.d.ts` filename beside that package's `Cargo.toml`.
21    pub dts_filename: &'static str,
22    /// tsconfig `paths` key / import specifier.
23    pub specifier: &'static str,
24    /// Location under `superui_modules/` (also the tsconfig marker tail).
25    pub subpath: &'static str,
26    /// Absence of a required module's package is a hard error; optional is a skip.
27    pub required: bool,
28}
29
30impl ProjectedModule {
31    /// Unique substring identifying this module's `paths` entry in a tsconfig.
32    pub fn marker(&self) -> String {
33        format!("superui_modules/{}", self.subpath)
34    }
35    /// The `paths` value pointing at the projected `index.d.ts`.
36    pub fn index_path(&self) -> String {
37        format!("./superui_modules/{}/index.d.ts", self.subpath)
38    }
39}
40
41/// The modules `cargo superui install` projects, in order. supersolid is the
42/// primary (required) module; superui/test types are optional.
43pub 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
62/// Given `cargo metadata` JSON, locate `package` and return the path to
63/// `filename` bundled alongside its `Cargo.toml`.
64pub 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
71/// Locate the `supersolid` package's bundled `supersolid.d.ts`.
72pub fn find_supersolid_dts(metadata_json: &str) -> Option<PathBuf> {
73    find_module_dts(metadata_json, "supersolid", "supersolid.d.ts")
74}
75
76/// Marker substring identifying the projected supersolid module in a tsconfig.
77pub const SUPERSOLID_PATH_MARKER: &str = "superui_modules/supersolid";
78
79/// The `.gitignore` line that hides the derived module tree.
80pub const GITIGNORE_ENTRY: &str = "superui_modules/";
81
82/// tsconfig written verbatim when an app dir has none. Makes
83/// `import ... from "supersolid"` resolve to the projected module.
84pub 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
108/// True if the tsconfig source already contains a `paths` marker substring.
109/// Substring check (not a JSONC parse) — sufficient because markers are unique.
110pub fn tsconfig_has_path(src: &str, marker: &str) -> bool {
111    src.contains(marker)
112}
113
114/// True if the tsconfig maps the supersolid module (back-compat wrapper).
115pub fn tsconfig_has_supersolid_path(src: &str) -> bool {
116    tsconfig_has_path(src, SUPERSOLID_PATH_MARKER)
117}
118
119/// True if `.gitignore` source lacks a line ignoring the module tree. A bare
120/// `superui_modules` (no trailing slash) counts as present — git matches the
121/// directory either way.
122pub 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        // A project whose resolved deps include supersolid (required) but NOT
225        // superui (optional superui/test types) must still resolve cleanly.
226        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                // optional module simply resolves to None -> caller skips it
235                assert!(found.is_none(), "optional module {} absent -> None", m.specifier);
236            }
237        }
238    }
239}