Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion cli/tools/publish/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ pub enum PublishDiagnostic {
MissingLicense {
config_specifier: Url,
},
ConflictingPublishConfig {
primary_specifier: Url,
specifiers: Vec<Url>,
},
}

impl PublishDiagnostic {
Expand Down Expand Up @@ -193,6 +197,7 @@ impl Diagnostic for PublishDiagnostic {
SyntaxError { .. } => DiagnosticLevel::Error,
MissingLicense { .. } => DiagnosticLevel::Error,
UnstableRawImport { .. } => DiagnosticLevel::Error,
ConflictingPublishConfig { .. } => DiagnosticLevel::Error,
}
}

Expand All @@ -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")
}
}
}

Expand Down Expand Up @@ -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::<Vec<_>>()
.join(", ")
)),
}
}

Expand Down Expand Up @@ -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),
},
}
}

Expand Down Expand Up @@ -402,6 +423,7 @@ impl Diagnostic for PublishDiagnostic {
}
SyntaxError(diagnostic) => diagnostic.snippet(),
MissingLicense { .. } => None,
ConflictingPublishConfig { .. } => None,
}
}

Expand Down Expand Up @@ -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",
)),
}
}

Expand Down Expand Up @@ -486,7 +511,8 @@ impl Diagnostic for PublishDiagnostic {
| MissingConstraint { .. }
| BannedTripleSlashDirectives { .. }
| MissingLicense { .. }
| UnstableRawImport { .. } => None,
| UnstableRawImport { .. }
| ConflictingPublishConfig { .. } => None,
}
}

Expand Down Expand Up @@ -541,6 +567,7 @@ impl Diagnostic for PublishDiagnostic {
SyntaxError(diagnostic) => diagnostic.info(),
MissingLicense { .. } => Cow::Borrowed(&[]),
UnstableRawImport { .. } => Cow::Borrowed(&[]),
ConflictingPublishConfig { .. } => Cow::Borrowed(&[]),
}
}

Expand Down Expand Up @@ -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"))
}
}
}
}
156 changes: 154 additions & 2 deletions cli/tools/publish/mod.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like it will work when publishing a workspace. For example, someone might publish a workspace with members containing a deno.json and jsr.json -- Maybe we need more of a first class solution in libs/config instead of trying to fix this in the cli.

Copy link
Member

@dsherret dsherret Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, maybe we can get rid of additional_config_file_names and instead have a more specific discover_jsr_config: true flag that will surface a WorkspaceDiagnostic for this scenario.

Related is my comment here: https://github.com/denoland/deno/pull/31383/files#r2572813359 -- fixing that will help fix this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes, I see what you mean with workspaces. This only works as a solution for the root package basically.

