1#![deny(clippy::print_stderr)]
4#![deny(clippy::print_stdout)]
5#![deny(clippy::unused_async)]
6#![deny(clippy::unnecessary_wraps)]
7
8use std::path::Path;
9use std::path::PathBuf;
10
11use boxed_error::Boxed;
12use deno_error::JsError;
13use deno_semver::npm::NpmVersionReqParseError;
14use deno_semver::package::PackageReq;
15use deno_semver::StackString;
16use deno_semver::VersionReq;
17use indexmap::IndexMap;
18use serde::Serialize;
19use serde_json::Map;
20use serde_json::Value;
21use sys_traits::FsRead;
22use thiserror::Error;
23use url::Url;
24
25mod sync;
26
27#[allow(clippy::disallowed_types)]
28pub type PackageJsonRc = crate::sync::MaybeArc<PackageJson>;
29#[allow(clippy::disallowed_types)]
30pub type PackageJsonDepsRc = crate::sync::MaybeArc<PackageJsonDeps>;
31#[allow(clippy::disallowed_types)]
32type PackageJsonDepsRcCell = crate::sync::MaybeOnceLock<PackageJsonDepsRc>;
33
34pub trait PackageJsonCache {
35 fn get(&self, path: &Path) -> Option<PackageJsonRc>;
36 fn set(&self, path: PathBuf, package_json: PackageJsonRc);
37}
38
39#[derive(Debug, Clone, JsError, PartialEq, Eq, Boxed)]
40pub struct PackageJsonDepValueParseError(
41 pub Box<PackageJsonDepValueParseErrorKind>,
42);
43
44#[derive(Debug, Error, Clone, JsError, PartialEq, Eq)]
45pub enum PackageJsonDepValueParseErrorKind {
46 #[class(inherit)]
47 #[error(transparent)]
48 VersionReq(#[from] NpmVersionReqParseError),
49 #[class(type)]
50 #[error("Not implemented scheme '{scheme}'")]
51 Unsupported { scheme: String },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum PackageJsonDepWorkspaceReq {
56 Tilde,
58
59 Caret,
61
62 VersionReq(VersionReq),
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub enum PackageJsonDepValue {
68 File(String),
69 Req(PackageReq),
70 Workspace(PackageJsonDepWorkspaceReq),
71}
72
73impl PackageJsonDepValue {
74 pub fn parse(
75 key: &str,
76 value: &str,
77 ) -> Result<Self, PackageJsonDepValueParseError> {
78 fn parse_dep_entry_name_and_raw_version<'a>(
81 key: &'a str,
82 value: &'a str,
83 ) -> (&'a str, &'a str) {
84 if let Some(package_and_version) = value.strip_prefix("npm:") {
85 if let Some((name, version)) = package_and_version.rsplit_once('@') {
86 if name.is_empty() {
88 (package_and_version, "*")
89 } else {
90 (name, version)
91 }
92 } else {
93 (package_and_version, "*")
94 }
95 } else {
96 (key, value)
97 }
98 }
99
100 if let Some(workspace_key) = value.strip_prefix("workspace:") {
101 let workspace_req = match workspace_key {
102 "~" => PackageJsonDepWorkspaceReq::Tilde,
103 "^" => PackageJsonDepWorkspaceReq::Caret,
104 _ => PackageJsonDepWorkspaceReq::VersionReq(
105 VersionReq::parse_from_npm(workspace_key)?,
106 ),
107 };
108 return Ok(Self::Workspace(workspace_req));
109 }
110 if value.starts_with("git:")
111 || value.starts_with("http:")
112 || value.starts_with("https:")
113 {
114 return Err(
115 PackageJsonDepValueParseErrorKind::Unsupported {
116 scheme: value.split(':').next().unwrap().to_string(),
117 }
118 .into_box(),
119 );
120 }
121 if let Some(path) = value.strip_prefix("file:") {
122 return Ok(Self::File(path.to_string()));
123 }
124 let (name, version_req) = parse_dep_entry_name_and_raw_version(key, value);
125 let result = VersionReq::parse_from_npm(version_req);
126 match result {
127 Ok(version_req) => Ok(Self::Req(PackageReq {
128 name: name.into(),
129 version_req,
130 })),
131 Err(err) => {
132 Err(PackageJsonDepValueParseErrorKind::VersionReq(err).into_box())
133 }
134 }
135 }
136}
137
138pub type PackageJsonDepsMap = IndexMap<
139 StackString,
140 Result<PackageJsonDepValue, PackageJsonDepValueParseError>,
141>;
142
143#[derive(Debug, Clone)]
144pub struct PackageJsonDeps {
145 pub dependencies: PackageJsonDepsMap,
146 pub dev_dependencies: PackageJsonDepsMap,
147}
148
149impl PackageJsonDeps {
150 pub fn get(
152 &self,
153 alias: &str,
154 ) -> Option<&Result<PackageJsonDepValue, PackageJsonDepValueParseError>> {
155 self
156 .dependencies
157 .get(alias)
158 .or_else(|| self.dev_dependencies.get(alias))
159 }
160}
161
162#[derive(Debug, Error, JsError)]
163pub enum PackageJsonLoadError {
164 #[class(inherit)]
165 #[error("Failed reading '{}'.", .path.display())]
166 Io {
167 path: PathBuf,
168 #[source]
169 #[inherit]
170 source: std::io::Error,
171 },
172 #[class(inherit)]
173 #[error("Malformed package.json '{}'.", .path.display())]
174 Deserialize {
175 path: PathBuf,
176 #[source]
177 #[inherit]
178 source: serde_json::Error,
179 },
180 #[error("\"exports\" cannot contains some keys starting with '.' and some not.\nThe exports object must either be an object of package subpath keys\nor an object of main entry condition name keys only.")]
181 #[class(type)]
182 InvalidExports,
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub enum NodeModuleKind {
187 Esm,
188 Cjs,
189}
190
191#[derive(Clone, Debug, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct PackageJson {
194 pub exports: Option<Map<String, Value>>,
195 pub imports: Option<Map<String, Value>>,
196 pub bin: Option<Value>,
197 main: Option<String>, module: Option<String>, pub name: Option<String>,
200 pub version: Option<String>,
201 #[serde(skip)]
202 pub path: PathBuf,
203 #[serde(rename = "type")]
204 pub typ: String,
205 pub types: Option<String>,
206 pub types_versions: Option<Map<String, Value>>,
207 pub dependencies: Option<IndexMap<String, String>>,
208 pub dev_dependencies: Option<IndexMap<String, String>>,
209 pub peer_dependencies: Option<IndexMap<String, String>>,
210 pub peer_dependencies_meta: Option<Value>,
211 pub optional_dependencies: Option<IndexMap<String, String>>,
212 pub scripts: Option<IndexMap<String, String>>,
213 pub workspaces: Option<Vec<String>>,
214 pub os: Option<Vec<String>>,
215 pub cpu: Option<Vec<String>>,
216 #[serde(skip_serializing)]
217 resolved_deps: PackageJsonDepsRcCell,
218}
219
220impl PackageJson {
221 pub fn load_from_path(
222 sys: &impl FsRead,
223 maybe_cache: Option<&dyn PackageJsonCache>,
224 path: &Path,
225 ) -> Result<PackageJsonRc, PackageJsonLoadError> {
226 if let Some(item) = maybe_cache.and_then(|c| c.get(path)) {
227 Ok(item)
228 } else {
229 match sys.fs_read_to_string_lossy(path) {
230 Ok(file_text) => {
231 let pkg_json =
232 PackageJson::load_from_string(path.to_path_buf(), &file_text)?;
233 let pkg_json = crate::sync::new_rc(pkg_json);
234 if let Some(cache) = maybe_cache {
235 cache.set(path.to_path_buf(), pkg_json.clone());
236 }
237 Ok(pkg_json)
238 }
239 Err(err) => Err(PackageJsonLoadError::Io {
240 path: path.to_path_buf(),
241 source: err,
242 }),
243 }
244 }
245 }
246
247 pub fn load_from_string(
248 path: PathBuf,
249 source: &str,
250 ) -> Result<PackageJson, PackageJsonLoadError> {
251 if source.trim().is_empty() {
252 return Ok(PackageJson {
253 path,
254 main: None,
255 name: None,
256 version: None,
257 module: None,
258 typ: "none".to_string(),
259 types: None,
260 types_versions: None,
261 exports: None,
262 imports: None,
263 bin: None,
264 dependencies: None,
265 dev_dependencies: None,
266 peer_dependencies: None,
267 peer_dependencies_meta: None,
268 optional_dependencies: None,
269 scripts: None,
270 workspaces: None,
271 os: None,
272 cpu: None,
273 resolved_deps: Default::default(),
274 });
275 }
276
277 let package_json: Value = serde_json::from_str(source).map_err(|err| {
278 PackageJsonLoadError::Deserialize {
279 path: path.clone(),
280 source: err,
281 }
282 })?;
283 Self::load_from_value(path, package_json)
284 }
285
286 pub fn load_from_value(
287 path: PathBuf,
288 package_json: serde_json::Value,
289 ) -> Result<PackageJson, PackageJsonLoadError> {
290 fn parse_string_map(
291 value: serde_json::Value,
292 ) -> Option<IndexMap<String, String>> {
293 if let Value::Object(map) = value {
294 let mut result = IndexMap::with_capacity(map.len());
295 for (k, v) in map {
296 if let Some(v) = map_string(v) {
297 result.insert(k, v);
298 }
299 }
300 Some(result)
301 } else {
302 None
303 }
304 }
305
306 fn map_object(value: serde_json::Value) -> Option<Map<String, Value>> {
307 match value {
308 Value::Object(v) => Some(v),
309 _ => None,
310 }
311 }
312
313 fn map_string(value: serde_json::Value) -> Option<String> {
314 match value {
315 Value::String(v) => Some(v),
316 Value::Number(v) => Some(v.to_string()),
317 _ => None,
318 }
319 }
320
321 fn map_array(value: serde_json::Value) -> Option<Vec<Value>> {
322 match value {
323 Value::Array(v) => Some(v),
324 _ => None,
325 }
326 }
327
328 fn parse_string_array(value: serde_json::Value) -> Option<Vec<String>> {
329 let value = map_array(value)?;
330 let mut result = Vec::with_capacity(value.len());
331 for v in value {
332 if let Some(v) = map_string(v) {
333 result.push(v);
334 }
335 }
336 Some(result)
337 }
338
339 let mut package_json = match package_json {
340 Value::Object(o) => o,
341 _ => Default::default(),
342 };
343 let imports_val = package_json.remove("imports");
344 let main_val = package_json.remove("main");
345 let module_val = package_json.remove("module");
346 let name_val = package_json.remove("name");
347 let version_val = package_json.remove("version");
348 let type_val = package_json.remove("type");
349 let bin = package_json.remove("bin");
350 let exports = package_json
351 .remove("exports")
352 .map(|exports| {
353 if is_conditional_exports_main_sugar(&exports)? {
354 let mut map = Map::new();
355 map.insert(".".to_string(), exports.to_owned());
356 Ok::<_, PackageJsonLoadError>(Some(map))
357 } else {
358 Ok(exports.as_object().map(|o| o.to_owned()))
359 }
360 })
361 .transpose()?
362 .flatten();
363
364 let imports = imports_val.and_then(map_object);
365 let main = main_val.and_then(map_string);
366 let name = name_val.and_then(map_string);
367 let version = version_val.and_then(map_string);
368 let module = module_val.and_then(map_string);
369
370 let dependencies = package_json
371 .remove("dependencies")
372 .and_then(parse_string_map);
373 let dev_dependencies = package_json
374 .remove("devDependencies")
375 .and_then(parse_string_map);
376 let peer_dependencies = package_json
377 .remove("peerDependencies")
378 .and_then(parse_string_map);
379 let peer_dependencies_meta = package_json.remove("peerDependenciesMeta");
380 let optional_dependencies = package_json
381 .remove("optionalDependencies")
382 .and_then(parse_string_map);
383
384 let scripts: Option<IndexMap<String, String>> =
385 package_json.remove("scripts").and_then(parse_string_map);
386
387 let typ = if let Some(t) = type_val {
389 if let Some(t) = t.as_str() {
390 if t != "module" && t != "commonjs" {
391 "none".to_string()
392 } else {
393 t.to_string()
394 }
395 } else {
396 "none".to_string()
397 }
398 } else {
399 "none".to_string()
400 };
401
402 let types = package_json
404 .remove("typings")
405 .or_else(|| package_json.remove("types"))
406 .and_then(map_string);
407 let types_versions = package_json
408 .remove("typesVersions")
409 .and_then(|exports| exports.as_object().map(|o| o.to_owned()));
410 let workspaces = package_json
411 .remove("workspaces")
412 .and_then(parse_string_array);
413 let os = package_json.remove("os").and_then(parse_string_array);
414 let cpu = package_json.remove("cpu").and_then(parse_string_array);
415
416 Ok(PackageJson {
417 path,
418 main,
419 name,
420 version,
421 module,
422 typ,
423 types,
424 types_versions,
425 exports,
426 imports,
427 bin,
428 dependencies,
429 dev_dependencies,
430 peer_dependencies,
431 peer_dependencies_meta,
432 optional_dependencies,
433 scripts,
434 workspaces,
435 os,
436 cpu,
437 resolved_deps: Default::default(),
438 })
439 }
440
441 pub fn specifier(&self) -> Url {
442 deno_path_util::url_from_file_path(&self.path).unwrap()
443 }
444
445 pub fn dir_path(&self) -> &Path {
446 self.path.parent().unwrap()
447 }
448
449 pub fn main(&self, referrer_kind: NodeModuleKind) -> Option<&str> {
450 let main = if referrer_kind == NodeModuleKind::Esm && self.typ == "module" {
451 self.module.as_ref().or(self.main.as_ref())
452 } else {
453 self.main.as_ref()
454 };
455 main.map(|m| m.trim()).filter(|m| !m.is_empty())
456 }
457
458 pub fn resolve_local_package_json_deps(&self) -> &PackageJsonDepsRc {
460 fn get_map(deps: Option<&IndexMap<String, String>>) -> PackageJsonDepsMap {
461 let Some(deps) = deps else {
462 return Default::default();
463 };
464 let mut result = IndexMap::with_capacity(deps.len());
465 for (key, value) in deps {
466 result
467 .entry(StackString::from(key.as_str()))
468 .or_insert_with(|| PackageJsonDepValue::parse(key, value));
469 }
470 result
471 }
472
473 self.resolved_deps.get_or_init(|| {
474 PackageJsonDepsRc::new(PackageJsonDeps {
475 dependencies: get_map(self.dependencies.as_ref()),
476 dev_dependencies: get_map(self.dev_dependencies.as_ref()),
477 })
478 })
479 }
480}
481
482fn is_conditional_exports_main_sugar(
483 exports: &Value,
484) -> Result<bool, PackageJsonLoadError> {
485 if exports.is_string() || exports.is_array() {
486 return Ok(true);
487 }
488
489 if exports.is_null() || !exports.is_object() {
490 return Ok(false);
491 }
492
493 let exports_obj = exports.as_object().unwrap();
494 let mut is_conditional_sugar = false;
495 let mut i = 0;
496 for key in exports_obj.keys() {
497 let cur_is_conditional_sugar = key.is_empty() || !key.starts_with('.');
498 if i == 0 {
499 is_conditional_sugar = cur_is_conditional_sugar;
500 i += 1;
501 } else if is_conditional_sugar != cur_is_conditional_sugar {
502 return Err(PackageJsonLoadError::InvalidExports);
503 }
504 }
505
506 Ok(is_conditional_sugar)
507}
508
509#[cfg(test)]
510mod test {
511 use super::*;
512 use pretty_assertions::assert_eq;
513 use std::error::Error;
514 use std::path::PathBuf;
515
516 #[test]
517 fn null_exports_should_not_crash() {
518 let package_json = PackageJson::load_from_string(
519 PathBuf::from("/package.json"),
520 r#"{ "exports": null }"#,
521 )
522 .unwrap();
523
524 assert!(package_json.exports.is_none());
525 }
526
527 fn get_local_package_json_version_reqs_for_tests(
528 package_json: &PackageJson,
529 ) -> IndexMap<
530 String,
531 Result<PackageJsonDepValue, PackageJsonDepValueParseErrorKind>,
532 > {
533 let deps = package_json.resolve_local_package_json_deps();
534 deps
535 .dependencies
536 .clone()
537 .into_iter()
538 .chain(deps.dev_dependencies.clone())
539 .map(|(k, v)| {
540 (
541 k.to_string(),
542 match v {
543 Ok(v) => Ok(v),
544 Err(err) => Err(err.into_kind()),
545 },
546 )
547 })
548 .collect::<IndexMap<_, _>>()
549 }
550
551 #[test]
552 fn test_get_local_package_json_version_reqs() {
553 let mut package_json =
554 PackageJson::load_from_string(PathBuf::from("/package.json"), "{}")
555 .unwrap();
556 package_json.dependencies = Some(IndexMap::from([
557 ("test".to_string(), "^1.2".to_string()),
558 ("other".to_string(), "npm:package@~1.3".to_string()),
559 ]));
560 package_json.dev_dependencies = Some(IndexMap::from([
561 ("package_b".to_string(), "~2.2".to_string()),
562 ("other".to_string(), "^3.2".to_string()),
563 ]));
564 let deps = package_json.resolve_local_package_json_deps();
565 assert_eq!(
566 deps
567 .dependencies
568 .clone()
569 .into_iter()
570 .map(|d| (d.0, d.1.unwrap()))
571 .collect::<Vec<_>>(),
572 Vec::from([
573 (
574 "test".into(),
575 PackageJsonDepValue::Req(PackageReq::from_str("test@^1.2").unwrap())
576 ),
577 (
578 "other".into(),
579 PackageJsonDepValue::Req(
580 PackageReq::from_str("package@~1.3").unwrap()
581 )
582 ),
583 ])
584 );
585 assert_eq!(
586 deps
587 .dev_dependencies
588 .clone()
589 .into_iter()
590 .map(|d| (d.0, d.1.unwrap()))
591 .collect::<Vec<_>>(),
592 Vec::from([
593 (
594 "package_b".into(),
595 PackageJsonDepValue::Req(
596 PackageReq::from_str("package_b@~2.2").unwrap()
597 )
598 ),
599 (
600 "other".into(),
601 PackageJsonDepValue::Req(PackageReq::from_str("other@^3.2").unwrap())
602 ),
603 ])
604 );
605 }
606
607 #[test]
608 fn test_get_local_package_json_version_reqs_errors_non_npm_specifier() {
609 let mut package_json =
610 PackageJson::load_from_string(PathBuf::from("/package.json"), "{}")
611 .unwrap();
612 package_json.dependencies = Some(IndexMap::from([(
613 "test".to_string(),
614 "%*(#$%()".to_string(),
615 )]));
616 let map = get_local_package_json_version_reqs_for_tests(&package_json);
617 assert_eq!(map.len(), 1);
618 let err = map.get("test").unwrap().as_ref().unwrap_err();
619 assert_eq!(format!("{}", err), "Invalid version requirement");
620 assert_eq!(
621 format!("{}", err.source().unwrap()),
622 concat!("Unexpected character.\n", " %*(#$%()\n", " ~")
623 );
624 }
625
626 #[test]
627 fn test_get_local_package_json_version_reqs_range() {
628 let mut package_json =
629 PackageJson::load_from_string(PathBuf::from("/package.json"), "{}")
630 .unwrap();
631 package_json.dependencies = Some(IndexMap::from([(
632 "test".to_string(),
633 "1.x - 1.3".to_string(),
634 )]));
635 let map = get_local_package_json_version_reqs_for_tests(&package_json);
636 assert_eq!(
637 map,
638 IndexMap::from([(
639 "test".to_string(),
640 Ok(PackageJsonDepValue::Req(PackageReq {
641 name: "test".into(),
642 version_req: VersionReq::parse_from_npm("1.x - 1.3").unwrap()
643 }))
644 )])
645 );
646 }
647
648 #[test]
649 fn test_get_local_package_json_version_reqs_skips_certain_specifiers() {
650 let mut package_json =
651 PackageJson::load_from_string(PathBuf::from("/package.json"), "{}")
652 .unwrap();
653 package_json.dependencies = Some(IndexMap::from([
654 ("test".to_string(), "1".to_string()),
655 (
656 "work-test-version-req".to_string(),
657 "workspace:1.1.1".to_string(),
658 ),
659 ("work-test-star".to_string(), "workspace:*".to_string()),
660 ("work-test-tilde".to_string(), "workspace:~".to_string()),
661 ("work-test-caret".to_string(), "workspace:^".to_string()),
662 ("file-test".to_string(), "file:something".to_string()),
663 ("git-test".to_string(), "git:something".to_string()),
664 ("http-test".to_string(), "http://something".to_string()),
665 ("https-test".to_string(), "https://something".to_string()),
666 ]));
667 let result = get_local_package_json_version_reqs_for_tests(&package_json);
668 assert_eq!(
669 result,
670 IndexMap::from([
671 (
672 "test".to_string(),
673 Ok(PackageJsonDepValue::Req(
674 PackageReq::from_str("test@1").unwrap()
675 ))
676 ),
677 (
678 "work-test-star".to_string(),
679 Ok(PackageJsonDepValue::Workspace(
680 PackageJsonDepWorkspaceReq::VersionReq(
681 VersionReq::parse_from_npm("*").unwrap()
682 )
683 ))
684 ),
685 (
686 "work-test-version-req".to_string(),
687 Ok(PackageJsonDepValue::Workspace(
688 PackageJsonDepWorkspaceReq::VersionReq(
689 VersionReq::parse_from_npm("1.1.1").unwrap()
690 )
691 ))
692 ),
693 (
694 "work-test-tilde".to_string(),
695 Ok(PackageJsonDepValue::Workspace(
696 PackageJsonDepWorkspaceReq::Tilde
697 ))
698 ),
699 (
700 "work-test-caret".to_string(),
701 Ok(PackageJsonDepValue::Workspace(
702 PackageJsonDepWorkspaceReq::Caret
703 ))
704 ),
705 (
706 "file-test".to_string(),
707 Ok(PackageJsonDepValue::File("something".to_string())),
708 ),
709 (
710 "git-test".to_string(),
711 Err(PackageJsonDepValueParseErrorKind::Unsupported {
712 scheme: "git".to_string()
713 }),
714 ),
715 (
716 "http-test".to_string(),
717 Err(PackageJsonDepValueParseErrorKind::Unsupported {
718 scheme: "http".to_string()
719 }),
720 ),
721 (
722 "https-test".to_string(),
723 Err(PackageJsonDepValueParseErrorKind::Unsupported {
724 scheme: "https".to_string()
725 }),
726 ),
727 ])
728 );
729 }
730
731 #[test]
732 fn test_deserialize_serialize() {
733 let json_value = serde_json::json!({
734 "name": "test",
735 "version": "1",
736 "exports": {
737 ".": "./main.js",
738 },
739 "bin": "./main.js",
740 "types": "./types.d.ts",
741 "typesVersions": {
742 "<4.0": { "index.d.ts": ["index.v3.d.ts"] }
743 },
744 "imports": {
745 "#test": "./main.js",
746 },
747 "main": "./main.js",
748 "module": "./module.js",
749 "type": "module",
750 "dependencies": {
751 "name": "1.2",
752 },
753 "devDependencies": {
754 "name": "1.2",
755 },
756 "scripts": {
757 "test": "echo \"Error: no test specified\" && exit 1",
758 },
759 "workspaces": ["asdf", "asdf2"],
760 "cpu": ["x86_64"],
761 "os": ["win32"],
762 "optionalDependencies": {
763 "optional": "1.1"
764 },
765 "peerDependencies": {
766 "peer": "1.0"
767 },
768 "peerDependenciesMeta": {
769 "peer": {
770 "optional": true
771 }
772 },
773 });
774 let package_json = PackageJson::load_from_value(
775 PathBuf::from("/package.json"),
776 json_value.clone(),
777 )
778 .unwrap();
779 let serialized_value = serde_json::to_value(&package_json).unwrap();
780 assert_eq!(serialized_value, json_value);
781 }
782
783 #[test]
785 fn test_exports_error() {
786 let json_value = serde_json::json!({
787 "name": "test",
788 "version": "1",
789 "exports": { ".": "./a", "a": "./a" },
790 });
791 assert!(matches!(
792 PackageJson::load_from_value(
793 PathBuf::from("/package.json"),
794 json_value.clone(),
795 ),
796 Err(PackageJsonLoadError::InvalidExports)
797 ));
798 }
799}