1use std::path::Path;
4use std::path::PathBuf;
5
6use deno_semver::npm::NpmVersionReqParseError;
7use deno_semver::package::PackageReq;
8use deno_semver::VersionReq;
9use indexmap::IndexMap;
10use serde::Serialize;
11use serde_json::Map;
12use serde_json::Value;
13use thiserror::Error;
14use url::Url;
15
16#[allow(clippy::disallowed_types)]
17pub type PackageJsonRc = crate::sync::MaybeArc<PackageJson>;
18
19pub trait PackageJsonCache {
20 fn get(&self, path: &Path) -> Option<PackageJsonRc>;
21 fn set(&self, path: PathBuf, package_json: PackageJsonRc);
22}
23
24#[derive(Debug, Error, Clone)]
25pub enum PackageJsonDepValueParseError {
26 #[error(transparent)]
27 VersionReq(#[from] NpmVersionReqParseError),
28 #[error("Not implemented scheme '{scheme}'")]
29 Unsupported { scheme: String },
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum PackageJsonDepValue {
34 Req(PackageReq),
35 Workspace(VersionReq),
36}
37
38pub type PackageJsonDeps =
39 IndexMap<String, Result<PackageJsonDepValue, PackageJsonDepValueParseError>>;
40
41#[derive(Debug, Error)]
42pub enum PackageJsonLoadError {
43 #[error("Failed reading '{}'.", .path.display())]
44 Io {
45 path: PathBuf,
46 #[source]
47 source: std::io::Error,
48 },
49 #[error("Malformed package.json '{}'.", .path.display())]
50 Deserialize {
51 path: PathBuf,
52 #[source]
53 source: serde_json::Error,
54 },
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum NodeModuleKind {
59 Esm,
60 Cjs,
61}
62
63#[derive(Clone, Debug, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct PackageJson {
66 pub exports: Option<Map<String, Value>>,
67 pub imports: Option<Map<String, Value>>,
68 pub bin: Option<Value>,
69 main: Option<String>, module: Option<String>, pub name: Option<String>,
72 pub version: Option<String>,
73 #[serde(skip)]
74 pub path: PathBuf,
75 #[serde(rename = "type")]
76 pub typ: String,
77 pub types: Option<String>,
78 pub dependencies: Option<IndexMap<String, String>>,
79 pub dev_dependencies: Option<IndexMap<String, String>>,
80 pub scripts: Option<IndexMap<String, String>>,
81 pub workspaces: Option<Vec<String>>,
82}
83
84impl PackageJson {
85 pub fn load_from_path(
86 path: &Path,
87 fs: &dyn crate::fs::DenoConfigFs,
88 maybe_cache: Option<&dyn PackageJsonCache>,
89 ) -> Result<PackageJsonRc, PackageJsonLoadError> {
90 if let Some(item) = maybe_cache.and_then(|c| c.get(path)) {
91 Ok(item)
92 } else {
93 match fs.read_to_string_lossy(path) {
94 Ok(file_text) => {
95 let pkg_json =
96 PackageJson::load_from_string(path.to_path_buf(), file_text)?;
97 let pkg_json = crate::sync::new_rc(pkg_json);
98 if let Some(cache) = maybe_cache {
99 cache.set(path.to_path_buf(), pkg_json.clone());
100 }
101 Ok(pkg_json)
102 }
103 Err(err) => Err(PackageJsonLoadError::Io {
104 path: path.to_path_buf(),
105 source: err,
106 }),
107 }
108 }
109 }
110
111 pub fn load_from_string(
112 path: PathBuf,
113 source: String,
114 ) -> Result<PackageJson, PackageJsonLoadError> {
115 if source.trim().is_empty() {
116 return Ok(PackageJson {
117 path,
118 main: None,
119 name: None,
120 version: None,
121 module: None,
122 typ: "none".to_string(),
123 types: None,
124 exports: None,
125 imports: None,
126 bin: None,
127 dependencies: None,
128 dev_dependencies: None,
129 scripts: None,
130 workspaces: None,
131 });
132 }
133
134 let package_json: Value = serde_json::from_str(&source).map_err(|err| {
135 PackageJsonLoadError::Deserialize {
136 path: path.clone(),
137 source: err,
138 }
139 })?;
140 Ok(Self::load_from_value(path, package_json))
141 }
142
143 pub fn load_from_value(
144 path: PathBuf,
145 package_json: serde_json::Value,
146 ) -> PackageJson {
147 fn parse_string_map(
148 value: serde_json::Value,
149 ) -> Option<IndexMap<String, String>> {
150 if let Value::Object(map) = value {
151 let mut result = IndexMap::with_capacity(map.len());
152 for (k, v) in map {
153 if let Some(v) = map_string(v) {
154 result.insert(k, v);
155 }
156 }
157 Some(result)
158 } else {
159 None
160 }
161 }
162
163 fn map_object(value: serde_json::Value) -> Option<Map<String, Value>> {
164 match value {
165 Value::Object(v) => Some(v),
166 _ => None,
167 }
168 }
169
170 fn map_string(value: serde_json::Value) -> Option<String> {
171 match value {
172 Value::String(v) => Some(v),
173 Value::Number(v) => Some(v.to_string()),
174 _ => None,
175 }
176 }
177
178 fn map_array(value: serde_json::Value) -> Option<Vec<Value>> {
179 match value {
180 Value::Array(v) => Some(v),
181 _ => None,
182 }
183 }
184
185 fn parse_string_array(value: serde_json::Value) -> Option<Vec<String>> {
186 let value = map_array(value)?;
187 let mut result = Vec::with_capacity(value.len());
188 for v in value {
189 if let Some(v) = map_string(v) {
190 result.push(v);
191 }
192 }
193 Some(result)
194 }
195
196 let mut package_json = match package_json {
197 Value::Object(o) => o,
198 _ => Default::default(),
199 };
200 let imports_val = package_json.remove("imports");
201 let main_val = package_json.remove("main");
202 let module_val = package_json.remove("module");
203 let name_val = package_json.remove("name");
204 let version_val = package_json.remove("version");
205 let type_val = package_json.remove("type");
206 let bin = package_json.remove("bin");
207 let exports = package_json.remove("exports").and_then(|exports| {
208 Some(if is_conditional_exports_main_sugar(&exports) {
209 let mut map = Map::new();
210 map.insert(".".to_string(), exports.to_owned());
211 map
212 } else {
213 exports.as_object()?.to_owned()
214 })
215 });
216
217 let imports = imports_val.and_then(map_object);
218 let main = main_val.and_then(map_string);
219 let name = name_val.and_then(map_string);
220 let version = version_val.and_then(map_string);
221 let module = module_val.and_then(map_string);
222
223 let dependencies = package_json
224 .remove("dependencies")
225 .and_then(parse_string_map);
226 let dev_dependencies = package_json
227 .remove("devDependencies")
228 .and_then(parse_string_map);
229
230 let scripts: Option<IndexMap<String, String>> =
231 package_json.remove("scripts").and_then(parse_string_map);
232
233 let typ = if let Some(t) = type_val {
235 if let Some(t) = t.as_str() {
236 if t != "module" && t != "commonjs" {
237 "none".to_string()
238 } else {
239 t.to_string()
240 }
241 } else {
242 "none".to_string()
243 }
244 } else {
245 "none".to_string()
246 };
247
248 let types = package_json
250 .remove("typings")
251 .or_else(|| package_json.remove("types"))
252 .and_then(map_string);
253 let workspaces = package_json
254 .remove("workspaces")
255 .and_then(parse_string_array);
256
257 PackageJson {
258 path,
259 main,
260 name,
261 version,
262 module,
263 typ,
264 types,
265 exports,
266 imports,
267 bin,
268 dependencies,
269 dev_dependencies,
270 scripts,
271 workspaces,
272 }
273 }
274
275 pub fn specifier(&self) -> Url {
276 Url::from_file_path(&self.path).unwrap()
277 }
278
279 pub fn dir_path(&self) -> &Path {
280 self.path.parent().unwrap()
281 }
282
283 pub fn main(&self, referrer_kind: NodeModuleKind) -> Option<&str> {
284 let main = if referrer_kind == NodeModuleKind::Esm && self.typ == "module" {
285 self.module.as_ref().or(self.main.as_ref())
286 } else {
287 self.main.as_ref()
288 };
289 main.map(|m| m.trim()).filter(|m| !m.is_empty())
290 }
291
292 pub fn resolve_local_package_json_deps(&self) -> PackageJsonDeps {
294 fn parse_dep_entry_name_and_raw_version<'a>(
297 key: &'a str,
298 value: &'a str,
299 ) -> (&'a str, &'a str) {
300 if let Some(package_and_version) = value.strip_prefix("npm:") {
301 if let Some((name, version)) = package_and_version.rsplit_once('@') {
302 if name.is_empty() {
304 (package_and_version, "*")
305 } else {
306 (name, version)
307 }
308 } else {
309 (package_and_version, "*")
310 }
311 } else {
312 (key, value)
313 }
314 }
315
316 fn parse_entry(
317 key: &str,
318 value: &str,
319 ) -> Result<PackageJsonDepValue, PackageJsonDepValueParseError> {
320 if let Some(workspace_key) = value.strip_prefix("workspace:") {
321 let version_req = VersionReq::parse_from_npm(workspace_key)?;
322 return Ok(PackageJsonDepValue::Workspace(version_req));
323 }
324 if value.starts_with("file:")
325 || value.starts_with("git:")
326 || value.starts_with("http:")
327 || value.starts_with("https:")
328 {
329 return Err(PackageJsonDepValueParseError::Unsupported {
330 scheme: value.split(':').next().unwrap().to_string(),
331 });
332 }
333 let (name, version_req) =
334 parse_dep_entry_name_and_raw_version(key, value);
335 let result = VersionReq::parse_from_npm(version_req);
336 match result {
337 Ok(version_req) => Ok(PackageJsonDepValue::Req(PackageReq {
338 name: name.to_string(),
339 version_req,
340 })),
341 Err(err) => Err(PackageJsonDepValueParseError::VersionReq(err)),
342 }
343 }
344
345 fn insert_deps(
346 deps: Option<&IndexMap<String, String>>,
347 result: &mut PackageJsonDeps,
348 ) {
349 if let Some(deps) = deps {
350 for (key, value) in deps {
351 result
352 .entry(key.to_string())
353 .or_insert_with(|| parse_entry(key, value));
354 }
355 }
356 }
357
358 let deps = self.dependencies.as_ref();
359 let dev_deps = self.dev_dependencies.as_ref();
360 let mut result = IndexMap::new();
361
362 insert_deps(deps, &mut result);
364 insert_deps(dev_deps, &mut result);
365
366 result
367 }
368}
369
370fn is_conditional_exports_main_sugar(exports: &Value) -> bool {
371 if exports.is_string() || exports.is_array() {
372 return true;
373 }
374
375 if exports.is_null() || !exports.is_object() {
376 return false;
377 }
378
379 let exports_obj = exports.as_object().unwrap();
380 let mut is_conditional_sugar = false;
381 let mut i = 0;
382 for key in exports_obj.keys() {
383 let cur_is_conditional_sugar = key.is_empty() || !key.starts_with('.');
384 if i == 0 {
385 is_conditional_sugar = cur_is_conditional_sugar;
386 i += 1;
387 } else if is_conditional_sugar != cur_is_conditional_sugar {
388 panic!("\"exports\" cannot contains some keys starting with \'.\' and some not.
389 The exports object must either be an object of package subpath keys
390 or an object of main entry condition name keys only.")
391 }
392 }
393
394 is_conditional_sugar
395}
396
397#[cfg(test)]
398mod test {
399 use super::*;
400 use pretty_assertions::assert_eq;
401 use std::path::PathBuf;
402
403 #[test]
404 fn null_exports_should_not_crash() {
405 let package_json = PackageJson::load_from_string(
406 PathBuf::from("/package.json"),
407 r#"{ "exports": null }"#.to_string(),
408 )
409 .unwrap();
410
411 assert!(package_json.exports.is_none());
412 }
413
414 fn get_local_package_json_version_reqs_for_tests(
415 package_json: &PackageJson,
416 ) -> IndexMap<String, Result<PackageJsonDepValue, String>> {
417 package_json
418 .resolve_local_package_json_deps()
419 .into_iter()
420 .map(|(k, v)| {
421 (
422 k,
423 match v {
424 Ok(v) => Ok(v),
425 Err(err) => Err(err.to_string()),
426 },
427 )
428 })
429 .collect::<IndexMap<_, _>>()
430 }
431
432 #[test]
433 fn test_get_local_package_json_version_reqs() {
434 let mut package_json = PackageJson::load_from_string(
435 PathBuf::from("/package.json"),
436 "{}".to_string(),
437 )
438 .unwrap();
439 package_json.dependencies = Some(IndexMap::from([
440 ("test".to_string(), "^1.2".to_string()),
441 ("other".to_string(), "npm:package@~1.3".to_string()),
442 ]));
443 package_json.dev_dependencies = Some(IndexMap::from([
444 ("package_b".to_string(), "~2.2".to_string()),
445 ("other".to_string(), "^3.2".to_string()),
447 ]));
448 let deps = get_local_package_json_version_reqs_for_tests(&package_json);
449 assert_eq!(
450 deps,
451 IndexMap::from([
452 (
453 "test".to_string(),
454 Ok(PackageJsonDepValue::Req(
455 PackageReq::from_str("test@^1.2").unwrap()
456 ))
457 ),
458 (
459 "other".to_string(),
460 Ok(PackageJsonDepValue::Req(
461 PackageReq::from_str("package@~1.3").unwrap()
462 ))
463 ),
464 (
465 "package_b".to_string(),
466 Ok(PackageJsonDepValue::Req(
467 PackageReq::from_str("package_b@~2.2").unwrap()
468 ))
469 )
470 ])
471 );
472 }
473
474 #[test]
475 fn test_get_local_package_json_version_reqs_errors_non_npm_specifier() {
476 let mut package_json = PackageJson::load_from_string(
477 PathBuf::from("/package.json"),
478 "{}".to_string(),
479 )
480 .unwrap();
481 package_json.dependencies = Some(IndexMap::from([(
482 "test".to_string(),
483 "%*(#$%()".to_string(),
484 )]));
485 let map = get_local_package_json_version_reqs_for_tests(&package_json);
486 assert_eq!(
487 map,
488 IndexMap::from([(
489 "test".to_string(),
490 Err(
491 concat!(
492 "Invalid npm version requirement. Unexpected character.\n",
493 " %*(#$%()\n",
494 " ~"
495 )
496 .to_string()
497 )
498 )])
499 );
500 }
501
502 #[test]
503 fn test_get_local_package_json_version_reqs_range() {
504 let mut package_json = PackageJson::load_from_string(
505 PathBuf::from("/package.json"),
506 "{}".to_string(),
507 )
508 .unwrap();
509 package_json.dependencies = Some(IndexMap::from([(
510 "test".to_string(),
511 "1.x - 1.3".to_string(),
512 )]));
513 let map = get_local_package_json_version_reqs_for_tests(&package_json);
514 assert_eq!(
515 map,
516 IndexMap::from([(
517 "test".to_string(),
518 Ok(PackageJsonDepValue::Req(PackageReq {
519 name: "test".to_string(),
520 version_req: VersionReq::parse_from_npm("1.x - 1.3").unwrap()
521 }))
522 )])
523 );
524 }
525
526 #[test]
527 fn test_get_local_package_json_version_reqs_skips_certain_specifiers() {
528 let mut package_json = PackageJson::load_from_string(
529 PathBuf::from("/package.json"),
530 "{}".to_string(),
531 )
532 .unwrap();
533 package_json.dependencies = Some(IndexMap::from([
534 ("test".to_string(), "1".to_string()),
535 ("work-test".to_string(), "workspace:1.1.1".to_string()),
536 ("file-test".to_string(), "file:something".to_string()),
537 ("git-test".to_string(), "git:something".to_string()),
538 ("http-test".to_string(), "http://something".to_string()),
539 ("https-test".to_string(), "https://something".to_string()),
540 ]));
541 let result = get_local_package_json_version_reqs_for_tests(&package_json);
542 assert_eq!(
543 result,
544 IndexMap::from([
545 (
546 "file-test".to_string(),
547 Err("Not implemented scheme 'file'".to_string()),
548 ),
549 (
550 "git-test".to_string(),
551 Err("Not implemented scheme 'git'".to_string()),
552 ),
553 (
554 "http-test".to_string(),
555 Err("Not implemented scheme 'http'".to_string()),
556 ),
557 (
558 "https-test".to_string(),
559 Err("Not implemented scheme 'https'".to_string()),
560 ),
561 (
562 "test".to_string(),
563 Ok(PackageJsonDepValue::Req(
564 PackageReq::from_str("test@1").unwrap()
565 ))
566 ),
567 (
568 "work-test".to_string(),
569 Ok(PackageJsonDepValue::Workspace(
570 VersionReq::parse_from_npm("1.1.1").unwrap()
571 ))
572 )
573 ])
574 );
575 }
576
577 #[test]
578 fn test_deserialize_serialize() {
579 let json_value = serde_json::json!({
580 "name": "test",
581 "version": "1",
582 "exports": {
583 ".": "./main.js",
584 },
585 "bin": "./main.js",
586 "types": "./types.d.ts",
587 "imports": {
588 "#test": "./main.js",
589 },
590 "main": "./main.js",
591 "module": "./module.js",
592 "type": "module",
593 "dependencies": {
594 "name": "1.2",
595 },
596 "devDependencies": {
597 "name": "1.2",
598 },
599 "scripts": {
600 "test": "echo \"Error: no test specified\" && exit 1",
601 },
602 "workspaces": ["asdf", "asdf2"]
603 });
604 let package_json = PackageJson::load_from_value(
605 PathBuf::from("/package.json"),
606 json_value.clone(),
607 );
608 let serialized_value = serde_json::to_value(&package_json).unwrap();
609 assert_eq!(serialized_value, json_value);
610 }
611}