deno_workspace/
package_json.rs

1// Copyright 2018-2024 the Deno authors. MIT license.
2
3use 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>,   // use .main(...)
70  module: Option<String>, // use .main(...)
71  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    // Ignore unknown types for forwards compatibility
234    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    // for typescript, it looks for "typings" first, then "types"
249    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  /// Resolve the package.json's dependencies.
293  pub fn resolve_local_package_json_deps(&self) -> PackageJsonDeps {
294    /// Gets the name and raw version constraint for a registry info or
295    /// package.json dependency entry taking into account npm package aliases.
296    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 empty, then the name was scoped and there's no version
303          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    // favors the deps over dev_deps
363    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      // should be ignored
446      ("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}