diff --git a/Cargo.lock b/Cargo.lock index ad4a2905cad909..8541b0bf04c545 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1604,6 +1604,7 @@ dependencies = [ "deno_lib", "deno_lint", "deno_lockfile", + "deno_maybe_sync", "deno_media_type", "deno_npm", "deno_npm_cache", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a0ba218648c081..d7945bbfdc794e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -77,6 +77,7 @@ deno_graph = { workspace = true, features = ["fast_check"] } deno_lib.workspace = true deno_lint.workspace = true deno_lockfile.workspace = true +deno_maybe_sync.workspace = true deno_media_type = { workspace = true, features = ["data_url", "decoding", "module_specifier"] } deno_npm.workspace = true deno_npm_cache.workspace = true diff --git a/cli/factory.rs b/cli/factory.rs index 37468f956ff812..0d720ff62f794c 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -1282,14 +1282,7 @@ fn new_workspace_factory_options( flags: &Flags, ) -> deno_resolver::factory::WorkspaceFactoryOptions { deno_resolver::factory::WorkspaceFactoryOptions { - additional_config_file_names: if matches!( - flags.subcommand, - DenoSubcommand::Publish(..) - ) { - &["jsr.json", "jsr.jsonc"] - } else { - &[] - }, + discover_jsr_config: true, config_discovery: match &flags.config_flag { ConfigFlag::Discover => { if let Some(start_paths) = flags.config_path_args(initial_cwd) { diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index e832938a0fca02..076ac5bf14b4e3 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1284,7 +1284,7 @@ impl ConfigData { } }, &WorkspaceDiscoverOptions { - additional_config_file_names: &[], + discover_jsr_config: true, deno_json_cache: Some(deno_json_cache), pkg_json_cache: Some(pkg_json_cache), workspace_cache: Some(workspace_cache), @@ -1428,7 +1428,7 @@ impl ConfigData { CliSys::default(), member_dir.dir_path(), WorkspaceFactoryOptions { - additional_config_file_names: &[], + discover_jsr_config: true, config_discovery: ConfigDiscoveryOption::DiscoverCwd, maybe_custom_deno_dir_root: None, is_package_manager_subcommand: false, diff --git a/cli/tools/publish/diagnostics.rs b/cli/tools/publish/diagnostics.rs index 5f3801741f48b5..6ac7f3a21225ab 100644 --- a/cli/tools/publish/diagnostics.rs +++ b/cli/tools/publish/diagnostics.rs @@ -142,6 +142,10 @@ pub enum PublishDiagnostic { MissingLicense { config_specifier: Url, }, + ConflictingPublishConfig { + primary_specifier: Url, + specifiers: Vec, + }, } impl PublishDiagnostic { @@ -193,6 +197,7 @@ impl Diagnostic for PublishDiagnostic { SyntaxError { .. } => DiagnosticLevel::Error, MissingLicense { .. } => DiagnosticLevel::Error, UnstableRawImport { .. } => DiagnosticLevel::Error, + ConflictingPublishConfig { .. } => DiagnosticLevel::Error, } } @@ -214,6 +219,9 @@ impl Diagnostic for PublishDiagnostic { SyntaxError { .. } => Cow::Borrowed("syntax-error"), MissingLicense { .. } => Cow::Borrowed("missing-license"), UnstableRawImport { .. } => Cow::Borrowed("unstable-raw-import"), + ConflictingPublishConfig { .. } => { + Cow::Borrowed("conflicting-publish-config") + } } } @@ -255,6 +263,14 @@ impl Diagnostic for PublishDiagnostic { UnstableRawImport { .. } => { Cow::Borrowed("raw imports have not been stabilized") } + ConflictingPublishConfig { specifiers, .. } => Cow::Owned(format!( + "Publish configuration is defined in multiple files: {}.", + specifiers + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + )), } } @@ -328,6 +344,11 @@ impl Diagnostic for PublishDiagnostic { MissingLicense { config_specifier } => DiagnosticLocation::Module { specifier: Cow::Borrowed(config_specifier), }, + ConflictingPublishConfig { + primary_specifier, .. + } => DiagnosticLocation::Module { + specifier: Cow::Borrowed(primary_specifier), + }, } } @@ -402,6 +423,7 @@ impl Diagnostic for PublishDiagnostic { } SyntaxError(diagnostic) => diagnostic.snippet(), MissingLicense { .. } => None, + ConflictingPublishConfig { .. } => None, } } @@ -448,6 +470,9 @@ impl Diagnostic for PublishDiagnostic { UnstableRawImport { .. } => Some(Cow::Borrowed( "for the time being, embed the data directly into a JavaScript file (ex. as encoded base64 text)", )), + ConflictingPublishConfig { .. } => Some(Cow::Borrowed( + "Remove the conflicting configuration so that only one file defines the package metadata", + )), } } @@ -486,7 +511,8 @@ impl Diagnostic for PublishDiagnostic { | MissingConstraint { .. } | BannedTripleSlashDirectives { .. } | MissingLicense { .. } - | UnstableRawImport { .. } => None, + | UnstableRawImport { .. } + | ConflictingPublishConfig { .. } => None, } } @@ -541,6 +567,7 @@ impl Diagnostic for PublishDiagnostic { SyntaxError(diagnostic) => diagnostic.info(), MissingLicense { .. } => Cow::Borrowed(&[]), UnstableRawImport { .. } => Cow::Borrowed(&[]), + ConflictingPublishConfig { .. } => Cow::Borrowed(&[]), } } @@ -580,6 +607,9 @@ impl Diagnostic for PublishDiagnostic { UnstableRawImport { .. } => Some(Cow::Borrowed( "https://github.com/denoland/deno/issues/29904", )), + ConflictingPublishConfig { .. } => { + Some(Cow::Borrowed("https://jsr.io/docs/package-configuration")) + } } } } diff --git a/cli/tools/publish/mod.rs b/cli/tools/publish/mod.rs index 8939d3029d1436..aab4a616e13459 100644 --- a/cli/tools/publish/mod.rs +++ b/cli/tools/publish/mod.rs @@ -16,6 +16,7 @@ use deno_ast::SourceTextInfo; use deno_config::deno_json::ConfigFile; use deno_config::workspace::JsrPackageConfig; use deno_config::workspace::Workspace; +use deno_config::workspace::WorkspaceDiagnosticKind; use deno_core::anyhow::Context; use deno_core::anyhow::bail; use deno_core::error::AnyError; @@ -85,22 +86,46 @@ pub async fn publish( let cli_options = cli_factory.cli_options()?; let directory_path = cli_options.initial_cwd(); + let diagnostics_collector = PublishDiagnosticsCollector::default(); + // Convert workspace diagnostics to publish diagnostics + for workspace_diagnostic in cli_options.start_dir.workspace.diagnostics() { + if let WorkspaceDiagnosticKind::ConflictingPublishConfig { + deno_config_url, + jsr_config_url, + } = &workspace_diagnostic.kind + { + let mut specifiers = + vec![deno_config_url.clone(), jsr_config_url.clone()]; + specifiers.sort(); + specifiers.dedup(); + diagnostics_collector.push(PublishDiagnostic::ConflictingPublishConfig { + primary_specifier: jsr_config_url.clone(), + specifiers, + }); + } + } + if diagnostics_collector.has_error() { + return diagnostics_collector.print_and_error(); + } let mut publish_configs = cli_options.start_dir.jsr_packages_for_publish(); if publish_configs.is_empty() { - match cli_options.start_dir.maybe_deno_json() { - Some(deno_json) => { - debug_assert!(!deno_json.is_package()); - if deno_json.json.name.is_none() { - bail!("Missing 'name' field in '{}'.", deno_json.specifier); - } - error_missing_exports_field(deno_json)?; + if let Some(deno_json) = cli_options.start_dir.maybe_deno_json() { + debug_assert!(!deno_json.is_package()); + if deno_json.json.name.is_none() { + bail!("Missing 'name' field in '{}'.", deno_json.specifier); } - None => { - bail!( - "Couldn't find a deno.json, deno.jsonc, jsr.json or jsr.jsonc configuration file in {}.", - directory_path.display() - ); + error_missing_exports_field(deno_json)?; + } else if let Some(jsr_json) = cli_options.start_dir.maybe_jsr_json() { + debug_assert!(!jsr_json.is_package()); + if jsr_json.json.name.is_none() { + bail!("Missing 'name' field in '{}'.", jsr_json.specifier); } + error_missing_exports_field(jsr_json)?; + } else { + bail!( + "Couldn't find a deno.json, deno.jsonc, jsr.json or jsr.jsonc configuration file in {}.", + directory_path.display() + ); } } @@ -123,7 +148,6 @@ pub async fn publish( cli_options.unstable_bare_node_builtins(), ); - let diagnostics_collector = PublishDiagnosticsCollector::default(); let parsed_source_cache = cli_factory.parsed_source_cache()?; let module_content_provider = Arc::new(ModuleContentProvider::new( parsed_source_cache.clone(), @@ -1357,7 +1381,10 @@ mod tests { use std::collections::HashMap; use deno_ast::ModuleSpecifier; + use deno_ast::diagnostics::Diagnostic; + use deno_core::url::Url; + use super::PublishDiagnostic; use super::has_license_file; use super::tar::PublishableTarball; use super::tar::PublishableTarballFile; @@ -1486,4 +1513,55 @@ mod tests { "file:///test/tLICENSE" ]),); } + + #[test] + fn test_conflicting_publish_config_diagnostic_lists_specifiers() { + let deno_specifier = Url::parse("file:///example/deno.json").unwrap(); + let jsr_specifier = Url::parse("file:///example/jsr.json").unwrap(); + let diagnostic = PublishDiagnostic::ConflictingPublishConfig { + primary_specifier: jsr_specifier.clone(), + specifiers: vec![deno_specifier.clone(), jsr_specifier.clone()], + }; + let message = diagnostic.message(); + assert!(message.contains("deno.json")); + assert!(message.contains("jsr.json")); + let hint = diagnostic.hint().expect("expected hint"); + assert!(hint.contains("Remove the conflicting configuration")); + } + + #[test] + fn test_workspace_diagnostics_converted_to_publish_diagnostics() { + use deno_ast::diagnostics::DiagnosticLevel; + use deno_config::workspace::WorkspaceDiagnosticKind; + + let deno_config_url = Url::parse("file:///example/deno.json").unwrap(); + let jsr_config_url = Url::parse("file:///example/jsr.json").unwrap(); + + let workspace_diagnostic_kind = + WorkspaceDiagnosticKind::ConflictingPublishConfig { + deno_config_url: deno_config_url.clone(), + jsr_config_url: jsr_config_url.clone(), + }; + + // Simulate the conversion logic from publish() + let mut specifiers = vec![deno_config_url.clone(), jsr_config_url.clone()]; + specifiers.sort(); + specifiers.dedup(); + + let publish_diagnostic = PublishDiagnostic::ConflictingPublishConfig { + primary_specifier: jsr_config_url.clone(), + specifiers: specifiers.clone(), + }; + + // Verify the diagnostic was created correctly + assert!(matches!( + workspace_diagnostic_kind, + WorkspaceDiagnosticKind::ConflictingPublishConfig { .. } + )); + assert!(matches!(publish_diagnostic.level(), DiagnosticLevel::Error)); + let message = publish_diagnostic.message(); + assert!(message.contains("deno.json")); + assert!(message.contains("jsr.json")); + assert_eq!(specifiers.len(), 2); + } } diff --git a/libs/config/workspace/discovery.rs b/libs/config/workspace/discovery.rs index 3c90c41ad0e289..a318c65d86ba77 100644 --- a/libs/config/workspace/discovery.rs +++ b/libs/config/workspace/discovery.rs @@ -43,91 +43,72 @@ use crate::util::is_skippable_io_error; use crate::workspace::ConfigReadError; use crate::workspace::Workspace; -#[derive(Debug)] -pub enum DenoOrPkgJson { - Deno(ConfigFileRc), - PkgJson(PackageJsonRc), +#[derive(Debug, Clone)] +pub struct ConfigFolder { + deno_json: Option, + jsr_json: Option, + pkg_json: Option, } -impl DenoOrPkgJson { - pub fn specifier(&self) -> Cow<'_, Url> { - match self { - Self::Deno(config) => Cow::Borrowed(&config.specifier), - Self::PkgJson(pkg_json) => Cow::Owned(pkg_json.specifier()), +impl ConfigFolder { + pub fn new( + deno_json: Option, + jsr_json: Option, + pkg_json: Option, + ) -> Option { + if deno_json.is_none() && jsr_json.is_none() && pkg_json.is_none() { + None + } else { + Some(Self { + deno_json, + jsr_json, + pkg_json, + }) } } -} -#[derive(Debug)] -pub enum ConfigFolder { - Single(DenoOrPkgJson), - Both { - deno_json: ConfigFileRc, - pkg_json: PackageJsonRc, - }, -} - -impl ConfigFolder { pub fn folder_url(&self) -> Url { match self { - Self::Single(DenoOrPkgJson::Deno(config)) => { - url_parent(&config.specifier) - } - Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => { - url_from_directory_path(pkg_json.path.parent().unwrap()).unwrap() - } - Self::Both { deno_json, .. } => url_parent(&deno_json.specifier), + Self { + deno_json: Some(config), + .. + } => url_parent(&config.specifier), + Self { + jsr_json: Some(config), + .. + } => url_parent(&config.specifier), + Self { + pkg_json: Some(pkg_json), + .. + } => url_from_directory_path(pkg_json.path.parent().unwrap()).unwrap(), + _ => unreachable!("config folder must have at least one config"), } } pub fn has_workspace_members(&self) -> bool { match self { - Self::Single(DenoOrPkgJson::Deno(config)) => { - config.json.workspace.is_some() - } - Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => { - pkg_json.workspaces.is_some() - } - Self::Both { - deno_json, - pkg_json, - } => deno_json.json.workspace.is_some() || pkg_json.workspaces.is_some(), + Self { + deno_json: Some(config), + .. + } => config.json.workspace.is_some(), + Self { + pkg_json: Some(pkg_json), + .. + } => pkg_json.workspaces.is_some(), + Self { .. } => false, } } pub fn deno_json(&self) -> Option<&ConfigFileRc> { - match self { - Self::Single(DenoOrPkgJson::Deno(deno_json)) => Some(deno_json), - Self::Both { deno_json, .. } => Some(deno_json), - _ => None, - } + self.deno_json.as_ref() } - pub fn pkg_json(&self) -> Option<&PackageJsonRc> { - match self { - Self::Single(DenoOrPkgJson::PkgJson(pkg_json)) => Some(pkg_json), - Self::Both { pkg_json, .. } => Some(pkg_json), - _ => None, - } + pub fn jsr_json(&self) -> Option<&ConfigFileRc> { + self.jsr_json.as_ref() } - pub fn from_maybe_both( - maybe_deno_json: Option, - maybe_pkg_json: Option, - ) -> Option { - match (maybe_deno_json, maybe_pkg_json) { - (Some(deno_json), Some(pkg_json)) => Some(Self::Both { - deno_json, - pkg_json, - }), - (Some(deno_json), None) => { - Some(Self::Single(DenoOrPkgJson::Deno(deno_json))) - } - (None, Some(pkg_json)) => { - Some(Self::Single(DenoOrPkgJson::PkgJson(pkg_json))) - } - (None, None) => None, - } + pub fn pkg_json(&self) -> Option<&PackageJsonRc> { + self.pkg_json.as_ref() } } @@ -156,9 +137,14 @@ impl ConfigFileDiscovery { } fn config_folder_config_specifier(res: &ConfigFolder) -> Cow<'_, Url> { - match res { - ConfigFolder::Single(config) => config.specifier(), - ConfigFolder::Both { deno_json, .. } => Cow::Borrowed(&deno_json.specifier), + if let Some(deno_json) = res.deno_json() { + Cow::Borrowed(&deno_json.specifier) + } else if let Some(jsr_json) = res.jsr_json() { + Cow::Borrowed(&jsr_json.specifier) + } else if let Some(pkg_json) = res.pkg_json() { + Cow::Owned(pkg_json.specifier()) + } else { + unreachable!("Config folder should have at least one configuration file.") } } @@ -263,8 +249,7 @@ fn discover_workspace_config_files_for_single_dir< let start_dir: Option<&Path>; let mut first_config_folder_url: Option = None; let mut found_config_folders: HashMap<_, ConfigFolder> = HashMap::new(); - let config_file_names = - ConfigFile::resolve_config_file_names(opts.additional_config_file_names); + let config_file_names = ConfigFile::resolve_config_file_names(&[]); let load_pkg_json_in_folder = |folder_path: &Path| { if opts.discover_pkg_json { let pkg_json_path = folder_path.join("package.json"); @@ -294,15 +279,27 @@ fn discover_workspace_config_files_for_single_dir< } }; let load_config_folder = |folder_path: &Path| -> Result<_, ConfigReadError> { + const JSR_CONFIG_FILE_NAMES: [&str; 2] = ["jsr.json", "jsr.jsonc"]; let maybe_config_file = ConfigFile::maybe_find_in_folder( sys, opts.deno_json_cache, folder_path, &config_file_names, )?; + let maybe_jsr_config = if opts.discover_jsr_config { + ConfigFile::maybe_find_in_folder( + sys, + opts.deno_json_cache, + folder_path, + &JSR_CONFIG_FILE_NAMES, + )? + } else { + None + }; let maybe_pkg_json = load_pkg_json_in_folder(folder_path)?; - Ok(ConfigFolder::from_maybe_both( + Ok(ConfigFolder::new( maybe_config_file, + maybe_jsr_config, maybe_pkg_json, )) }; @@ -342,7 +339,7 @@ fn discover_workspace_config_files_for_single_dir< // don't try to load a workspace and don't store this information in // the workspace cache let config_folder = - ConfigFolder::Single(DenoOrPkgJson::Deno(config_file)); + ConfigFolder::new(Some(config_file), None, None).unwrap(); if config_folder.has_workspace_members() { return handle_workspace_folder_with_members( diff --git a/libs/config/workspace/mod.rs b/libs/config/workspace/mod.rs index 8694b5391407a4..f5325dc3ff5818 100644 --- a/libs/config/workspace/mod.rs +++ b/libs/config/workspace/mod.rs @@ -28,7 +28,6 @@ use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use discovery::ConfigFileDiscovery; use discovery::ConfigFolder; -use discovery::DenoOrPkgJson; use discovery::discover_workspace_config_files; use indexmap::IndexMap; use indexmap::IndexSet; @@ -179,6 +178,15 @@ pub enum WorkspaceDiagnosticKind { "\"minimumDependencyAge.exclude\" entry \"{entry}\" missing jsr: or npm: prefix." )] MinimumDependencyAgeExcludeMissingPrefix { entry: String }, + #[error( + "Publish configuration is defined in both {} and {}. Remove one of the files.", + deno_config_url, + jsr_config_url + )] + ConflictingPublishConfig { + deno_config_url: Url, + jsr_config_url: Url, + }, } #[derive(Debug, Error, JsError, Clone, PartialEq, Eq)] @@ -383,7 +391,7 @@ pub struct WorkspaceDiscoverOptions<'a> { /// A cache for workspaces. This is mostly only useful in the LSP where /// workspace discovery may occur multiple times. pub workspace_cache: Option<&'a dyn WorkspaceCache>, - pub additional_config_file_names: &'a [&'a str], + pub discover_jsr_config: bool, pub discover_pkg_json: bool, pub maybe_vendor_override: Option>, } @@ -398,29 +406,16 @@ pub struct WorkspaceDirectoryEmptyOptions<'a> { #[derive(Debug, Default, Clone)] pub struct FolderConfigs { pub deno_json: Option, + pub jsr_json: Option, pub pkg_json: Option, } impl FolderConfigs { fn from_config_folder(config_folder: ConfigFolder) -> Self { - match config_folder { - ConfigFolder::Single(deno_or_pkg_json) => match deno_or_pkg_json { - DenoOrPkgJson::Deno(deno_json) => FolderConfigs { - deno_json: Some(deno_json), - pkg_json: None, - }, - DenoOrPkgJson::PkgJson(pkg_json) => FolderConfigs { - deno_json: None, - pkg_json: Some(pkg_json), - }, - }, - ConfigFolder::Both { - deno_json, - pkg_json, - } => FolderConfigs { - deno_json: Some(deno_json), - pkg_json: Some(pkg_json), - }, + FolderConfigs { + deno_json: config_folder.deno_json().cloned(), + jsr_json: config_folder.jsr_json().cloned(), + pkg_json: config_folder.pkg_json().cloned(), } } } @@ -739,17 +734,11 @@ impl Workspace { pub fn jsr_packages<'a>( self: &'a WorkspaceRc, ) -> impl Iterator + 'a { - self.deno_jsons().filter_map(|c| { - if !c.is_package() { - return None; - } - Some(JsrPackageConfig { - member_dir: self.resolve_member_dir(&c.specifier), - name: c.json.name.clone()?, - config_file: c.clone(), - license: c.to_license(), - }) - }) + self + .config_folders + .keys() + .map(|dir_url| self.resolve_member_dir(dir_url)) + .filter_map(|dir| dir.maybe_package_config()) } pub fn npm_packages(self: &WorkspaceRc) -> Vec { @@ -881,6 +870,18 @@ impl Workspace { let root = self.config_folders.get(&self.root_dir_url).unwrap(); root.pkg_json.as_ref().map(|c| (&self.root_dir_url, c)) }); + let maybe_jsr_json = folder + .jsr_json + .as_ref() + .map(|c| (member_url, c)) + .or_else(|| { + let parent = parent_specifier_str(member_url.as_str())?; + self.resolve_jsr_json_from_str(parent) + }) + .or_else(|| { + let root = self.config_folders.get(&self.root_dir_url).unwrap(); + root.jsr_json.as_ref().map(|c| (&self.root_dir_url, c)) + }); WorkspaceDirectory { dir_url: member_url.clone(), pkg_json: maybe_pkg_json.map(|(member_url, pkg_json)| { @@ -913,6 +914,21 @@ impl Workspace { member: config.clone(), } }), + jsr_json: maybe_jsr_json.map(|(member_url, config)| { + WorkspaceDirConfig { + root: if self.root_dir_url == *member_url { + None + } else { + self + .config_folders + .get(&self.root_dir_url) + .unwrap() + .jsr_json + .clone() + }, + member: config.clone(), + } + }), workspace: self.clone(), cached: Default::default(), } @@ -949,6 +965,23 @@ impl Workspace { } } + fn resolve_jsr_json_from_str( + &self, + specifier: &str, + ) -> Option<(&UrlRc, &ConfigFileRc)> { + let mut specifier = specifier; + if !specifier.ends_with('/') { + specifier = parent_specifier_str(specifier)?; + } + loop { + let (folder_url, folder) = self.resolve_folder_str(specifier)?; + if let Some(config) = folder.jsr_json.as_ref() { + return Some((folder_url, config)); + } + specifier = parent_specifier_str(folder_url.as_str())?; + } + } + fn resolve_pkg_json_from_str( &self, specifier: &str, @@ -1154,6 +1187,21 @@ impl Workspace { } let mut diagnostics = Vec::new(); + for folder in self.config_folders.values() { + if let (Some(deno_json), Some(jsr_json)) = + (folder.deno_json.as_ref(), folder.jsr_json.as_ref()) + && deno_json.is_package() + && jsr_json.is_package() + { + diagnostics.push(WorkspaceDiagnostic { + config_url: jsr_json.specifier.clone(), + kind: WorkspaceDiagnosticKind::ConflictingPublishConfig { + deno_config_url: deno_json.specifier.clone(), + jsr_config_url: jsr_json.specifier.clone(), + }, + }); + } + } for (url, folder) in &self.config_folders { if let Some(config) = &folder.deno_json { let is_root = url == &self.root_dir_url; @@ -1542,6 +1590,7 @@ pub struct WorkspaceDirectory { dir_url: UrlRc, pkg_json: Option>, deno_json: Option>, + jsr_json: Option>, cached: CachedDirectoryValues, } @@ -1680,6 +1729,12 @@ impl WorkspaceDirectory { root: None, } }), + jsr_json: root_folder.jsr_json.as_ref().map(|config| { + WorkspaceDirConfig { + member: config.clone(), + root: None, + } + }), workspace, cached: Default::default(), } @@ -1734,6 +1789,10 @@ impl WorkspaceDirectory { self.deno_json.as_ref().map(|c| &c.member) } + pub fn maybe_jsr_json(&self) -> Option<&ConfigFileRc> { + self.jsr_json.as_ref().map(|c| &c.member) + } + pub fn maybe_pkg_json(&self) -> Option<&PackageJsonRc> { self.pkg_json.as_ref().map(|c| &c.member) } @@ -1741,17 +1800,29 @@ impl WorkspaceDirectory { pub fn maybe_package_config( self: &WorkspaceDirectoryRc, ) -> Option { - let deno_json = self.maybe_deno_json()?; - let pkg_name = deno_json.json.name.as_ref()?; - if !deno_json.is_package() { - return None; + if let Some(deno_json) = self.maybe_deno_json() + && deno_json.is_package() + && let Some(pkg_name) = deno_json.json.name.clone() + { + return Some(JsrPackageConfig { + name: pkg_name, + config_file: deno_json.clone(), + member_dir: self.clone(), + license: deno_json.to_license(), + }); } - Some(JsrPackageConfig { - name: pkg_name.clone(), - config_file: deno_json.clone(), - member_dir: self.clone(), - license: deno_json.to_license(), - }) + if let Some(jsr_json) = self.maybe_jsr_json() + && jsr_json.is_package() + && let Some(pkg_name) = jsr_json.json.name.clone() + { + return Some(JsrPackageConfig { + name: pkg_name, + config_file: jsr_json.clone(), + member_dir: self.clone(), + license: jsr_json.to_license(), + }); + } + None } /// Gets a list of raw compiler options that the user provided, in a vec of @@ -2911,6 +2982,105 @@ pub mod test { assert_eq!(workspace_dir.workspace.config_folders.len(), 3); } + #[test] + fn test_jsr_package_config_fallback_without_deno() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("jsr.json"), + json!({ + "name": "@scope/pkg", + "version": "1.2.3", + "exports": "./mod.ts" + }), + ); + let workspace_dir = WorkspaceDirectory::discover( + &sys, + WorkspaceDiscoverStart::Paths(&[root_dir()]), + &WorkspaceDiscoverOptions { + discover_jsr_config: true, + ..Default::default() + }, + ) + .unwrap(); + let package = workspace_dir + .maybe_package_config() + .expect("expected package config"); + assert_eq!(package.name, "@scope/pkg"); + assert!(package.config_file.specifier.path().ends_with("/jsr.json")); + } + + #[test] + fn test_jsr_package_config_fallback_with_deno_not_package() { + let sys = InMemorySys::default(); + // deno.json exists but is not a package (no name/version/exports) + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "fmt": { + "semiColons": false + } + }), + ); + // jsr.json has package metadata + sys.fs_insert_json( + root_dir().join("jsr.json"), + json!({ + "name": "@scope/pkg", + "version": "1.2.3", + "exports": "./mod.ts" + }), + ); + let workspace_dir = WorkspaceDirectory::discover( + &sys, + WorkspaceDiscoverStart::Paths(&[root_dir()]), + &WorkspaceDiscoverOptions { + discover_jsr_config: true, + ..Default::default() + }, + ) + .unwrap(); + let package = workspace_dir + .maybe_package_config() + .expect("expected package config"); + assert_eq!(package.name, "@scope/pkg"); + assert!(package.config_file.specifier.path().ends_with("/jsr.json")); + } + + #[test] + fn test_conflicting_publish_configs_diagnostic() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "name": "@scope/pkg", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + sys.fs_insert_json( + root_dir().join("jsr.json"), + json!({ + "name": "@scope/pkg", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + let workspace_dir = WorkspaceDirectory::discover( + &sys, + WorkspaceDiscoverStart::Paths(&[root_dir()]), + &WorkspaceDiscoverOptions { + discover_jsr_config: true, + ..Default::default() + }, + ) + .unwrap(); + let diagnostics = workspace_dir.workspace.diagnostics(); + assert!(diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + WorkspaceDiagnosticKind::ConflictingPublishConfig { .. } + ))); + } + #[test] fn test_tasks() { let sys = InMemorySys::default(); diff --git a/libs/resolver/factory.rs b/libs/resolver/factory.rs index a839bf03e63a0c..ff236619a0a680 100644 --- a/libs/resolver/factory.rs +++ b/libs/resolver/factory.rs @@ -199,7 +199,7 @@ pub struct NpmProcessStateOptions { #[derive(Debug, Default)] pub struct WorkspaceFactoryOptions { - pub additional_config_file_names: &'static [&'static str], + pub discover_jsr_config: bool, pub config_discovery: ConfigDiscoveryOption, pub is_package_manager_subcommand: bool, pub frozen_lockfile: Option, @@ -570,9 +570,7 @@ impl WorkspaceFactory { deno_json_cache: None, pkg_json_cache: Some(&node_resolver::PackageJsonThreadLocalCache), workspace_cache: None, - additional_config_file_names: self - .options - .additional_config_file_names, + discover_jsr_config: self.options.discover_jsr_config, discover_pkg_json, maybe_vendor_override, } diff --git a/tests/integration/publish_tests.rs b/tests/integration/publish_tests.rs index 6c0b8e985497e0..54e0239da839b4 100644 --- a/tests/integration/publish_tests.rs +++ b/tests/integration/publish_tests.rs @@ -527,3 +527,55 @@ fn publish_context_builder_with_git_checks() -> TestContextBuilder { .envs(env_vars_for_jsr_tests_with_git_check()) .use_temp_cwd() } + +#[test] +fn publish_workspace_with_jsr_json_fallback() { + let context = publish_context_builder().build(); + let temp_dir = context.temp_dir().path(); + + // Root workspace config + temp_dir.join("deno.json").write_json(&json!({ + "workspace": ["./pkg1", "./pkg2"] + })); + temp_dir.join("LICENSE").write("MIT License"); + + // pkg1: deno.json without package fields, jsr.json with package metadata + let pkg1_dir = temp_dir.join("pkg1"); + pkg1_dir.create_dir_all(); + pkg1_dir.join("deno.json").write_json(&json!({ + "fmt": { + "semiColons": false + } + })); + pkg1_dir.join("jsr.json").write_json(&json!({ + "name": "@scope/pkg1", + "version": "0.1.0", + "exports": "./mod.ts" + })); + pkg1_dir.join("mod.ts").write( + "export function add(a: number, b: number): number { return a + b; }", + ); + + // pkg2: deno.json with package metadata (standard case) + let pkg2_dir = temp_dir.join("pkg2"); + pkg2_dir.create_dir_all(); + pkg2_dir.join("deno.json").write_json(&json!({ + "name": "@scope/pkg2", + "version": "0.2.0", + "exports": "./mod.ts" + })); + pkg2_dir.join("mod.ts").write( + "export function subtract(a: number, b: number): number { return a - b; }", + ); + + let output = context + .new_command() + .args("publish --dry-run --allow-dirty") + .run(); + output.assert_exit_code(0); + let output_text = output.combined_output(); + assert_contains!(output_text, "@scope/pkg1@0.1.0"); + assert_contains!(output_text, "@scope/pkg2@0.2.0"); + assert_contains!(output_text, "pkg1/jsr.json"); + assert_contains!(output_text, "pkg2/deno.json"); +}