monochange_deno 0.5.1

Deno workspace and package discovery for monochange
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

use monochange_core::Ecosystem;
use monochange_core::EcosystemAdapter;
use monochange_core::PackageRecord;
use monochange_core::PublishState;
use monochange_core::materialize_dependency_edges;
use semver::Version;
use serde_json::json;
use tempfile::tempdir;

use crate::DenoVersionedFileKind;
use crate::adapter;
use crate::default_lockfile_commands;
use crate::discover_deno_packages;
use crate::discover_lockfiles;
use crate::supported_versioned_file_kind;
use crate::update_lockfile;

#[test]
fn discovers_deno_workspace_packages() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/deno/workspace");
	let discovery = discover_deno_packages(&fixture_root)
		.unwrap_or_else(|error| panic!("deno discovery: {error}"));

	assert_eq!(discovery.packages.len(), 2);
	assert!(
		discovery
			.packages
			.iter()
			.any(|package| package.name == "deno-tool")
	);
	assert!(
		discovery
			.packages
			.iter()
			.any(|package| package.name == "deno-shared")
	);
	let dependency_edges = materialize_dependency_edges(&discovery.packages);
	assert_eq!(dependency_edges.len(), 1);
	assert_eq!(
		dependency_edges.first().unwrap().source_field.as_deref(),
		Some("dependencies")
	);
}

#[test]
fn adapter_reports_deno_ecosystem() {
	assert_eq!(adapter().ecosystem(), Ecosystem::Deno);
}

#[test]
fn supported_versioned_file_kind_recognizes_manifest_and_lockfiles() {
	assert_eq!(
		supported_versioned_file_kind(Path::new("deno.json")),
		Some(DenoVersionedFileKind::Manifest)
	);
	assert_eq!(
		supported_versioned_file_kind(Path::new("deno.jsonc")),
		Some(DenoVersionedFileKind::Manifest)
	);
	assert_eq!(
		supported_versioned_file_kind(Path::new("deno.lock")),
		Some(DenoVersionedFileKind::Lock)
	);
	assert_eq!(supported_versioned_file_kind(Path::new("README.md")), None);
}

#[test]
fn discovers_deno_jsonc_manifests_with_comments() {
	let fixture_root =
		Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/tests/deno/jsonc-manifest");
	let discovery = discover_deno_packages(&fixture_root)
		.unwrap_or_else(|error| panic!("discover deno jsonc: {error}"));
	assert_eq!(discovery.packages.len(), 1);
	let package = discovery.packages.first().expect("discovered deno package");
	assert_eq!(package.name, "jsonc-tool");
	assert_eq!(
		package
			.current_version
			.as_ref()
			.map(ToString::to_string)
			.as_deref(),
		Some("1.0.0")
	);
}

#[test]
fn update_lockfile_rewrites_npm_dependency_versions() {
	let mut lock = json!({
		"packages": {
			"jsr:@scope/pkg": "1.0.0",
			"npm:core@1.0.0": {
				"integrity": "sha512-test"
			},
			"other": "core@1.0.0"
		}
	});
	let versions = BTreeMap::from([("core".to_string(), "2.1.0".to_string())]);

	update_lockfile(&mut lock, &versions);

	let rendered = serde_json::to_string(&lock).unwrap_or_else(|error| panic!("json: {error}"));
	assert!(rendered.contains("npm:core@2.1.0"));
	assert!(rendered.contains("core@2.1.0"));
}

#[test]
fn discover_lockfiles_prefers_workspace_root_then_manifest_directory() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/monochange/deno-lock-release");
	let package = PackageRecord::new(
		Ecosystem::Deno,
		"workflow-app",
		fixture_root.join("packages/app/deno.json"),
		fixture_root.clone(),
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	let lockfiles = discover_lockfiles(&package);
	assert_eq!(lockfiles.len(), 1);
	assert_eq!(
		lockfiles.first(),
		Some(&monochange_core::normalize_path(
			&fixture_root.join("packages/app/deno.lock")
		))
	);
}

#[test]
fn discover_lockfiles_returns_empty_when_no_lockfile_exists() {
	let workspace_root = PathBuf::from("/tmp/deno-workspace");
	let package = PackageRecord::new(
		Ecosystem::Deno,
		"tool",
		workspace_root.join("tools/deno.json"),
		workspace_root,
		None,
		PublishState::Public,
	);
	assert!(discover_lockfiles(&package).is_empty());
}

#[test]
fn default_lockfile_commands_do_not_infer_a_deno_command() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/monochange/deno-lock-release");
	let package = PackageRecord::new(
		Ecosystem::Deno,
		"workflow-app",
		fixture_root.join("packages/app/deno.json"),
		fixture_root,
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	assert!(default_lockfile_commands(&package).is_empty());
}

#[test]
fn default_dependency_version_prefix_is_correct() {
	assert_eq!(super::default_dependency_version_prefix(), "^");
}

#[test]
fn default_dependency_fields_are_non_empty() {
	assert!(!super::default_dependency_fields().is_empty());
}

#[test]
fn validate_versioned_file_accepts_valid_deno_json() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("deno.json");
	fs::write(&path, r#"{"version": "1.0.0"}"#).unwrap_or_else(|error| panic!("write: {error}"));
	assert!(super::validate_versioned_file(&path, "deno.json", None).is_ok());
}

#[test]
fn validate_versioned_file_accepts_custom_field() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("deno.json");
	fs::write(&path, r#"{"customVersion": "1.0.0"}"#)
		.unwrap_or_else(|error| panic!("write: {error}"));
	let custom_fields = vec!["customVersion".to_string()];
	assert!(super::validate_versioned_file(&path, "deno.json", Some(&custom_fields)).is_ok());
}

#[test]
fn validate_versioned_file_rejects_invalid_json() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("deno.json");
	fs::write(&path, "not json").unwrap_or_else(|error| panic!("write: {error}"));
	let result = super::validate_versioned_file(&path, "deno.json", None);
	assert!(result.is_err());
	assert!(result.unwrap_err().to_string().contains("not valid JSON"));
}

#[test]
fn validate_versioned_file_rejects_missing_version() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("deno.json");
	fs::write(&path, r#"{"name": "test"}"#).unwrap_or_else(|error| panic!("write: {error}"));
	let result = super::validate_versioned_file(&path, "deno.json", None);
	assert!(result.is_err());
	assert!(
		result
			.unwrap_err()
			.to_string()
			.contains("does not contain a `version` string field")
	);
}

#[test]
fn validate_versioned_file_rejects_missing_file() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("missing.json");
	let result = super::validate_versioned_file(&path, "missing.json", None);
	assert!(result.is_err());
	assert!(result.unwrap_err().to_string().contains("not readable"));
}