code_moniker_core/lang/ts/
build.rs1use serde_json::Value;
2
3#[derive(Clone, Debug, Eq, PartialEq)]
4pub struct Dep {
5 pub name: String,
6 pub version: Option<String>,
7 pub dep_kind: String,
8 pub import_root: String,
9}
10
11#[derive(Debug)]
12pub enum PackageJsonError {
13 Parse(serde_json::Error),
14 Schema(String),
15}
16
17impl std::fmt::Display for PackageJsonError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::Parse(e) => write!(f, "package.json parse error: {e}"),
21 Self::Schema(s) => write!(f, "package.json schema error: {s}"),
22 }
23 }
24}
25
26impl std::error::Error for PackageJsonError {}
27
28pub fn parse(content: &str) -> Result<Vec<Dep>, PackageJsonError> {
29 let value: Value = serde_json::from_str(content).map_err(PackageJsonError::Parse)?;
30 let obj = value
31 .as_object()
32 .ok_or_else(|| PackageJsonError::Schema("top-level value is not a JSON object".into()))?;
33 let mut out = Vec::new();
34
35 if let Some(name) = obj.get("name").and_then(Value::as_str) {
36 let version = obj
37 .get("version")
38 .and_then(Value::as_str)
39 .map(str::to_string);
40 out.push(Dep {
41 name: name.to_string(),
42 version,
43 dep_kind: "package".to_string(),
44 import_root: ts_import_root(name),
45 });
46 }
47
48 for (field, kind_label) in [
49 ("dependencies", "normal"),
50 ("devDependencies", "dev"),
51 ("peerDependencies", "peer"),
52 ("optionalDependencies", "optional"),
53 ] {
54 let Some(table) = obj.get(field).and_then(Value::as_object) else {
55 continue;
56 };
57 for (name, spec) in table {
58 let version = extract_version(spec);
59 out.push(Dep {
60 name: name.clone(),
61 version,
62 dep_kind: kind_label.to_string(),
63 import_root: ts_import_root(name),
64 });
65 }
66 }
67
68 Ok(out)
69}
70
71pub(crate) fn ts_import_root(name: &str) -> String {
72 name.to_string()
73}
74
75pub fn package_moniker(project: &[u8], import_root: &str) -> crate::core::moniker::Moniker {
76 super::canonicalize::external_pkg_builder(project, import_root).build()
77}
78
79fn extract_version(spec: &Value) -> Option<String> {
80 match spec {
81 Value::String(s) => Some(s.clone()),
82 Value::Object(o) => o.get("version").and_then(Value::as_str).map(str::to_string),
83 _ => None,
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn parse_minimal_package() {
93 let json = r#"{ "name": "demo", "version": "0.1.0" }"#;
94 let deps = parse(json).unwrap();
95 assert_eq!(
96 deps,
97 vec![Dep {
98 name: "demo".into(),
99 version: Some("0.1.0".into()),
100 dep_kind: "package".into(),
101 import_root: "demo".into(),
102 }]
103 );
104 }
105
106 #[test]
107 fn parse_normal_dep_keeps_version_string() {
108 let json = r#"{
109 "name": "demo",
110 "version": "0.1.0",
111 "dependencies": { "react": "^18.0.0" }
112 }"#;
113 let deps = parse(json).unwrap();
114 assert!(deps.contains(&Dep {
115 name: "react".into(),
116 version: Some("^18.0.0".into()),
117 dep_kind: "normal".into(),
118 import_root: "react".into(),
119 }));
120 }
121
122 #[test]
123 fn parse_object_dep_with_version_field() {
124 let json = r#"{
125 "name": "demo",
126 "version": "0.1.0",
127 "dependencies": { "tsup": { "version": "8.0.0" } }
128 }"#;
129 let deps = parse(json).unwrap();
130 assert!(
131 deps.iter()
132 .any(|d| d.name == "tsup" && d.version.as_deref() == Some("8.0.0"))
133 );
134 }
135
136 #[test]
137 fn parse_dev_peer_optional_kinds() {
138 let json = r#"{
139 "name": "demo",
140 "version": "0.1.0",
141 "devDependencies": { "vitest": "1.0.0" },
142 "peerDependencies": { "react": "^18.0.0" },
143 "optionalDependencies": { "fsevents": "2.0.0" }
144 }"#;
145 let deps = parse(json).unwrap();
146 assert!(
147 deps.iter()
148 .any(|d| d.name == "vitest" && d.dep_kind == "dev")
149 );
150 assert!(
151 deps.iter()
152 .any(|d| d.name == "react" && d.dep_kind == "peer")
153 );
154 assert!(
155 deps.iter()
156 .any(|d| d.name == "fsevents" && d.dep_kind == "optional")
157 );
158 }
159
160 #[test]
161 fn parse_scoped_package_keeps_full_name_in_import_root() {
162 let json = r#"{
163 "name": "demo",
164 "version": "0.1.0",
165 "dependencies": { "@scope/pkg": "1.0.0" }
166 }"#;
167 let deps = parse(json).unwrap();
168 let scoped = deps.iter().find(|d| d.name == "@scope/pkg").unwrap();
169 assert_eq!(scoped.import_root, "@scope/pkg");
170 }
171
172 #[test]
173 fn parse_invalid_json_returns_parse_error() {
174 assert!(matches!(
175 parse("{not json"),
176 Err(PackageJsonError::Parse(_))
177 ));
178 }
179
180 #[test]
181 fn parse_non_object_top_level_is_schema_error() {
182 assert!(matches!(parse("[1,2,3]"), Err(PackageJsonError::Schema(_))));
183 }
184
185 #[test]
186 fn parse_missing_name_omits_package_row() {
187 let json = r#"{
188 "private": true,
189 "dependencies": { "react": "^18.0.0" }
190 }"#;
191 let deps = parse(json).unwrap();
192 assert!(deps.iter().all(|d| d.dep_kind != "package"));
193 assert!(deps.iter().any(|d| d.name == "react"));
194 }
195}