Original file line number Diff line number Diff line change
Expand Up @@ -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::WorkspaceDirectoryRc;
use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
Expand All @@ -27,6 +28,7 @@ use deno_core::serde_json;
use deno_core::serde_json::Value;
use deno_core::serde_json::json;
use deno_core::url::Url;
use deno_maybe_sync::new_rc;
use deno_resolver::collections::FolderScopedMap;
use deno_runtime::deno_fetch;
use deno_terminal::colors;
Expand All @@ -53,6 +55,7 @@ use crate::graph_util::CreatePublishGraphOptions;
use crate::graph_util::ModuleGraphCreator;
use crate::http_util::HttpClient;
use crate::registry;
use crate::sys::CliSys;
use crate::tools::lint::collect_no_slow_type_diagnostics;
use crate::type_checker::CheckOptions;
use crate::type_checker::TypeChecker;
Expand Down Expand Up @@ -86,6 +89,34 @@ pub async fn publish(
let cli_options = cli_factory.cli_options()?;
let directory_path = cli_options.initial_cwd();
let mut publish_configs = cli_options.start_dir.jsr_packages_for_publish();
let sys = cli_factory.sys();
let diagnostics_collector = PublishDiagnosticsCollector::default();
let mut jsr_package =
maybe_jsr_package_config(&sys, &cli_options.start_dir, false)?;
if !publish_configs.is_empty() {
if let Some(jsr_package) = jsr_package {
let mut specifiers = publish_configs
.iter()
.map(|pkg| pkg.config_file.specifier.clone())
.collect::<Vec<_>>();
specifiers.push(jsr_package.config_file.specifier.clone());
specifiers.sort();
specifiers.dedup();
diagnostics_collector.push(PublishDiagnostic::ConflictingPublishConfig {
primary_specifier: jsr_package.config_file.specifier.clone(),
specifiers,
});
return diagnostics_collector.print_and_error();
}
} else {
if jsr_package.is_none() {
jsr_package =
maybe_jsr_package_config(&sys, &cli_options.start_dir, true)?;
}
if let Some(jsr_package_config) = jsr_package {
publish_configs.push(jsr_package_config);
}
}
if publish_configs.is_empty() {
match cli_options.start_dir.maybe_deno_json() {
Some(deno_json) => {
Expand Down Expand Up @@ -123,7 +154,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(),
Expand Down Expand Up @@ -1346,6 +1376,47 @@ fn error_missing_exports_field(deno_json: &ConfigFile) -> Result<(), AnyError> {
);
}

fn maybe_jsr_package_config(
Copy link
Member

@dsherret dsherret Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe_package_config() on WorkspaceDirectory needs to be the source of truth for JsrPackageConfig. That way, someone using workspace_directory.maybe_package_config() will always get the right information (so, in other words, we should remove this function and make this work in WorkspaceDirectory.maybe_package_config())

For example, right now cli/tools/lint/mod.rs calls workspace_directory.maybe_package_config() and right now in this PR it's not getting the right information.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go back to the drawing table and come back with a suggested solution based on this input. Thanks for the review so far.

sys: &CliSys,
workspace_dir: &WorkspaceDirectoryRc,
strict: bool,
) -> Result<Option<JsrPackageConfig>, AnyError> {
let dir_path = workspace_dir.dir_path();
for file_name in ["jsr.json", "jsr.jsonc"] {
let config_path = dir_path.join(file_name);
match ConfigFile::read(sys, &config_path) {
Ok(config_file) => {
let config_file = new_rc(config_file);
if !config_file.is_package() {
if strict {
if config_file.json.name.is_none() {
bail!("Missing 'name' field in '{}'.", config_file.specifier);
}
error_missing_exports_field(config_file.as_ref())?;
}
continue;
}
let Some(name) = config_file.json.name.clone() else {
if strict {
bail!("Missing 'name' field in '{}'.", config_file.specifier);
} else {
continue;
}
};
return Ok(Some(JsrPackageConfig {
name,
member_dir: workspace_dir.clone(),
config_file: config_file.clone(),
license: config_file.to_license(),
}));
}
Err(err) if err.is_not_found() => continue,
Err(err) => return Err(err.into()),
}
}
Ok(None)
}

#[allow(clippy::print_stderr)]
fn ring_bell() {
// ASCII code for the bell character.
Expand All @@ -1355,13 +1426,25 @@ fn ring_bell() {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fs;

use deno_ast::ModuleSpecifier;

use deno_ast::diagnostics::Diagnostic;
use deno_config::workspace::VendorEnablement;
use deno_config::workspace::WorkspaceDirectory;
use deno_config::workspace::WorkspaceDirectoryEmptyOptions;
use deno_core::url::Url;
use deno_path_util::url_from_directory_path;
use tempfile::tempdir;

use super::PublishDiagnostic;
use super::has_license_file;
use super::maybe_jsr_package_config;
use super::new_rc;
use super::tar::PublishableTarball;
use super::tar::PublishableTarballFile;
use super::verify_version_manifest;
use crate::sys::CliSys;

#[test]
fn test_verify_version_manifest() {
Expand Down Expand Up @@ -1486,4 +1569,73 @@ mod tests {
"file:///test/tLICENSE"
]),);
}

#[test]
fn test_maybe_jsr_package_config_reads_jsr_json() {
let temp_dir = tempdir().unwrap();
let dir_path = temp_dir.path();
fs::write(
dir_path.join("jsr.json"),
r#"{
"name": "@scope/pkg",
"version": "1.2.3",
"exports": "./mod.ts"
}"#,
)
.unwrap();
let workspace_dir =
WorkspaceDirectory::empty(WorkspaceDirectoryEmptyOptions {
root_dir: new_rc(url_from_directory_path(dir_path).unwrap()),
use_vendor_dir: VendorEnablement::Disable,
});
let jsr_package =
maybe_jsr_package_config(&CliSys::default(), &workspace_dir, true)
.unwrap()
.expect("expected jsr package");
assert_eq!(jsr_package.name, "@scope/pkg");
assert!(
jsr_package
.config_file
.specifier
.path()
.ends_with("/jsr.json")
);
}

#[test]
fn test_maybe_jsr_package_config_requires_name() {
let temp_dir = tempdir().unwrap();
let dir_path = temp_dir.path();
fs::write(
dir_path.join("jsr.json"),
r#"{
"exports": "./mod.ts"
}"#,
)
.unwrap();
let workspace_dir =
WorkspaceDirectory::empty(WorkspaceDirectoryEmptyOptions {
root_dir: new_rc(url_from_directory_path(dir_path).unwrap()),
use_vendor_dir: VendorEnablement::Disable,
});
let err =
maybe_jsr_package_config(&CliSys::default(), &workspace_dir, true)
.unwrap_err();
assert!(err.to_string().contains("Missing 'name' field"));
}

#[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"));
}
}
Loading