From eb7f7377a2e94343da2e96a820eaa1d99ae9d27c Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 10 Jul 2024 12:22:13 -0400 Subject: [PATCH] fix: support publishing jsr packages in an npm workspace (#77) This supports publishing jsr packages when the root is an npm workspace. --- src/workspace/mod.rs | 106 +++++++++++++++++++++++++++++++++----- src/workspace/resolver.rs | 106 ++++++++++++++++++++++++++------------ 2 files changed, 166 insertions(+), 46 deletions(-) diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index bc7e488..13d0453 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -674,27 +674,25 @@ impl Workspace { pub fn jsr_packages_for_publish(self: &WorkspaceRc) -> Vec { let ctx = self.resolve_start_ctx(); - let Some(config) = &ctx.deno_json else { - return Vec::new(); - }; - let deno_json = &config.member; + // only publish the current folder if it's a package + if let Some(package_config) = ctx.maybe_package_config() { + return vec![package_config]; + } if let Some(pkg_json) = &ctx.pkg_json { + let ctx_dir_path = ctx.dir_url.to_file_path().unwrap(); // don't publish anything if in a package.json only directory within // a workspace - if pkg_json.member.dir_path().starts_with(deno_json.dir_path()) - && deno_json.dir_path() != pkg_json.member.dir_path() + if pkg_json.member.dir_path().starts_with(&ctx_dir_path) + && ctx_dir_path != pkg_json.member.dir_path() { return Vec::new(); } } - if deno_json.dir_path() == self.root_dir.to_file_path().unwrap() - && !(deno_json.is_workspace() && deno_json.is_package()) - { - return self.jsr_packages(); - } - match ctx.maybe_package_config() { - Some(pkg) => vec![pkg], - None => Vec::new(), + if ctx.dir_url == self.root_dir { + self.jsr_packages() + } else { + // nothing to publish + Vec::new() } } @@ -3027,6 +3025,86 @@ mod test { assert_eq!(names, vec!["@scope/pkg"]); } + #[test] + fn test_packages_for_publish_npm_workspace() { + let mut fs = TestFileSystem::default(); + fs.insert_json( + root_dir().join("package.json"), + json!({ + "workspaces": ["./a", "./b", "./c", "./d"] + }), + ); + fs.insert_json(root_dir().join("a/package.json"), json!({})); + fs.insert_json( + root_dir().join("a/deno.json"), + json!({ + "name": "@scope/a", + "version": "1.0.0", + "exports": "./main.ts", + }), + ); + fs.insert_json(root_dir().join("b/package.json"), json!({})); + fs.insert_json( + root_dir().join("b/deno.json"), + json!({ + "name": "@scope/b", + "version": "1.0.0", + "exports": "./main.ts", + }), + ); + fs.insert_json(root_dir().join("c/package.json"), json!({})); + fs.insert_json( + root_dir().join("c/deno.json"), + // not a package + json!({}), + ); + fs.insert_json( + root_dir().join("d/package.json"), + json!({ + "name": "pkg", + "version": "1.0.0", + }), + ); + // root + { + let workspace = workspace_at_start_dir(&fs, &root_dir()); + assert_eq!(workspace.diagnostics(), vec![]); + let jsr_pkgs = workspace.jsr_packages_for_publish(); + let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::>(); + assert_eq!(names, vec!["@scope/a", "@scope/b"]); + } + // member + { + let workspace = workspace_at_start_dir(&fs, &root_dir().join("a")); + assert_eq!(workspace.diagnostics(), vec![]); + let jsr_pkgs = workspace.jsr_packages_for_publish(); + let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::>(); + assert_eq!(names, vec!["@scope/a"]); + } + // member, not a package + { + let workspace = workspace_at_start_dir(&fs, &root_dir().join("c")); + assert_eq!(workspace.diagnostics(), vec![]); + let jsr_pkgs = workspace.jsr_packages_for_publish(); + assert!(jsr_pkgs.is_empty()); + } + // package.json + { + let workspace = workspace_at_start_dir(&fs, &root_dir().join("d")); + assert_eq!(workspace.diagnostics(), vec![]); + let jsr_pkgs = workspace.jsr_packages_for_publish(); + assert!(jsr_pkgs.is_empty()); + assert_eq!( + workspace + .npm_packages() + .into_iter() + .map(|p| p.pkg_json.dir_path().to_path_buf()) + .collect::>(), + vec![root_dir().join("d")] + ); + } + } + #[test] fn test_no_auto_discovery_node_modules_dir() { let mut fs = TestFileSystem::default(); diff --git a/src/workspace/resolver.rs b/src/workspace/resolver.rs index 807c448..56f8c45 100644 --- a/src/workspace/resolver.rs +++ b/src/workspace/resolver.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. MIT license. +use std::borrow::Cow; use std::collections::BTreeMap; use std::future::Future; use std::path::Path; @@ -190,40 +191,40 @@ impl WorkspaceResolver { (base_url, import_map) } None => { - let Some(config) = root_folder.deno_json.as_ref() else { + if !deno_jsons.iter().any(|p| p.is_package()) + && !deno_jsons.iter().any(|c| { + c.json.import_map.is_some() + || c.json.scopes.is_some() + || c.json.imports.is_some() + }) + { + // no configs have an import map and none are a package, so exit return Ok(None); + } + + let config_specified_import_map = match root_folder.deno_json.as_ref() + { + Some(deno_json) => deno_json + .to_import_map_value(fetch_text) + .await + .map_err(|source| WorkspaceResolverCreateError::ImportMapFetch { + referrer: deno_json.specifier.clone(), + source, + })? + .unwrap_or_else(|| { + ( + Cow::Borrowed(&deno_json.specifier), + serde_json::Value::Object(Default::default()), + ) + }), + None => ( + Cow::Owned(workspace.root_dir.join("deno.json").unwrap()), + serde_json::Value::Object(Default::default()), + ), }; - let config_specified_import_map = config - .to_import_map_value(fetch_text) - .await - .map_err(|source| WorkspaceResolverCreateError::ImportMapFetch { - referrer: config.specifier.clone(), - source, - })?; - let base_import_map_config = match config_specified_import_map { - Some((base_url, import_map_value)) => { - import_map::ext::ImportMapConfig { - base_url: base_url.into_owned(), - import_map_value, - } - } - None => { - if !deno_jsons.iter().any(|p| p.is_package()) - && !deno_jsons.iter().any(|c| { - c.json.import_map.is_some() - || c.json.scopes.is_some() - || c.json.imports.is_some() - }) - { - // no configs have an import map and none are a package, so exit - return Ok(None); - } - - import_map::ext::ImportMapConfig { - base_url: config.specifier.clone(), - import_map_value: serde_json::Value::Object(Default::default()), - } - } + let base_import_map_config = import_map::ext::ImportMapConfig { + base_url: config_specified_import_map.0.into_owned(), + import_map_value: config_specified_import_map.1, }; let child_import_map_configs = deno_jsons .iter() @@ -703,6 +704,47 @@ mod test { } } + #[tokio::test] + async fn resolve_workspace_pkg_json_workspace_deno_json_import_map() { + let mut fs = TestFileSystem::default(); + fs.insert_json( + root_dir().join("package.json"), + json!({ + "workspaces": ["*"] + }), + ); + fs.insert_json( + root_dir().join("a/package.json"), + json!({ + "name": "@scope/a", + "version": "1.0.0", + }), + ); + fs.insert_json( + root_dir().join("a/deno.json"), + json!({ + "name": "@scope/jsr-pkg", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + + let workspace = workspace_at_start_dir(&fs, &root_dir()); + let resolver = create_resolver(&workspace).await; + let resolution = resolver + .resolve( + "@scope/jsr-pkg", + &Url::from_file_path(root_dir().join("b.ts")).unwrap(), + ) + .unwrap(); + match resolution { + MappedResolution::ImportMap(specifier) => { + assert_eq!(specifier, Url::parse("jsr:@scope/jsr-pkg@^1.0.0").unwrap()); + } + _ => unreachable!(), + } + } + #[tokio::test] async fn specified_import_map() { let mut fs = TestFileSystem::default();