1use serde_json::Value;
2
3use crate::core::moniker::{Moniker, MonikerBuilder};
4
5use super::kinds;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct Dep {
9 pub name: String,
10 pub version: Option<String>,
11 pub dep_kind: String,
12 pub import_root: String,
13}
14
15#[derive(Debug)]
16pub enum PackageJsonError {
17 Parse(serde_json::Error),
18 Schema(String),
19}
20
21impl std::fmt::Display for PackageJsonError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::Parse(e) => write!(f, "package.json parse error: {e}"),
25 Self::Schema(s) => write!(f, "package.json schema error: {s}"),
26 }
27 }
28}
29
30impl std::error::Error for PackageJsonError {}
31
32pub fn parse(content: &str) -> Result<Vec<Dep>, PackageJsonError> {
33 let value: Value = serde_json::from_str(content).map_err(PackageJsonError::Parse)?;
34 let obj = value
35 .as_object()
36 .ok_or_else(|| PackageJsonError::Schema("top-level value is not a JSON object".into()))?;
37 let mut out = Vec::new();
38
39 if let Some(name) = obj.get("name").and_then(Value::as_str) {
40 let version = obj
41 .get("version")
42 .and_then(Value::as_str)
43 .map(str::to_string);
44 out.push(Dep {
45 name: name.to_string(),
46 version,
47 dep_kind: "package".to_string(),
48 import_root: ts_import_root(name),
49 });
50 }
51
52 for (field, kind_label) in [
53 ("dependencies", "normal"),
54 ("devDependencies", "dev"),
55 ("peerDependencies", "peer"),
56 ("optionalDependencies", "optional"),
57 ] {
58 let Some(table) = obj.get(field).and_then(Value::as_object) else {
59 continue;
60 };
61 for (name, spec) in table {
62 let version = extract_version(spec);
63 out.push(Dep {
64 name: name.clone(),
65 version,
66 dep_kind: kind_label.to_string(),
67 import_root: ts_import_root(name),
68 });
69 }
70 }
71
72 Ok(out)
73}
74
75pub(crate) fn ts_import_root(name: &str) -> String {
79 let Some(types) = name.strip_prefix("@types/") else {
80 return name.to_string();
81 };
82 match types.split_once("__") {
83 Some((scope, package)) => format!("@{scope}/{package}"),
84 None => types.to_string(),
85 }
86}
87
88pub fn package_moniker(project: &[u8], import_root: &str) -> Moniker {
89 external_pkg_builder(project, import_root).build()
90}
91
92pub(in crate::lang::ts) fn external_pkg_builder(project: &[u8], pkg: &str) -> MonikerBuilder {
93 let (head, tail) = split_package_specifier(pkg);
94 let mut b = MonikerBuilder::new();
95 b.project(project);
96 b.segment(kinds::EXTERNAL_PKG, head.as_bytes());
97 for piece in tail.split('/').filter(|s| !s.is_empty()) {
98 b.segment(kinds::PATH, piece.as_bytes());
99 }
100 b
101}
102
103pub(in crate::lang::ts) fn module_specifier_builder(project: &[u8], spec: &str) -> MonikerBuilder {
104 if let Some((node_module, tail)) = node_builtin_specifier(spec) {
105 let mut builder = crate::lang::sdk::sdk_target_builder(project, b"ts");
106 builder.segment(kinds::PATH, node_module.as_bytes());
107 for piece in tail.split('/').filter(|piece| !piece.is_empty()) {
108 builder.segment(kinds::PATH, piece.as_bytes());
109 }
110 return builder;
111 }
112 external_pkg_builder(project, spec)
113}
114
115fn node_builtin_specifier(spec: &str) -> Option<(String, &str)> {
116 if let Some(explicit) = spec.strip_prefix("node:") {
117 let (root, tail) = explicit
118 .split_once('/')
119 .map_or((explicit, ""), |(root, tail)| (root, tail));
120 return (!root.is_empty()).then(|| (format!("node:{root}"), tail));
121 }
122 let (root, tail) = spec
123 .split_once('/')
124 .map_or((spec, ""), |(root, tail)| (root, tail));
125 NODE_BARE_BUILTINS
126 .contains(&root)
127 .then(|| (format!("node:{root}"), tail))
128}
129
130const NODE_BARE_BUILTINS: &[&str] = &[
131 "assert",
132 "async_hooks",
133 "buffer",
134 "child_process",
135 "cluster",
136 "console",
137 "constants",
138 "crypto",
139 "dgram",
140 "diagnostics_channel",
141 "dns",
142 "domain",
143 "events",
144 "fs",
145 "http",
146 "http2",
147 "https",
148 "inspector",
149 "module",
150 "net",
151 "os",
152 "path",
153 "perf_hooks",
154 "process",
155 "punycode",
156 "querystring",
157 "readline",
158 "repl",
159 "stream",
160 "string_decoder",
161 "sys",
162 "timers",
163 "tls",
164 "trace_events",
165 "tty",
166 "url",
167 "util",
168 "v8",
169 "vm",
170 "wasi",
171 "worker_threads",
172 "zlib",
173];
174
175fn split_package_specifier(spec: &str) -> (&str, &str) {
176 if let Some(after_scope) = spec.strip_prefix('@') {
177 let mut parts = after_scope.splitn(3, '/');
178 let scope = parts.next().unwrap_or("");
179 let name = parts.next().unwrap_or("");
180 let tail = parts.next().unwrap_or("");
181 let head_end = if name.is_empty() {
182 1 + scope.len()
183 } else {
184 1 + scope.len() + 1 + name.len()
185 };
186 (&spec[..head_end], tail)
187 } else {
188 match spec.find('/') {
189 Some(i) => (&spec[..i], &spec[i + 1..]),
190 None => (spec, ""),
191 }
192 }
193}
194
195fn extract_version(spec: &Value) -> Option<String> {
196 match spec {
197 Value::String(s) => Some(s.clone()),
198 Value::Object(o) => o.get("version").and_then(Value::as_str).map(str::to_string),
199 _ => None,
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn types_package_declares_the_runtime_import_root() {
209 let json = r#"{
210 "name": "demo",
211 "version": "0.1.0",
212 "devDependencies": {
213 "@types/vscode": "^1.90.0",
214 "@types/babel__core": "^7.20.0"
215 }
216 }"#;
217 let deps = parse(json).unwrap();
218 assert!(
219 deps.iter()
220 .any(|d| d.name == "@types/vscode" && d.import_root == "vscode"),
221 "@types/vscode must declare the vscode import root: {deps:#?}"
222 );
223 assert!(
224 deps.iter()
225 .any(|d| d.name == "@types/babel__core" && d.import_root == "@babel/core"),
226 "DefinitelyTyped scoped mangling must unfold: {deps:#?}"
227 );
228 }
229
230 #[test]
231 fn parse_minimal_package() {
232 let json = r#"{ "name": "demo", "version": "0.1.0" }"#;
233 let deps = parse(json).unwrap();
234 assert_eq!(
235 deps,
236 vec![Dep {
237 name: "demo".into(),
238 version: Some("0.1.0".into()),
239 dep_kind: "package".into(),
240 import_root: "demo".into(),
241 }]
242 );
243 }
244
245 #[test]
246 fn parse_normal_dep_keeps_version_string() {
247 let json = r#"{
248 "name": "demo",
249 "version": "0.1.0",
250 "dependencies": { "react": "^18.0.0" }
251 }"#;
252 let deps = parse(json).unwrap();
253 assert!(deps.contains(&Dep {
254 name: "react".into(),
255 version: Some("^18.0.0".into()),
256 dep_kind: "normal".into(),
257 import_root: "react".into(),
258 }));
259 }
260
261 #[test]
262 fn parse_object_dep_with_version_field() {
263 let json = r#"{
264 "name": "demo",
265 "version": "0.1.0",
266 "dependencies": { "tsup": { "version": "8.0.0" } }
267 }"#;
268 let deps = parse(json).unwrap();
269 assert!(
270 deps.iter()
271 .any(|d| d.name == "tsup" && d.version.as_deref() == Some("8.0.0"))
272 );
273 }
274
275 #[test]
276 fn parse_dev_peer_optional_kinds() {
277 let json = r#"{
278 "name": "demo",
279 "version": "0.1.0",
280 "devDependencies": { "vitest": "1.0.0" },
281 "peerDependencies": { "react": "^18.0.0" },
282 "optionalDependencies": { "fsevents": "2.0.0" }
283 }"#;
284 let deps = parse(json).unwrap();
285 assert!(
286 deps.iter()
287 .any(|d| d.name == "vitest" && d.dep_kind == "dev")
288 );
289 assert!(
290 deps.iter()
291 .any(|d| d.name == "react" && d.dep_kind == "peer")
292 );
293 assert!(
294 deps.iter()
295 .any(|d| d.name == "fsevents" && d.dep_kind == "optional")
296 );
297 }
298
299 #[test]
300 fn parse_scoped_package_keeps_full_name_in_import_root() {
301 let json = r#"{
302 "name": "demo",
303 "version": "0.1.0",
304 "dependencies": { "@scope/pkg": "1.0.0" }
305 }"#;
306 let deps = parse(json).unwrap();
307 let scoped = deps.iter().find(|d| d.name == "@scope/pkg").unwrap();
308 assert_eq!(scoped.import_root, "@scope/pkg");
309 }
310
311 #[test]
312 fn package_moniker_keeps_scoped_package_head() {
313 let moniker = package_moniker(b"app", "@scope/pkg/sub/path");
314 let segments = moniker.as_view().segments().collect::<Vec<_>>();
315 assert_eq!(segments[0].kind, b"external_pkg");
316 assert_eq!(segments[0].name, b"@scope/pkg");
317 assert_eq!(segments[1].kind, b"path");
318 assert_eq!(segments[1].name, b"sub");
319 assert_eq!(segments[2].kind, b"path");
320 assert_eq!(segments[2].name, b"path");
321 }
322
323 #[test]
324 fn node_prefixed_and_bare_builtins_share_one_sdk_identity() {
325 let prefixed = module_specifier_builder(b"app", "node:fs/promises").build();
326 let bare = module_specifier_builder(b"app", "fs/promises").build();
327
328 assert_eq!(bare, prefixed);
329 let segments = bare.as_view().segments().collect::<Vec<_>>();
330 assert_eq!(
331 segments
332 .iter()
333 .map(|segment| (segment.kind, segment.name))
334 .collect::<Vec<_>>(),
335 vec![
336 (b"sdk".as_slice(), b"ts".as_slice()),
337 (b"path".as_slice(), b"node:fs".as_slice()),
338 (b"path".as_slice(), b"promises".as_slice()),
339 ]
340 );
341 }
342
343 #[test]
344 fn ordinary_package_specifier_remains_external() {
345 let package = module_specifier_builder(b"app", "react").build();
346 let first = package.as_view().segments().next().unwrap();
347
348 assert_eq!(first.kind, b"external_pkg");
349 assert_eq!(first.name, b"react");
350 }
351
352 #[test]
353 fn parse_invalid_json_returns_parse_error() {
354 assert!(matches!(
355 parse("{not json"),
356 Err(PackageJsonError::Parse(_))
357 ));
358 }
359
360 #[test]
361 fn parse_non_object_top_level_is_schema_error() {
362 assert!(matches!(parse("[1,2,3]"), Err(PackageJsonError::Schema(_))));
363 }
364
365 #[test]
366 fn parse_missing_name_omits_package_row() {
367 let json = r#"{
368 "private": true,
369 "dependencies": { "react": "^18.0.0" }
370 }"#;
371 let deps = parse(json).unwrap();
372 assert!(deps.iter().all(|d| d.dep_kind != "package"));
373 assert!(deps.iter().any(|d| d.name == "react"));
374 }
375}