mod util;
use std::{
fs,
path::Path,
process::{Command, Output},
};
use tempfile::TempDir;
use util::{dir_snapshot, write_changeset, write_pre_changeset};
const CHANGELOG: &str = "# ublacklist\n\n## 1.1.0\n\n### Minor Changes\n\n- Add feature\n\n## 1.0.0\n\n### Patch Changes\n\n- Fix bug\n";
fn command(dir: &Path, args: &[&str]) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_changesette"));
command
.args(args)
.current_dir(dir)
.env_remove("CHANGESETTE_ROOT");
command
}
fn changesette(dir: &Path, args: &[&str]) -> Output {
command(dir, args).output().unwrap()
}
fn stdout(output: &Output) -> String {
String::from_utf8(output.stdout.clone()).unwrap()
}
fn stderr(output: &Output) -> String {
String::from_utf8(output.stderr.clone()).unwrap()
}
fn expected_path(dir: &Path, rel: &str) -> String {
let mut path = dunce::canonicalize(dir).unwrap();
path.extend(rel.split('/').filter(|part| !part.is_empty()));
path.display().to_string()
}
fn package_dir() -> TempDir {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\"\n}\n",
)
.unwrap();
dir
}
fn added_path(err: &str) -> &str {
err.lines()
.find_map(|line| line.strip_prefix("Added "))
.unwrap_or_else(|| panic!("unexpected output: {err:?}"))
}
fn assert_changeset_path(line: &str) {
let path = Path::new(line);
assert!(path.is_absolute(), "unexpected path: {line}");
assert_eq!(
path.parent().and_then(Path::file_name),
Some(".changeset".as_ref()),
"unexpected path: {line}"
);
let name = path
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| name.strip_suffix(".md"))
.unwrap_or_else(|| panic!("unexpected path: {line}"));
let words: Vec<&str> = name.split('-').collect();
assert_eq!(words.len(), 3, "unexpected word count: {name}");
assert!(
words
.iter()
.all(|word| !word.is_empty() && word.chars().all(|c| c.is_ascii_lowercase())),
"unexpected name characters: {name}"
);
}
#[test]
fn init_creates_the_changeset_directory_with_a_readme_and_a_config() {
let dir = package_dir();
let output = changesette(dir.path(), &["init"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
stderr(&output),
format!(
"Created {}\nCreated {}\n",
expected_path(dir.path(), ".changeset/README.md"),
expected_path(dir.path(), ".changeset/config.json")
)
);
let readme = fs::read_to_string(dir.path().join(".changeset/README.md")).unwrap();
assert!(readme.starts_with("# Changesets\n"), "{readme}");
let config = fs::read_to_string(dir.path().join(".changeset/config.json")).unwrap();
assert_eq!(
config,
"{\n \"fixed\": [],\n \"linked\": [],\n \"privatePackages\": {\n \"version\": false\n },\n \"ignore\": [],\n \"snapshot\": {\n \"useCalculatedVersion\": false\n }\n}\n"
);
}
#[test]
fn init_creates_the_directory_at_the_workspace_root() {
let dir = workspace_dir();
let output = changesette(&dir.path().join("packages/a"), &["init"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
format!(
"Created {}\nCreated {}\n",
expected_path(dir.path(), ".changeset/README.md"),
expected_path(dir.path(), ".changeset/config.json")
)
);
assert!(dir.path().join(".changeset/README.md").is_file());
assert!(!dir.path().join("packages/a/.changeset").exists());
}
#[test]
fn init_backfills_missing_files_into_an_existing_directory() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
fs::write(dir.path().join(".changeset/README.md"), "custom\n").unwrap();
let output = changesette(dir.path(), &["init"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
stderr(&output),
format!(
"Created {}\n",
expected_path(dir.path(), ".changeset/config.json")
)
);
assert_eq!(
fs::read_to_string(dir.path().join(".changeset/README.md")).unwrap(),
"custom\n"
);
assert!(dir.path().join(".changeset/config.json").is_file());
}
#[test]
fn init_does_nothing_when_everything_exists() {
let dir = package_dir();
let output = changesette(dir.path(), &["init"]);
assert!(output.status.success(), "{}", stderr(&output));
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["init"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
stderr(&output),
format!(
"{} is already initialized\n",
expected_path(dir.path(), ".changeset")
)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn add_with_flags_creates_a_changeset() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&["add", "--minor", "ublacklist", "--message", "Add feature"],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
let err = stderr(&output);
let line = added_path(&err);
assert_changeset_path(line);
let content = fs::read_to_string(dir.path().join(line)).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n\nAdd feature\n");
assert!(err.contains("Summary of changesets:"), "{err}");
assert!(err.contains("minor: ublacklist"), "{err}");
}
#[test]
fn add_quotes_a_scoped_package_name() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"@iorate/ublacklist\",\n \"version\": \"1.2.3\"\n}\n",
)
.unwrap();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&[
"add",
"--minor",
"@iorate/ublacklist",
"--message",
"Add feature",
],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(
content,
"---\n\"@iorate/ublacklist\": minor\n---\n\nAdd feature\n"
);
}
#[test]
fn add_is_the_default_command() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&["--minor", "ublacklist", "--message", "Add feature"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert_changeset_path(added_path(&err));
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n\nAdd feature\n");
}
#[test]
fn add_creates_the_changeset_directory_when_missing() {
let dir = package_dir();
let output = changesette(
dir.path(),
&["add", "--minor", "ublacklist", "--message", "Add feature"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n\nAdd feature\n");
assert!(!dir.path().join(".changeset/README.md").exists());
assert!(!dir.path().join(".changeset/config.json").exists());
}
#[test]
fn add_fails_on_an_invalid_config() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
fs::write(dir.path().join(".changeset/config.json"), "").unwrap();
let output = changesette(
dir.path(),
&["add", "--minor", "ublacklist", "--message", "Add feature"],
);
assert!(!output.status.success());
assert!(
stderr(&output).contains("config.json"),
"{}",
stderr(&output)
);
}
#[test]
fn add_accepts_a_multi_line_message_via_the_short_flag() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&["add", "--patch", "ublacklist", "-m", "line1\nline2"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\nublacklist: patch\n---\n\nline1\nline2\n");
}
#[test]
fn add_records_multiple_packages_in_flag_order() {
let dir = workspace_dir();
for (name, version) in [("pkg-b", "2.0.0"), ("pkg-c", "3.0.0")] {
let member_dir = dir.path().join("packages").join(name);
fs::create_dir_all(&member_dir).unwrap();
fs::write(
member_dir.join("package.json"),
format!("{{\n \"name\": \"{name}\",\n \"version\": \"{version}\"\n}}\n"),
)
.unwrap();
}
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&[
"add",
"--minor",
"pkg-a",
"--patch",
"pkg-c,pkg-b",
"-m",
"Improve things",
],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(
content,
"---\npkg-a: minor\npkg-c: patch\npkg-b: patch\n---\n\nImprove things\n"
);
}
#[test]
fn add_accumulates_repeated_bump_flags() {
let dir = workspace_dir();
let member_dir = dir.path().join("packages/b");
fs::create_dir_all(&member_dir).unwrap();
fs::write(
member_dir.join("package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n",
)
.unwrap();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&[
"add", "--patch", "pkg-a", "--patch", "pkg-b", "-m", "Fix bugs",
],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(
content,
"---\npkg-a: patch\npkg-b: patch\n---\n\nFix bugs\n"
);
}
#[test]
fn add_rejects_an_unknown_package_name() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--minor", "nope", "-m", "Add feature"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("`nope`"), "{err}");
assert!(err.contains("--minor"), "{err}");
assert_eq!(
fs::read_dir(dir.path().join(".changeset")).unwrap().count(),
0
);
}
#[test]
fn add_rejects_a_skipped_package_in_flags() {
let dir = private_two_package_workspace_dir();
let output = changesette(dir.path(), &["add", "--patch", "pkg-b", "-m", "Fix bug"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("`pkg-b`"), "{err}");
assert!(err.contains("skipped"), "{err}");
assert_eq!(
fs::read_dir(dir.path().join(".changeset")).unwrap().count(),
0
);
}
#[test]
fn add_accepts_a_private_package_when_the_config_versions_it() {
let dir = private_two_package_workspace_dir();
fs::create_dir_all(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/config.json"),
"{ \"privatePackages\": { \"version\": true } }\n",
)
.unwrap();
let output = changesette(dir.path(), &["add", "--patch", "pkg-b", "-m", "Fix bug"]);
assert!(output.status.success(), "{}", stderr(&output));
}
#[test]
fn add_fails_without_versionable_packages() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\",\n \"private\": true\n}\n",
)
.unwrap();
let output = changesette(
dir.path(),
&["add", "--patch", "ublacklist", "-m", "Fix bug"],
);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no versionable packages found"),
"{}",
stderr(&output)
);
}
#[test]
fn add_empty_fails_without_versionable_packages() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\",\n \"private\": true\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["add", "--empty", "-m", "Note"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no versionable packages found"),
"{}",
stderr(&output)
);
assert!(!dir.path().join(".changeset").exists());
}
#[test]
fn add_rejects_a_package_passed_to_multiple_bump_flags() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&[
"add",
"--minor",
"ublacklist",
"--patch",
"ublacklist",
"-m",
"Add feature",
],
);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("--minor, --patch"), "{err}");
}
#[test]
fn add_without_message_fails_naming_the_missing_flag() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--minor", "ublacklist"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("--message"), "{err}");
assert!(!err.contains("--major/--minor/--patch"), "{err}");
}
#[test]
fn add_without_bump_flags_fails_naming_them() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "-m", "Add feature"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("--major/--minor/--patch"), "{err}");
assert!(!err.contains("--message"), "{err}");
}
#[test]
fn add_open_fails_in_non_interactive_mode() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["add", "--empty", "-m", "Note", "--open"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(
err.contains("cannot use --open in non-interactive mode"),
"{err}"
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn add_without_any_flags_fails_naming_all_missing_flags() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("--major/--minor/--patch, --message"),
"{}",
stderr(&output)
);
}
#[test]
fn add_accepts_an_empty_message() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--minor", "ublacklist", "-m", ""]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n");
}
#[test]
fn add_rejects_the_removed_bump_flag() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
for flag in ["--bump", "-b"] {
let output = changesette(dir.path(), &["add", flag, "minor", "-m", "Add feature"]);
assert!(!output.status.success(), "{flag} should be rejected");
}
}
#[test]
fn add_empty_creates_an_empty_changeset() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--empty"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert_changeset_path(added_path(&err));
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\n---\n");
assert!(!err.contains("Summary of changesets:"), "{err}");
}
#[test]
fn add_empty_with_a_message_appends_the_summary() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--empty", "-m", "Note only"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let content = fs::read_to_string(dir.path().join(added_path(&err))).unwrap();
assert_eq!(content, "---\n---\n\nNote only\n");
}
#[test]
fn add_empty_conflicts_with_bump_flags() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&[
"add",
"--empty",
"--minor",
"ublacklist",
"-m",
"Add feature",
],
);
assert!(!output.status.success());
assert_eq!(
fs::read_dir(dir.path().join(".changeset")).unwrap().count(),
0
);
}
#[test]
fn add_with_a_major_bump_lists_it_in_the_summary() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
dir.path(),
&["add", "--major", "ublacklist", "-m", "Rework everything"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(err.contains("major: ublacklist"), "{err}");
}
#[test]
fn add_from_a_subdirectory_targets_the_workspace_root() {
let dir = workspace_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(
&dir.path().join("packages/a"),
&["add", "--minor", "pkg-a", "-m", "Add feature"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let line = added_path(&err);
assert_changeset_path(line);
assert!(
line.starts_with(&expected_path(dir.path(), ".changeset")),
"{line}"
);
let content = fs::read_to_string(line).unwrap();
assert_eq!(content, "---\npkg-a: minor\n---\n\nAdd feature\n");
}
#[test]
fn add_fails_in_a_memberless_workspace() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": []\n}\n",
)
.unwrap();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["add", "--empty"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no packages found"),
"{}",
stderr(&output)
);
}
fn workspace_dir() -> TempDir {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/*\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/a")).unwrap();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.1.4\"\n}\n",
)
.unwrap();
dir
}
#[test]
fn get_changelog_entry_without_a_version_fails() {
let dir = package_dir();
fs::write(dir.path().join("CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist"]);
assert!(!output.status.success());
assert!(stderr(&output).contains("<VERSION>"), "{}", stderr(&output));
}
#[test]
fn get_changelog_entry_prints_the_requested_version() {
let dir = package_dir();
fs::write(dir.path().join("CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist", "1.0.0"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "### Patch Changes\n\n- Fix bug\n");
}
#[test]
fn get_changelog_entry_reads_a_workspace_member_changelog() {
let dir = workspace_dir();
fs::write(dir.path().join("packages/a/CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "pkg-a", "1.0.0"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "### Patch Changes\n\n- Fix bug\n");
}
#[test]
fn get_changelog_entry_fails_for_a_missing_version() {
let dir = package_dir();
fs::write(dir.path().join("CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist", "9.9.9"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("CHANGELOG.md: version 9.9.9 not found"),
"{}",
stderr(&output)
);
}
#[test]
fn get_changelog_entry_rejects_an_invalid_version() {
let dir = package_dir();
fs::write(dir.path().join("CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist", "1.0"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("invalid value '1.0'"),
"{}",
stderr(&output)
);
}
#[test]
fn get_changelog_entry_fails_without_a_changelog_file() {
let dir = package_dir();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist", "1.0.0"]);
assert!(!output.status.success());
assert!(stderr(&output).contains("CHANGELOG.md not found"));
}
#[test]
fn set_summary_rewrites_the_summary() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/brave-owls-run.md"),
"---\nublacklist: minor\n---\n\nOld summary\n",
)
.unwrap();
let output = changesette(
dir.path(),
&["set-summary", "brave-owls-run", " New summary "],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
stderr(&output),
format!(
"Updated {}\n",
expected_path(dir.path(), ".changeset/brave-owls-run.md")
)
);
let content = fs::read_to_string(dir.path().join(".changeset/brave-owls-run.md")).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n\nNew summary\n");
}
#[test]
fn set_summary_normalizes_upstream_quoting() {
let dir = package_dir();
write_changeset(
dir.path(),
"quoted.md",
&[("ublacklist", "minor"), ("@iorate/ublacklist", "patch")],
"Old summary",
);
let output = changesette(dir.path(), &["set-summary", "quoted", "New summary"]);
assert!(output.status.success(), "{}", stderr(&output));
let content = fs::read_to_string(dir.path().join(".changeset/quoted.md")).unwrap();
assert_eq!(
content,
"---\nublacklist: minor\n\"@iorate/ublacklist\": patch\n---\n\nNew summary\n"
);
}
#[test]
fn set_summary_keeps_a_none_release() {
let dir = package_dir();
write_changeset(
dir.path(),
"none.md",
&[("ublacklist", "none")],
"Old summary",
);
let output = changesette(dir.path(), &["set-summary", "none", "New summary"]);
assert!(output.status.success(), "{}", stderr(&output));
let content = fs::read_to_string(dir.path().join(".changeset/none.md")).unwrap();
assert_eq!(content, "---\nublacklist: none\n---\n\nNew summary\n");
}
#[test]
fn set_summary_with_an_empty_summary_writes_a_frontmatter_only_file() {
let dir = package_dir();
write_changeset(
dir.path(),
"empty.md",
&[("ublacklist", "minor")],
"Old summary",
);
let output = changesette(dir.path(), &["set-summary", "empty", ""]);
assert!(output.status.success(), "{}", stderr(&output));
let content = fs::read_to_string(dir.path().join(".changeset/empty.md")).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n");
}
#[test]
fn set_summary_keeps_a_multi_line_summary() {
let dir = package_dir();
write_changeset(
dir.path(),
"multi.md",
&[("ublacklist", "minor")],
"Old summary",
);
let output = changesette(
dir.path(),
&["set-summary", "multi", "First line.\n\nSecond line."],
);
assert!(output.status.success(), "{}", stderr(&output));
let content = fs::read_to_string(dir.path().join(".changeset/multi.md")).unwrap();
assert_eq!(
content,
"---\nublacklist: minor\n---\n\nFirst line.\n\nSecond line.\n"
);
}
#[test]
fn set_summary_fails_for_an_unknown_id() {
let dir = package_dir();
write_changeset(
dir.path(),
"known.md",
&[("ublacklist", "minor")],
"Summary",
);
let output = changesette(dir.path(), &["set-summary", "unknown", "New summary"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no changeset with id `unknown`"),
"{}",
stderr(&output)
);
let content = fs::read_to_string(dir.path().join(".changeset/known.md")).unwrap();
assert_eq!(content, "---\n\"ublacklist\": minor\n---\n\nSummary\n");
}
#[test]
fn set_summary_rewrites_a_pre_changeset() {
let dir = package_dir();
write_pre_changeset(
dir.path(),
"early.md",
&[("ublacklist", "minor")],
"Old summary",
);
let output = changesette(dir.path(), &["set-summary", "pre/early", "New summary"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
format!(
"Updated {}\n",
expected_path(dir.path(), ".changeset/pre/early.md")
)
);
let content = fs::read_to_string(dir.path().join(".changeset/pre/early.md")).unwrap();
assert_eq!(content, "---\nublacklist: minor\n---\n\nNew summary\n");
}
#[test]
fn get_packages_prints_the_single_package_with_a_dot_dir() {
let dir = package_dir();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"ublacklist\",\"version\":\"1.2.3\",\"private\":false,\"dir\":\".\"}]\n"
);
}
#[test]
fn get_packages_lists_workspace_members_in_name_order() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"1.0.0\"\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"},{\"name\":\"pkg-b\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"packages/b\"}]\n"
);
}
#[test]
fn get_packages_keeps_dirs_relative_to_the_root_from_a_subdirectory() {
let dir = workspace_dir();
let output = changesette(&dir.path().join("packages/a"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n"
);
}
fn sibling_pattern_dir() -> TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("ws")).unwrap();
fs::write(
dir.path().join("ws/pnpm-workspace.yaml"),
"packages:\n - \"../sibling/*\"\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("sibling/a")).unwrap();
fs::write(
dir.path().join("sibling/a/package.json"),
"{ \"name\": \"pkg-a\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
dir
}
const SIBLING_A_JSON: &str =
"[{\"name\":\"pkg-a\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"../sibling/a\"}]\n";
#[test]
fn get_packages_reports_a_member_outside_the_root() {
let dir = sibling_pattern_dir();
let output = changesette(&dir.path().join("ws"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), SIBLING_A_JSON);
}
#[test]
fn get_packages_reports_a_member_outside_a_relative_root() {
let dir = sibling_pattern_dir();
let output = changesette(dir.path(), &["get-packages", "--root", "ws"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), SIBLING_A_JSON);
}
#[test]
fn a_yarn_root_listed_by_a_parent_pattern_is_expanded_once() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("root/packages/a")).unwrap();
fs::write(dir.path().join("root/yarn.lock"), "").unwrap();
fs::write(
dir.path().join("root/package.json"),
"{ \"workspaces\": [\"../*\", \"packages/*\"] }\n",
)
.unwrap();
fs::write(
dir.path().join("root/packages/a/package.json"),
"{ \"name\": \"pkg-a\", \"version\": \"1.0\" }\n",
)
.unwrap();
let output = changesette(&dir.path().join("root"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
assert_eq!(
stderr(&output),
format!(
"warning: {}: not a workspace member: \"version\" \"1.0\" is not a valid semver\n",
expected_path(dir.path(), "root/packages/a/package.json")
)
);
}
#[test]
fn version_fails_for_a_changeset_naming_an_excluded_package() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\"\n}\n",
)
.unwrap();
write_changeset(dir.path(), "a.md", &[("ublacklist", "patch")], "Fix bug");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--log-level", "debug"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(
err.contains(&format!(
"debug: {}: not a workspace member: \"version\" is missing",
expected_path(dir.path(), "package.json")
)),
"{err}"
);
assert!(err.contains("`ublacklist` not found"), "{err}");
assert_eq!(dir_snapshot(dir.path()), before);
}
fn mixed_workspace_dir() -> TempDir {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"1.0.0\",\n \"private\": true\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/c")).unwrap();
fs::write(
dir.path().join("packages/c/package.json"),
"{\n \"name\": \"pkg-c\",\n \"version\": \"2.0.0\",\n \"private\": false\n}\n",
)
.unwrap();
dir
}
#[test]
fn get_packages_excludes_skipped_packages_by_default() {
let dir = mixed_workspace_dir();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"},{\"name\":\"pkg-c\",\"version\":\"2.0.0\",\"private\":false,\"dir\":\"packages/c\"}]\n"
);
}
#[test]
fn get_packages_all_lists_every_member() {
let dir = mixed_workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--all"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"},{\"name\":\"pkg-b\",\"version\":\"1.0.0\",\"private\":true,\"dir\":\"packages/b\"},{\"name\":\"pkg-c\",\"version\":\"2.0.0\",\"private\":false,\"dir\":\"packages/c\"}]\n"
);
}
#[cfg(unix)]
#[test]
fn get_packages_warns_about_filesystem_errors_but_not_plain_absence() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/**\", \"docs/pkg\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/a")).unwrap();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.1.4\"\n}\n",
)
.unwrap();
fs::write(dir.path().join("docs"), "not a directory\n").unwrap();
std::os::unix::fs::symlink("missing", dir.path().join("packages/broken")).unwrap();
fs::create_dir_all(dir.path().join("packages/denied")).unwrap();
fs::set_permissions(
dir.path().join("packages/denied"),
fs::Permissions::from_mode(0o000),
)
.unwrap();
let output = changesette(dir.path(), &["get-packages", "--all"]);
fs::set_permissions(
dir.path().join("packages/denied"),
fs::Permissions::from_mode(0o755),
)
.unwrap();
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n"
);
let err = stderr(&output);
let denied = dir.path().canonicalize().unwrap().join("packages/denied");
assert!(
err.contains(&format!(
"warning: {}: Permission denied",
denied.join("package.json").display()
)),
"{err}"
);
assert!(
err.contains(&format!("warning: {}: Permission denied", denied.display())),
"{err}"
);
assert_eq!(err.matches("warning:").count(), 2, "{err}");
}
#[test]
fn get_packages_warns_about_excluded_candidates_and_omits_them() {
let dir = workspace_dir();
for (name, manifest) in [
("b", "{ \"version\": \"1.0.0\" }\n"),
("c", "{ \"name\": \"pkg-c\" }\n"),
("d", "{ \"name\": \"pkg-d\", \"version\": \"1.0\" }\n"),
("e", "{ \"name\": \"dup\", \"version\": \"1.0.0\" }\n"),
("f", "{ \"name\": \"dup\", \"version\": \"2.0.0\" }\n"),
("g", "{ \"name\": 1, \"version\": \"1.0.0\" }\n"),
("h", "{ \"name\": \"\", \"version\": \"1.0.0\" }\n"),
("i", "{ \"name\": \"pkg-i\", \"version\": 1 }\n"),
] {
fs::create_dir_all(dir.path().join("packages").join(name)).unwrap();
fs::write(
dir.path().join("packages").join(name).join("package.json"),
manifest,
)
.unwrap();
}
let output = changesette(dir.path(), &["get-packages", "--all"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n"
);
let err = stderr(&output);
let manifest = |name: &str| expected_path(dir.path(), &format!("packages/{name}/package.json"));
assert!(!err.contains(&manifest("b")), "{err}");
assert!(!err.contains(&manifest("c")), "{err}");
assert!(
err.contains(&format!(
"warning: {}: not a workspace member: \"version\" \"1.0\" is not a valid semver",
manifest("d")
)),
"{err}"
);
assert!(err.contains(&manifest("e")), "{err}");
assert!(err.contains(&manifest("f")), "{err}");
assert!(err.contains("`dup`"), "{err}");
assert!(
err.contains(&format!(
"warning: {}: not a workspace member: \"name\" is not a string",
manifest("g")
)),
"{err}"
);
assert!(
err.contains(&format!(
"warning: {}: not a workspace member: \"name\" is an empty string",
manifest("h")
)),
"{err}"
);
assert!(
err.contains(&format!(
"warning: {}: not a workspace member: \"version\" is not a string",
manifest("i")
)),
"{err}"
);
let output = changesette(
dir.path(),
&["get-packages", "--all", "--log-level", "debug"],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains(&format!(
"debug: {}: not a workspace member: \"name\" is missing",
manifest("b")
)),
"{err}"
);
assert!(
err.contains(&format!(
"debug: {}: not a workspace member: \"version\" is missing",
manifest("c")
)),
"{err}"
);
}
#[test]
fn get_packages_debug_reports_the_member_list() {
let dir = workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains(&format!(
"debug: workspace {} (npm): members: pkg-a (packages/a)",
expected_path(dir.path(), "")
)),
"{err}"
);
}
#[test]
fn get_packages_debug_reports_an_empty_member_list() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("package.json"), "{}\n").unwrap();
let output = changesette(
dir.path(),
&["get-packages", "--all", "--log-level", "debug"],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
let err = stderr(&output);
assert!(
err.contains(&format!(
"debug: workspace {} (single package): no members",
expected_path(dir.path(), "")
)),
"{err}"
);
}
#[test]
fn get_packages_debug_reports_skip_reasons() {
let dir = private_two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-a\"] }\n");
let output = changesette(dir.path(), &["get-packages", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
let err = stderr(&output);
assert!(err.contains("debug: `pkg-a` is skipped: ignored"), "{err}");
assert!(err.contains("debug: `pkg-b` is skipped: private"), "{err}");
}
#[test]
fn get_packages_debug_reports_a_negation_exclusion() {
let dir = workspace_dir();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/*\", \"!packages/b\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{ \"name\": \"pkg-b\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(!stdout(&output).contains("pkg-b"), "{}", stdout(&output));
let err = stderr(&output);
assert!(
err.contains("debug: packages/b: excluded by a negative workspace pattern"),
"{err}"
);
}
#[cfg(unix)]
#[test]
fn get_packages_debug_reports_a_symlink_not_entered_by_a_double_star() {
let dir = workspace_dir();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/**\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("target")).unwrap();
fs::write(
dir.path().join("target/package.json"),
"{ \"name\": \"pkg-t\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
std::os::unix::fs::symlink("../target", dir.path().join("packages/link")).unwrap();
let output = changesette(dir.path(), &["get-packages", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(!stdout(&output).contains("pkg-t"), "{}", stdout(&output));
let err = stderr(&output);
assert!(
err.contains("debug: packages/link: a symlinked directory is not entered by `**`"),
"{err}"
);
}
#[test]
fn get_packages_treats_an_escaped_slash_pattern_as_a_path() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"a\\\\/b\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("a/b")).unwrap();
fs::write(
dir.path().join("a/b/package.json"),
"{ \"name\": \"pkg-ab\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-ab\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"a/b\"}]\n"
);
}
#[test]
fn get_packages_expands_a_brace_alternative_holding_a_slash() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/{a,b/c}\"]\n}\n",
)
.unwrap();
for (rel, name) in [
("packages/a", "pkg-a"),
("packages/b", "pkg-b"),
("packages/b/c", "pkg-bc"),
] {
fs::create_dir_all(dir.path().join(rel)).unwrap();
fs::write(
dir.path().join(rel).join("package.json"),
format!("{{ \"name\": \"{name}\", \"version\": \"1.0.0\" }}\n"),
)
.unwrap();
}
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"packages/a\"},{\"name\":\"pkg-bc\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"packages/b/c\"}]\n"
);
}
#[test]
fn get_packages_applies_leading_bang_parity() {
let dir = workspace_dir();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"!!packages/a\"]\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n"
);
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/*\", \"!!!packages/a\"]\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
}
#[test]
fn get_packages_includes_private_packages_when_the_config_versions_them() {
let dir = private_two_package_workspace_dir();
fs::create_dir_all(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/config.json"),
"{ \"privatePackages\": { \"version\": true } }\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(stdout(&output).contains("pkg-b"), "{}", stdout(&output));
}
#[test]
fn get_packages_prints_an_empty_array_when_every_member_is_skipped() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\",\n \"private\": true\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
}
#[test]
fn get_packages_fails_on_an_invalid_config() {
let dir = package_dir();
fs::create_dir_all(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/config.json"),
"{ \"privatePackages\": \"all\" }\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("privatePackages"),
"{}",
stderr(&output)
);
}
#[test]
fn get_packages_prints_an_empty_array_for_a_memberless_workspace() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": []\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "[]\n");
}
#[test]
fn get_packages_fails_without_package_json() {
let dir = tempfile::tempdir().unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
}
#[test]
fn the_old_current_subcommand_is_rejected() {
let dir = package_dir();
let output = changesette(dir.path(), &["current"]);
assert!(!output.status.success());
}
#[test]
fn the_old_changelog_subcommand_is_rejected() {
let dir = package_dir();
fs::write(dir.path().join("CHANGELOG.md"), CHANGELOG).unwrap();
let output = changesette(dir.path(), &["changelog", "1.0.0"]);
assert!(!output.status.success());
}
const ULID_A: &str = "changesette-01H455VB4PEX5VSKNK084SN02Q.md";
const ULID_B: &str = "changesette-01H455WZ0H1X9PE0QB0MV1P1KG.md";
const ID_A: &str = "changesette-01H455VB4PEX5VSKNK084SN02Q";
const ID_B: &str = "changesette-01H455WZ0H1X9PE0QB0MV1P1KG";
#[test]
fn version_with_zero_changesets_fails_and_touches_nothing() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert_eq!(stdout(&output), "");
assert!(
stderr(&output).contains("no unreleased changesets found"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_treats_a_missing_changeset_directory_as_empty() {
let dir = package_dir();
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no unreleased changesets found"),
"{}",
stderr(&output)
);
assert!(!dir.path().join(".changeset").exists());
}
#[test]
fn version_fails_on_an_invalid_config() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/config.json"),
"{ \"ignore\": \"pkg\" }\n",
)
.unwrap();
write_changeset(dir.path(), "a.md", &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("config.json: \"ignore\" must be an array of strings"),
"{}",
stderr(&output)
);
}
#[test]
fn status_treats_a_missing_changeset_directory_as_empty() {
let dir = package_dir();
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "Packages to be bumped:\n");
assert!(!dir.path().join(".changeset").exists());
}
#[test]
fn version_allow_no_changesets_prints_a_notice_and_touches_nothing() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--allow-no-changesets"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(stderr(&output), "No unreleased changesets found.\n");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_output_with_zero_changesets_fails_without_writing_the_plan() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["version", "--output", "plan.json"]);
assert!(!output.status.success());
assert!(!dir.path().join("plan.json").exists());
}
#[test]
fn version_allow_no_changesets_output_writes_an_empty_plan() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["version", "-a", "--output", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(stderr(&output), "Wrote the release plan to plan.json\n");
assert_eq!(
fs::read_to_string(dir.path().join("plan.json")).unwrap(),
"{\n \"changesets\": [],\n \"releases\": []\n}\n"
);
}
#[test]
fn version_bumps_and_writes_the_changelog() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
fs::write(dir.path().join(".changeset/README.md"), "# Changesets\n").unwrap();
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(stderr(&output), "Bumped ublacklist 1.2.3 -> 1.3.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.3.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0\n\n### Minor Changes\n\n- Add feature\n"
);
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
assert!(dir.path().join(".changeset/README.md").exists());
}
#[test]
fn version_writes_an_empty_release_line_for_an_empty_summary() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[("ublacklist", "minor")], "");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0\n\n### Minor Changes\n\n- \n"
);
}
#[test]
fn version_leaves_the_package_lock_untouched() {
let dir = package_dir();
let package_lock = "{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\",\n \"lockfileVersion\": 3,\n \"packages\": {\n \"\": {\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\"\n }\n }\n}\n";
fs::write(dir.path().join("package-lock.json"), package_lock).unwrap();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.3.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("package-lock.json")).unwrap(),
package_lock
);
}
#[test]
fn version_uses_the_max_bump_across_changesets() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "major")],
"Rework everything",
);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version", "--output", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(plan.contains(ID_A), "{plan}");
assert!(plan.contains(ID_B), "{plan}");
assert!(plan.contains("\"newVersion\": \"2.0.0\""), "{plan}");
let changelog = fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap();
assert!(changelog.contains("## 2.0.0"), "{changelog}");
assert!(changelog.contains("### Major Changes"), "{changelog}");
assert!(changelog.contains("### Patch Changes"), "{changelog}");
}
#[test]
fn version_bumps_only_the_named_workspace_members() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/c")).unwrap();
let untouched = "{\n \"name\": \"pkg-c\",\n \"version\": \"3.0.0\"\n}\n";
fs::write(dir.path().join("packages/c/package.json"), untouched).unwrap();
write_changeset(
dir.path(),
ULID_B,
&[("pkg-b", "patch"), ("pkg-a", "minor")],
"Improve things",
);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Bumped pkg-a 3.1.4 -> 3.2.0\nBumped pkg-b 2.0.0 -> 2.0.1\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.2.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/CHANGELOG.md")).unwrap(),
"# pkg-a\n\n## 3.2.0\n\n### Minor Changes\n\n- Improve things\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.1\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/CHANGELOG.md")).unwrap(),
"# pkg-b\n\n## 2.0.1\n\n### Patch Changes\n\n- Improve things\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/c/package.json")).unwrap(),
untouched
);
assert!(!dir.path().join("packages/c/CHANGELOG.md").exists());
assert!(!dir.path().join("CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_consumes_a_none_only_changeset_without_bumping() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "-o", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(plan.contains("\"type\": \"none\""), "{plan}");
assert!(plan.contains("\"newVersion\": \"1.2.3\""), "{plan}");
assert!(!plan.contains("changelogEntry"), "{plan}");
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
String::from_utf8(before["package.json"].clone()).unwrap()
);
assert!(!dir.path().join("CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_consumes_an_empty_changeset() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[], "");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert!(!dir.path().join("CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_fails_on_a_validation_error_leaving_the_tree_untouched() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset").join(ULID_B),
"---\n\"other-package\": minor\n---\n\nAdd feature\n",
)
.unwrap();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_rerun_replaces_the_same_section() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let package_json = fs::read_to_string(dir.path().join("package.json")).unwrap();
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
let changelog = fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap();
fs::write(dir.path().join("package.json"), package_json).unwrap();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
changelog
);
}
fn pretty_plan(id: &str) -> String {
format!(
concat!(
"{{\n",
" \"changesets\": [\n",
" {{\n",
" \"id\": \"{0}\",\n",
" \"summary\": \"Add feature\",\n",
" \"releases\": [\n",
" {{\n",
" \"name\": \"ublacklist\",\n",
" \"type\": \"minor\"\n",
" }}\n",
" ]\n",
" }}\n",
" ],\n",
" \"releases\": [\n",
" {{\n",
" \"name\": \"ublacklist\",\n",
" \"type\": \"minor\",\n",
" \"oldVersion\": \"1.2.3\",\n",
" \"newVersion\": \"1.3.0\",\n",
" \"changesets\": [\n",
" \"{0}\"\n",
" ],\n",
" \"changelogEntry\": \"### Minor Changes\\n\\n- Add feature\"\n",
" }}\n",
" ]\n",
"}}\n"
),
id
)
}
fn compact_plan(id: &str) -> String {
format!(
concat!(
"{{\"changesets\":[{{\"id\":\"{0}\",\"summary\":\"Add feature\",",
"\"releases\":[{{\"name\":\"ublacklist\",\"type\":\"minor\"}}]}}],",
"\"releases\":[{{\"name\":\"ublacklist\",\"type\":\"minor\",",
"\"oldVersion\":\"1.2.3\",\"newVersion\":\"1.3.0\",",
"\"changesets\":[\"{0}\"],",
"\"changelogEntry\":\"### Minor Changes\\n\\n- Add feature\"}}]}}"
),
id
)
}
#[test]
fn version_output_writes_the_pretty_plan_and_applies_the_changesets() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--output", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
fs::read_to_string(dir.path().join("plan.json")).unwrap(),
pretty_plan(ID_B)
);
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.3.0\"\n}\n"
);
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_output_dash_writes_the_compact_plan_to_stdout() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--output", "-"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), compact_plan(ID_B) + "\n");
assert_eq!(stderr(&output), "");
assert!(!dir.path().join("-").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
fn two_package_workspace_dir() -> TempDir {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n",
)
.unwrap();
dir
}
fn write_config(dir: &Path, text: &str) {
fs::create_dir_all(dir.join(".changeset")).unwrap();
fs::write(dir.join(".changeset/config.json"), text).unwrap();
}
fn private_two_package_workspace_dir() -> TempDir {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\",\n \"private\": true\n}\n",
)
.unwrap();
dir
}
#[test]
fn version_ignore_skips_the_package_and_keeps_its_changeset() {
let dir = two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version", "--ignore", "pkg-b"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.2.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.2.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n"
);
assert!(!dir.path().join("packages/b/CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_ignore_includes_the_skipped_changeset_in_the_plan_without_a_release() {
let dir = two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(
dir.path(),
&["version", "--ignore", "pkg-b", "-o", "plan.json"],
);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(plan.contains(ID_A), "{plan}");
assert!(plan.contains(ID_B), "{plan}");
assert!(plan.contains("pkg-b"), "{plan}");
assert!(!plan.contains("2.0.0"), "{plan}");
}
#[test]
fn version_ignore_accepts_comma_separated_packages() {
let dir = two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "pkg-a,pkg-b"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_rejects_an_unknown_package() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "other-package"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("--ignore"), "{err}");
assert!(err.contains("other-package"), "{err}");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_may_be_repeated() {
let dir = two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let before = dir_snapshot(dir.path());
let output = changesette(
dir.path(),
&["version", "--ignore", "pkg-a", "--ignore", "pkg-b"],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_skips_a_none_only_changeset() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "ublacklist"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_rejects_a_mixed_changeset_with_a_none_release() {
let dir = two_package_workspace_dir();
write_changeset(
dir.path(),
ULID_B,
&[("pkg-a", "minor"), ("pkg-b", "none")],
"Improve things",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "pkg-a"]);
assert!(!output.status.success());
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_rejects_a_mixed_changeset() {
let dir = two_package_workspace_dir();
write_changeset(
dir.path(),
ULID_B,
&[("pkg-a", "minor"), ("pkg-b", "patch")],
"Improve things",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "pkg-a"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains(ULID_B), "{err}");
assert!(err.contains("pkg-a"), "{err}");
assert!(err.contains("pkg-b"), "{err}");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_skips_a_config_ignored_package_and_keeps_its_changeset() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-b\"] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.2.0\n");
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_resolves_config_ignore_globs_with_negation() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-*\", \"!pkg-a\"] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.2.0\n");
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_rejects_the_ignore_flag_with_a_config_ignore() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-b\"] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "pkg-a"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("use only one of them"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_rejects_the_ignore_flag_when_the_config_ignore_matches_nothing() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"missing-*\"] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--ignore", "pkg-b"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("use only one of them"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_ignore_flag_works_with_an_empty_config_ignore() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version", "--ignore", "pkg-b"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.2.0\n");
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn status_omits_a_config_ignored_package() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-b\"] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"Packages to be bumped:\n- minor\n - pkg-a\n"
);
}
#[test]
fn add_rejects_a_config_ignored_package_in_flags() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-b\"] }\n");
let output = changesette(dir.path(), &["add", "--patch", "pkg-b", "-m", "Fix bug"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("`pkg-b`"), "{err}");
assert!(err.contains("skipped"), "{err}");
}
#[test]
fn get_packages_excludes_a_config_ignored_package() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"ignore\": [\"pkg-b\"] }\n");
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n"
);
}
#[test]
fn version_skips_a_private_package_and_keeps_its_changeset() {
let dir = private_two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.2.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\",\n \"private\": true\n}\n"
);
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_bumps_a_private_package_when_the_config_versions_it() {
let dir = private_two_package_workspace_dir();
fs::create_dir_all(dir.path().join(".changeset")).unwrap();
fs::write(
dir.path().join(".changeset/config.json"),
"{ \"privatePackages\": true }\n",
)
.unwrap();
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-b 2.0.0 -> 2.0.1\n");
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_fixed_bumps_the_partner_with_a_heading_only_changelog() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Bumped pkg-a 3.1.4 -> 3.2.0\nBumped pkg-b 3.1.4 -> 3.2.0\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.2.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/CHANGELOG.md")).unwrap(),
"# pkg-a\n\n## 3.2.0\n\n### Minor Changes\n\n- Improve pkg-a\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"3.2.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/CHANGELOG.md")).unwrap(),
"# pkg-b\n\n## 3.2.0\n"
);
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
}
#[test]
fn version_linked_does_not_bump_a_non_releasing_member() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-b 3.1.4 -> 3.1.5\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.1.4\"\n}\n"
);
assert!(!dir.path().join("packages/a/CHANGELOG.md").exists());
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"3.1.5\"\n}\n"
);
}
#[test]
fn version_linked_aligns_the_releasing_members() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "minor")], "Improve pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Bumped pkg-a 3.1.4 -> 3.2.0\nBumped pkg-b 3.1.4 -> 3.2.0\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"3.2.0\"\n}\n"
);
}
#[test]
fn version_fixed_debug_reports_the_raised_bump() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let output = changesette(dir.path(), &["version", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains(
"debug: `pkg-b`: the \"fixed\" group raises the bump to minor (planning against 3.1.4)"
),
"{err}"
);
}
#[test]
fn version_linked_debug_reports_the_raised_bump() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "minor")], "Improve pkg-b");
let output = changesette(dir.path(), &["version", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains(
"debug: `pkg-a`: the \"linked\" group raises the bump to minor (planning against 3.1.4)"
),
"{err}"
);
}
#[test]
fn version_fixed_counts_a_skipped_member_without_adding_it() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"9.9.9\",\n \"private\": true\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 9.9.9 -> 9.10.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"9.10.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"9.9.9\",\n \"private\": true\n}\n"
);
assert!(!dir.path().join("packages/b/CHANGELOG.md").exists());
}
#[test]
fn version_fixed_in_pre_mode_aligns_the_counter() {
let dir = workspace_dir();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"1.0.1-beta.7\"\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.2\"\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
fs::write(dir.path().join(".changeset/pre.json"), PRE_JSON).unwrap();
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains("Bumped pkg-a 2.0.0-beta.2 -> 2.0.0-beta.8\n"),
"{err}"
);
assert!(
err.contains("Bumped pkg-b 2.0.0-beta.2 -> 2.0.0-beta.8\n"),
"{err}"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"2.0.0-beta.8\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.8\"\n}\n"
);
assert!(dir.path().join(".changeset/pre").join(ULID_B).exists());
}
#[test]
fn version_fixed_output_reports_the_overridden_old_version() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let output = changesette(dir.path(), &["version", "--output", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(
plan.contains(
" {\n \"name\": \"pkg-b\",\n \"type\": \"minor\",\n \"oldVersion\": \"3.1.4\",\n \"newVersion\": \"3.2.0\",\n \"changesets\": [],\n \"changelogEntry\": \"\"\n }"
),
"{plan}"
);
}
#[test]
fn version_warns_on_an_unmatched_group_pattern() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"missing-*\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"warning: fixed: the package or glob \"missing-*\" does not match any package in the workspace\nBumped pkg-a 3.1.4 -> 3.1.5\n"
);
}
#[test]
fn version_log_level_warn_keeps_warnings_and_drops_info() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"missing-*\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let output = changesette(dir.path(), &["version", "--log-level", "warn"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"warning: fixed: the package or glob \"missing-*\" does not match any package in the workspace\n"
);
}
#[test]
fn version_log_level_error_drops_warnings_but_reports_failures() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"missing-*\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let output = changesette(dir.path(), &["version", "--log-level", "error"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "");
let output = changesette(dir.path(), &["version", "--log-level", "error"]);
assert!(!output.status.success());
assert!(
stderr(&output).starts_with("error: "),
"{}",
stderr(&output)
);
}
#[test]
fn status_warns_on_an_unmatched_group_pattern() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"linked\": [[\"missing-*\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"warning: linked: the package or glob \"missing-*\" does not match any package in the workspace\n"
);
assert_eq!(
stdout(&output),
"Packages to be bumped:\n- patch\n - pkg-a\n"
);
}
#[test]
fn version_rejects_a_package_in_both_fixed_and_linked_groups() {
let dir = two_package_workspace_dir();
write_config(
dir.path(),
"{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]], \"linked\": [[\"pkg-b\"]] }\n",
);
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains(".changeset"), "{err}");
assert!(err.contains("`pkg-b`"), "{err}");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_linked_in_pre_mode_aligns_the_counter() {
let dir = workspace_dir();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"1.0.1-beta.7\"\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.2\"\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
fs::write(dir.path().join(".changeset/pre.json"), PRE_JSON).unwrap();
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains("Bumped pkg-b 2.0.0-beta.2 -> 2.0.0-beta.8\n"),
"{err}"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"1.0.1-beta.7\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.8\"\n}\n"
);
}
#[test]
fn version_after_exit_rescues_a_linked_partner_without_a_prerelease() {
let dir = two_package_workspace_dir();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.1-beta.4\"\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
fs::write(dir.path().join(".changeset/pre.json"), EXITED_PRE_JSON).unwrap();
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Bumped pkg-a 3.1.4 -> 3.1.5\nBumped pkg-b 2.0.1-beta.4 -> 2.0.1\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/CHANGELOG.md")).unwrap(),
"# pkg-a\n\n## 3.1.5\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/CHANGELOG.md")).unwrap(),
"# pkg-b\n\n## 2.0.1\n"
);
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_after_exit_rescues_the_fixed_group_of_a_skipped_prerelease() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.1\",\n \"private\": true\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
fs::write(dir.path().join(".changeset/pre.json"), EXITED_PRE_JSON).unwrap();
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.1.5\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/CHANGELOG.md")).unwrap(),
"# pkg-a\n\n## 3.1.5\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0-beta.1\",\n \"private\": true\n}\n"
);
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_fixed_in_pre_mode_counts_a_skipped_member_counter() {
let dir = workspace_dir();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"2.0.0-beta.2\"\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"1.0.1-beta.7\",\n \"private\": true\n}\n",
)
.unwrap();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
fs::write(dir.path().join(".changeset/pre.json"), PRE_JSON).unwrap();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains("Bumped pkg-a 2.0.0-beta.2 -> 2.0.0-beta.8\n"),
"{err}"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"1.0.1-beta.7\",\n \"private\": true\n}\n"
);
}
#[test]
fn version_linked_leaves_a_none_only_member_unchanged() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"linked\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "patch")], "Fix pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "none")], "Note only");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.1.4 -> 3.1.5\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n"
);
assert!(!dir.path().join("packages/b/CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_fixed_upgrades_a_none_only_member() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "none")], "Note only");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Bumped pkg-a 3.1.4 -> 3.2.0\nBumped pkg-b 3.1.4 -> 3.2.0\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/CHANGELOG.md")).unwrap(),
"# pkg-b\n\n## 3.2.0\n"
);
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_fixed_is_not_triggered_by_a_none_only_member() {
let dir = two_package_workspace_dir();
write_config(dir.path(), "{ \"fixed\": [[\"pkg-a\", \"pkg-b\"]] }\n");
write_changeset(dir.path(), ULID_A, &[("pkg-a", "none")], "Note only");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "No packages to bump.\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.1.4\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n"
);
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
}
#[test]
fn version_rejects_a_changeset_mixing_skipped_and_not_skipped_packages() {
let dir = private_two_package_workspace_dir();
write_changeset(
dir.path(),
ULID_B,
&[("pkg-a", "minor"), ("pkg-b", "patch")],
"Improve things",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("cannot mix skipped packages"), "{err}");
assert!(err.contains("`pkg-a`"), "{err}");
assert!(err.contains("`pkg-b`"), "{err}");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_succeeds_when_every_changeset_is_skipped() {
let dir = private_two_package_workspace_dir();
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "No packages to bump.\n");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_output_includes_a_skipped_changeset_without_a_release() {
let dir = private_two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version", "--output", "-"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
format!(
concat!(
"{{\"changesets\":[",
"{{\"id\":\"{0}\",\"summary\":\"Improve pkg-a\",",
"\"releases\":[{{\"name\":\"pkg-a\",\"type\":\"minor\"}}]}},",
"{{\"id\":\"{1}\",\"summary\":\"Fix pkg-b\",",
"\"releases\":[{{\"name\":\"pkg-b\",\"type\":\"patch\"}}]}}],",
"\"releases\":[{{\"name\":\"pkg-a\",\"type\":\"minor\",",
"\"oldVersion\":\"3.1.4\",\"newVersion\":\"3.2.0\",",
"\"changesets\":[\"{0}\"],",
"\"changelogEntry\":\"### Minor Changes\\n\\n- Improve pkg-a\"}}]}}\n"
),
ID_A, ID_B
)
);
assert!(!dir.path().join(".changeset").join(ULID_A).exists());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_excludes_a_member_with_an_invalid_version_and_bumps_the_rest() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"next\"\n}\n",
)
.unwrap();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
assert!(
err.contains(&expected_path(dir.path(), "packages/b/package.json")),
"{err}"
);
assert!(err.ends_with("Bumped pkg-a 3.1.4 -> 3.2.0\n"), "{err}");
}
#[test]
fn get_packages_treats_a_non_boolean_private_as_not_private() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\",\n \"private\": \"true\"\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"ublacklist\",\"version\":\"1.2.3\",\"private\":false,\"dir\":\".\"}]\n"
);
}
#[test]
fn version_in_pre_mode_leaves_a_skipped_changeset_in_place() {
let dir = private_two_package_workspace_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(dir.path().join(".changeset/pre").join(ULID_A).exists());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
assert!(!dir.path().join(".changeset/pre").join(ULID_B).exists());
}
#[test]
fn version_after_exit_does_not_rescue_a_skipped_prerelease() {
let dir = private_two_package_workspace_dir();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.1.0-beta.0\",\n \"private\": true\n}\n",
)
.unwrap();
write_pre_json(dir.path(), EXITED_PRE_JSON);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.1.0-beta.0\",\n \"private\": true\n}\n"
);
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn status_omits_skipped_packages() {
let dir = private_two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"Packages to be bumped:\n- minor\n - pkg-a\n"
);
}
#[test]
fn status_rejects_a_changeset_mixing_skipped_and_not_skipped_packages() {
let dir = private_two_package_workspace_dir();
write_changeset(
dir.path(),
ULID_B,
&[("pkg-a", "minor"), ("pkg-b", "patch")],
"Improve things",
);
let output = changesette(dir.path(), &["status"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("cannot mix skipped packages"),
"{}",
stderr(&output)
);
}
#[test]
fn version_rejects_the_removed_dry_run_flag() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
for flag in ["--dry-run", "-n"] {
let output = changesette(dir.path(), &["version", flag]);
assert!(!output.status.success(), "{flag} should be rejected");
}
}
#[test]
fn status_lists_packages_grouped_by_bump_without_modifying_files() {
let dir = workspace_dir();
fs::create_dir_all(dir.path().join("packages/b")).unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n",
)
.unwrap();
write_changeset(dir.path(), ULID_A, &[("pkg-b", "major")], "Rework");
write_changeset(dir.path(), ULID_B, &[("pkg-a", "minor")], "Add feature");
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"Packages to be bumped:\n- major\n - pkg-b\n- minor\n - pkg-a\n"
);
assert_eq!(stderr(&output), "");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn status_verbose_adds_versions_and_changeset_files() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "minor")],
"Add feature",
);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
for flag in ["--verbose", "-v"] {
let output = changesette(dir.path(), &["status", flag]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
format!(
"Packages to be bumped:\n- minor\n - ublacklist -> 1.3.0\n - .changeset/{ULID_A}\n - .changeset/{ULID_B}\n"
)
);
}
}
#[test]
fn status_omits_none_only_packages_from_the_listing() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "Packages to be bumped:\n");
}
#[test]
fn status_with_zero_changesets_prints_only_the_heading() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["status"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "Packages to be bumped:\n");
assert_eq!(stderr(&output), "");
}
#[test]
fn status_output_writes_the_pretty_plan_without_modifying_files() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["status", "--output", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
fs::read_to_string(dir.path().join("plan.json")).unwrap(),
pretty_plan(ID_B)
);
fs::remove_file(dir.path().join("plan.json")).unwrap();
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn status_output_dash_writes_the_compact_plan_to_stdout() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["status", "-o", "-"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), compact_plan(ID_B) + "\n");
assert_eq!(stderr(&output), "");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn status_output_with_zero_changesets_writes_an_empty_plan() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let output = changesette(dir.path(), &["status", "-o", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
fs::read_to_string(dir.path().join("plan.json")).unwrap(),
"{\n \"changesets\": [],\n \"releases\": []\n}\n"
);
}
const PRE_JSON: &str = "{\n \"mode\": \"pre\",\n \"tag\": \"beta\"\n}\n";
const EXITED_PRE_JSON: &str = "{\n \"mode\": \"exit\",\n \"tag\": \"beta\"\n}\n";
fn write_pre_json(dir: &Path, text: &str) {
let changeset_dir = dir.join(".changeset");
fs::create_dir_all(&changeset_dir).unwrap();
fs::write(changeset_dir.join("pre.json"), text).unwrap();
}
fn read_pre_json(dir: &Path) -> String {
fs::read_to_string(dir.join(".changeset/pre.json")).unwrap()
}
fn prerelease_package_dir(version: &str) -> TempDir {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
format!("{{\n \"name\": \"ublacklist\",\n \"version\": \"{version}\"\n}}\n"),
)
.unwrap();
dir
}
#[test]
fn pre_enter_creates_pre_json() {
let dir = package_dir();
let output = changesette(dir.path(), &["pre", "enter", "beta"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
stderr(&output),
"Entered pre mode with tag `beta`\nRun `changesette version` to bump to prerelease versions\n"
);
assert_eq!(read_pre_json(dir.path()), PRE_JSON);
}
#[test]
fn pre_enter_creates_pre_json_at_the_workspace_root() {
let dir = workspace_dir();
let output = changesette(&dir.path().join("packages/a"), &["pre", "enter", "beta"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(read_pre_json(dir.path()), PRE_JSON);
assert!(!dir.path().join("packages/a/.changeset").exists());
}
#[test]
fn pre_enter_accepts_a_dotted_tag() {
let dir = package_dir();
let output = changesette(dir.path(), &["pre", "enter", "beta.2"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
read_pre_json(dir.path()),
"{\n \"mode\": \"pre\",\n \"tag\": \"beta.2\"\n}\n"
);
}
#[test]
fn pre_enter_fails_when_already_in_pre_mode() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["pre", "enter", "alpha"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("already in pre mode"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn pre_enter_after_exit_rewrites_in_place() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{ // pre state\n\t\"tag\":\t\"alpha\",\n\t\"mode\": \"exit\",\n\t\"someday\": [1, 2, 3]\n}",
);
let output = changesette(dir.path(), &["pre", "enter", "beta"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
read_pre_json(dir.path()),
"{ // pre state\n\t\"tag\":\t\"beta\",\n\t\"mode\": \"pre\",\n\t\"someday\": [1, 2, 3]\n}"
);
}
#[test]
fn pre_enter_rejects_an_invalid_tag() {
for tag in ["", " ", "01", "beta 2"] {
let dir = package_dir();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["pre", "enter", tag]);
assert!(!output.status.success(), "{tag:?} should be rejected");
assert!(
stderr(&output).contains("invalid pre tag"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
}
#[test]
fn pre_enter_fails_on_a_v2_pre_json() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{\n \"mode\": \"pre\",\n \"tag\": \"beta\",\n \"initialVersions\": {},\n \"changesets\": []\n}\n",
);
let output = changesette(dir.path(), &["pre", "enter", "beta"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.contains("changesets v2 format"), "{err}");
assert!(err.contains("@changesets/cli@3"), "{err}");
}
#[test]
fn pre_exit_fails_without_pre_json() {
let dir = package_dir();
fs::create_dir(dir.path().join(".changeset")).unwrap();
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["pre", "exit"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("not in pre mode"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn pre_exit_flips_the_mode() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
let output = changesette(dir.path(), &["pre", "exit"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
"Exited pre mode\nRun `changesette version` to bump to final versions\n"
);
assert_eq!(read_pre_json(dir.path()), EXITED_PRE_JSON);
}
#[test]
fn pre_exit_is_idempotent() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
for _ in 0..2 {
let output = changesette(dir.path(), &["pre", "exit"]);
assert!(output.status.success(), "{}", stderr(&output));
}
assert_eq!(read_pre_json(dir.path()), EXITED_PRE_JSON);
}
#[test]
fn pre_exit_ignores_an_invalid_tag() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{\n \"mode\": \"pre\",\n \"tag\": \"not a tag\"\n}\n",
);
let output = changesette(dir.path(), &["pre", "exit"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
read_pre_json(dir.path()),
"{\n \"mode\": \"exit\",\n \"tag\": \"not a tag\"\n}\n"
);
}
#[test]
fn version_in_pre_mode_bumps_to_a_prerelease() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
let err = stderr(&output);
assert!(err.contains("In pre mode with tag `beta`"), "{err}");
assert!(!err.contains("warning:"), "{err}");
assert!(
err.contains("Bumped ublacklist 1.2.3 -> 1.3.0-beta.0\n"),
"{err}"
);
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.3.0-beta.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n"
);
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
assert!(dir.path().join(".changeset/pre").join(ULID_B).is_file());
assert_eq!(read_pre_json(dir.path()), PRE_JSON);
}
#[test]
fn version_in_pre_mode_increments_the_counter() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
write_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(
stderr(&output).contains("Bumped ublacklist 1.3.0-beta.0 -> 1.3.0-beta.1\n"),
"{}",
stderr(&output)
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0-beta.1\n\n### Patch Changes\n\n- Fix bug\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n"
);
assert!(dir.path().join(".changeset/pre").join(ULID_A).is_file());
assert!(dir.path().join(".changeset/pre").join(ULID_B).is_file());
}
#[test]
fn version_in_pre_mode_fails_on_a_move_collision() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_pre_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("refusing to overwrite"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_in_pre_mode_rejects_an_invalid_tag() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{\n \"mode\": \"pre\",\n \"tag\": \"not a tag\"\n}\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("invalid pre tag"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_in_pre_mode_leaves_ignored_changesets_in_place() {
let dir = two_package_workspace_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version", "--ignore", "pkg-b"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(
stderr(&output).contains("Bumped pkg-a 3.1.4 -> 3.2.0-beta.0\n"),
"{}",
stderr(&output)
);
assert!(dir.path().join(".changeset/pre").join(ULID_A).is_file());
assert!(dir.path().join(".changeset").join(ULID_B).is_file());
assert!(!dir.path().join(".changeset/pre").join(ULID_B).exists());
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.0\"\n}\n"
);
}
#[test]
fn version_in_pre_mode_keeps_a_none_only_package_unchanged() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\"\n}\n"
);
assert!(!dir.path().join("CHANGELOG.md").exists());
assert!(dir.path().join(".changeset/pre").join(ULID_B).is_file());
}
#[test]
fn version_in_pre_mode_with_no_new_changesets_fails() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_pre_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no unreleased changesets found"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_after_exit_finalizes() {
let dir = prerelease_package_dir("1.3.0-beta.0");
fs::write(
dir.path().join("CHANGELOG.md"),
"# ublacklist\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n",
)
.unwrap();
write_pre_json(dir.path(), EXITED_PRE_JSON);
write_pre_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "minor")],
"Add feature",
);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(stderr(&output), "Bumped ublacklist 1.3.0-beta.0 -> 1.3.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.3.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0\n\n### Minor Changes\n\n- Add feature\n\n### Patch Changes\n\n- Fix bug\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n"
);
assert!(!dir.path().join(".changeset/pre").join(ULID_A).exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_after_exit_rescues_prerelease_packages() {
let dir = two_package_workspace_dir();
fs::write(
dir.path().join("packages/a/package.json"),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.2.0-beta.0\"\n}\n",
)
.unwrap();
fs::write(
dir.path().join("packages/b/package.json"),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.1-alpha.2\"\n}\n",
)
.unwrap();
write_pre_json(dir.path(), EXITED_PRE_JSON);
let output = changesette(dir.path(), &["version", "--ignore", "pkg-b"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped pkg-a 3.2.0-beta.0 -> 3.2.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/package.json")).unwrap(),
"{\n \"name\": \"pkg-a\",\n \"version\": \"3.2.0\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/a/CHANGELOG.md")).unwrap(),
"# pkg-a\n\n## 3.2.0\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("packages/b/package.json")).unwrap(),
"{\n \"name\": \"pkg-b\",\n \"version\": \"2.0.1-alpha.2\"\n}\n"
);
assert!(!dir.path().join("packages/b/CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_after_exit_rescues_a_none_only_package() {
let dir = prerelease_package_dir("1.2.3-beta.1");
write_pre_json(dir.path(), EXITED_PRE_JSON);
write_pre_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped ublacklist 1.2.3-beta.1 -> 1.2.3\n");
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\"\n}\n"
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.2.3\n"
);
assert!(!dir.path().join(".changeset/pre").join(ULID_B).exists());
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_after_exit_succeeds_with_an_invalid_tag() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{\n \"mode\": \"pre\",\n \"tag\": \"not a tag\"\n}\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["pre", "exit"]);
assert!(output.status.success(), "{}", stderr(&output));
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped ublacklist 1.2.3 -> 1.3.0\n");
assert!(!dir.path().join(".changeset/pre.json").exists());
}
#[test]
fn version_after_exit_with_no_changesets_still_deletes_pre_json() {
let dir = package_dir();
write_pre_json(dir.path(), EXITED_PRE_JSON);
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "");
assert_eq!(stderr(&output), "");
assert!(!dir.path().join(".changeset/pre.json").exists());
assert_eq!(
fs::read_to_string(dir.path().join("package.json")).unwrap(),
"{\n \"name\": \"ublacklist\",\n \"version\": \"1.2.3\"\n}\n"
);
}
#[test]
fn version_consumes_pre_changesets_without_pre_json() {
let dir = package_dir();
write_pre_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "minor")],
"Add feature",
);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "Bumped ublacklist 1.2.3 -> 1.3.0\n");
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
"# ublacklist\n\n## 1.3.0\n\n### Minor Changes\n\n- Add feature\n\n### Patch Changes\n\n- Fix bug\n"
);
assert!(!dir.path().join(".changeset/pre").join(ULID_A).exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_output_omits_pre_state_without_pre_json() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "-o", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(!plan.contains("preState"), "{plan}");
}
#[test]
fn status_in_pre_mode_shows_prerelease_versions() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_pre_changeset(dir.path(), ULID_A, &[("ublacklist", "major")], "Rework");
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["status", "--verbose"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
format!(
"Packages to be bumped:\n- minor\n - ublacklist -> 1.3.0-beta.0\n - .changeset/{ULID_B}\n"
)
);
assert_eq!(stderr(&output), "");
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn status_in_pre_mode_rejects_an_invalid_tag() {
let dir = package_dir();
write_pre_json(
dir.path(),
"{\n \"mode\": \"pre\",\n \"tag\": \"not a tag\"\n}\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["status"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("invalid pre tag"),
"{}",
stderr(&output)
);
}
#[test]
fn status_output_in_pre_mode_includes_pre_state() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["status", "-o", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(
plan.contains("\"preState\": {\n \"mode\": \"pre\",\n \"tag\": \"beta\"\n }"),
"{plan}"
);
assert!(plan.contains("\"newVersion\": \"1.3.0-beta.0\""), "{plan}");
}
#[test]
fn status_output_includes_pre_state_and_prefixed_ids() {
let dir = prerelease_package_dir("1.3.0-beta.0");
write_pre_json(dir.path(), EXITED_PRE_JSON);
write_pre_changeset(
dir.path(),
ULID_A,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["status", "-o", "plan.json"]);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(
plan.contains("\"preState\": {\n \"mode\": \"exit\",\n \"tag\": \"beta\"\n }"),
"{plan}"
);
assert!(plan.contains(&format!("\"id\": \"pre/{ID_A}\"")), "{plan}");
assert!(plan.contains(&format!("\"pre/{ID_A}\"")), "{plan}");
assert!(plan.contains("\"newVersion\": \"1.3.0\""), "{plan}");
}
fn manifest_version(dir: &Path, rel: &str) -> String {
let text = fs::read_to_string(dir.join(rel)).unwrap();
let start = text.find("\"version\": \"").unwrap() + "\"version\": \"".len();
let len = text[start..].find('"').unwrap();
text[start..start + len].to_owned()
}
fn assert_datetime(text: &str) {
assert_eq!(text.len(), 14, "unexpected datetime: {text}");
assert!(
text.chars().all(|c| c.is_ascii_digit()),
"unexpected datetime: {text}"
);
}
#[test]
fn version_snapshot_bumps_to_a_zero_based_version() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
let suffix = version.strip_prefix("0.0.0-").unwrap_or_else(|| {
panic!("unexpected version: {version}");
});
assert_datetime(suffix);
assert!(
stderr(&output).contains(&format!("Bumped ublacklist 1.2.3 -> {version}\n")),
"{}",
stderr(&output)
);
assert_eq!(
fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap(),
format!("# ublacklist\n\n## {version}\n\n### Minor Changes\n\n- Add feature\n")
);
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_snapshot_with_a_tag_prefixes_the_suffix() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--snapshot", "canary"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
let suffix = version.strip_prefix("0.0.0-canary-").unwrap_or_else(|| {
panic!("unexpected version: {version}");
});
assert_datetime(suffix);
}
#[test]
fn version_snapshot_shares_one_suffix_across_packages() {
let dir = two_package_workspace_dir();
write_changeset(dir.path(), ULID_A, &[("pkg-a", "minor")], "Improve pkg-a");
write_changeset(dir.path(), ULID_B, &[("pkg-b", "patch")], "Fix pkg-b");
let output = changesette(dir.path(), &["version", "--snapshot", "canary"]);
assert!(output.status.success(), "{}", stderr(&output));
let version_a = manifest_version(dir.path(), "packages/a/package.json");
let version_b = manifest_version(dir.path(), "packages/b/package.json");
assert_eq!(version_a, version_b);
assert!(
version_a.starts_with("0.0.0-canary-"),
"unexpected version: {version_a}"
);
}
#[test]
fn version_snapshot_uses_the_config_template() {
let dir = package_dir();
write_config(
dir.path(),
"{ \"snapshot\": { \"prereleaseTemplate\": \"{tag}.{timestamp}\" } }\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--snapshot", "canary"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
let timestamp = version.strip_prefix("0.0.0-canary.").unwrap_or_else(|| {
panic!("unexpected version: {version}");
});
assert_eq!(timestamp.len(), 13, "unexpected timestamp: {timestamp}");
assert!(
timestamp.chars().all(|c| c.is_ascii_digit()),
"unexpected timestamp: {timestamp}"
);
}
#[test]
fn version_snapshot_cli_template_overrides_the_config() {
let dir = package_dir();
write_config(
dir.path(),
"{ \"snapshot\": { \"prereleaseTemplate\": \"{tag}-config-{datetime}\" } }\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(
dir.path(),
&[
"version",
"--snapshot",
"canary",
"--snapshot-prerelease-template",
"{tag}-cli-{datetime}",
],
);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
assert!(
version.starts_with("0.0.0-canary-cli-"),
"unexpected version: {version}"
);
}
#[test]
fn version_snapshot_uses_the_calculated_version() {
let dir = package_dir();
write_config(
dir.path(),
"{ \"snapshot\": { \"useCalculatedVersion\": true } }\n",
);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
let suffix = version.strip_prefix("1.3.0-").unwrap_or_else(|| {
panic!("unexpected version: {version}");
});
assert_datetime(suffix);
}
#[test]
fn version_snapshot_keeps_a_none_only_package_unchanged() {
let dir = package_dir();
write_changeset(dir.path(), ULID_B, &[("ublacklist", "none")], "Note only");
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stderr(&output), "No packages to bump.\n");
assert_eq!(manifest_version(dir.path(), "package.json"), "1.2.3");
assert!(!dir.path().join("CHANGELOG.md").exists());
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_snapshot_rejects_the_commit_placeholders() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
for template in ["sha-{commit}", "sha-{commit-short}"] {
let output = changesette(
dir.path(),
&[
"version",
"--snapshot",
"--snapshot-prerelease-template",
template,
],
);
assert!(!output.status.success(), "{template} should be rejected");
assert!(
stderr(&output).contains("no git operations"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
}
#[test]
fn version_snapshot_fails_in_pre_mode() {
let dir = package_dir();
write_pre_json(dir.path(), PRE_JSON);
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("not allowed in pre mode"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_snapshot_after_exit_keeps_pre_json() {
let dir = prerelease_package_dir("1.3.0-beta.1");
write_pre_json(dir.path(), EXITED_PRE_JSON);
write_changeset(dir.path(), ULID_B, &[("ublacklist", "patch")], "Fix bug");
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
assert!(
version.starts_with("0.0.0-"),
"unexpected version: {version}"
);
assert!(!dir.path().join(".changeset").join(ULID_B).exists());
assert_eq!(read_pre_json(dir.path()), EXITED_PRE_JSON);
}
#[test]
fn version_snapshot_after_exit_rescues_to_a_snapshot_version() {
let dir = prerelease_package_dir("1.3.0-beta.1");
write_pre_json(dir.path(), EXITED_PRE_JSON);
let output = changesette(dir.path(), &["version", "--snapshot"]);
assert!(output.status.success(), "{}", stderr(&output));
let version = manifest_version(dir.path(), "package.json");
let suffix = version.strip_prefix("0.0.0-").unwrap_or_else(|| {
panic!("unexpected version: {version}");
});
assert_datetime(suffix);
assert_eq!(read_pre_json(dir.path()), EXITED_PRE_JSON);
}
#[test]
fn version_snapshot_rejects_a_tag_without_the_tag_placeholder() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(
dir.path(),
&[
"version",
"--snapshot",
"canary",
"--snapshot-prerelease-template",
"{datetime}",
],
);
assert!(!output.status.success());
assert!(
stderr(&output).contains("does not contain \"{tag}\""),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_snapshot_rejects_the_tag_placeholder_without_a_tag() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(
dir.path(),
&[
"version",
"--snapshot",
"--snapshot-prerelease-template",
"{tag}-{datetime}",
],
);
assert!(!output.status.success());
assert!(
stderr(&output).contains("no snapshot tag is given"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_snapshot_rejects_an_invalid_tag() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let before = dir_snapshot(dir.path());
let output = changesette(dir.path(), &["version", "--snapshot", "pr#123"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("invalid snapshot suffix"),
"{}",
stderr(&output)
);
assert_eq!(dir_snapshot(dir.path()), before);
}
#[test]
fn version_snapshot_rejects_an_empty_tag() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(dir.path(), &["version", "--snapshot", ""]);
assert!(!output.status.success());
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_snapshot_template_requires_the_snapshot_flag() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(
dir.path(),
&["version", "--snapshot-prerelease-template", "{datetime}"],
);
assert!(!output.status.success());
assert!(
stderr(&output).contains("--snapshot"),
"{}",
stderr(&output)
);
assert!(dir.path().join(".changeset").join(ULID_B).exists());
}
#[test]
fn version_snapshot_output_writes_snapshot_versions() {
let dir = package_dir();
write_changeset(
dir.path(),
ULID_B,
&[("ublacklist", "minor")],
"Add feature",
);
let output = changesette(
dir.path(),
&["version", "--snapshot", "canary", "-o", "plan.json"],
);
assert!(output.status.success(), "{}", stderr(&output));
let plan = fs::read_to_string(dir.path().join("plan.json")).unwrap();
assert!(plan.contains("\"oldVersion\": \"1.2.3\""), "{plan}");
assert!(plan.contains("\"newVersion\": \"0.0.0-canary-"), "{plan}");
}
#[test]
fn get_changelog_entry_reads_a_prerelease_section() {
let dir = package_dir();
fs::write(
dir.path().join("CHANGELOG.md"),
"# ublacklist\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n",
)
.unwrap();
let output = changesette(
dir.path(),
&["get-changelog-entry", "ublacklist", "1.3.0-beta.0"],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "### Minor Changes\n\n- Add feature\n");
}
#[test]
fn get_changelog_entry_returns_an_empty_rescued_section() {
let dir = package_dir();
fs::write(
dir.path().join("CHANGELOG.md"),
"# ublacklist\n\n## 1.3.0\n\n## 1.3.0-beta.0\n\n### Minor Changes\n\n- Add feature\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-changelog-entry", "ublacklist", "1.3.0"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), "\n");
}
#[test]
fn prints_the_crate_version() {
let dir = tempfile::tempdir().unwrap();
let output = changesette(dir.path(), &["--version"]);
assert!(output.status.success());
assert_eq!(
stdout(&output),
format!("changesette {}\n", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn prints_help() {
let dir = tempfile::tempdir().unwrap();
let output = changesette(dir.path(), &["--help"]);
assert!(output.status.success());
let out = stdout(&output);
for subcommand in [
"init",
"add",
"version",
"pre",
"status",
"get-packages",
"get-changelog-entry",
] {
assert!(out.contains(subcommand), "{out}");
}
}
#[test]
fn rejects_an_unknown_subcommand() {
let dir = tempfile::tempdir().unwrap();
let output = changesette(dir.path(), &["publish"]);
assert!(!output.status.success());
}
#[test]
fn get_packages_warns_about_a_broken_ancestor_manifest_in_npm_mode() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"app\"],\n<<<<<<< HEAD\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("app")).unwrap();
fs::write(
dir.path().join("app/package.json"),
"{ \"name\": \"app\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
let output = changesette(&dir.path().join("app"), &["get-packages", "--all"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(stdout(&output).contains("\"app\""), "{}", stdout(&output));
let err = stderr(&output);
assert!(
err.contains(&format!(
"warning: {}: ",
expected_path(dir.path(), "package.json")
)) && err.contains("passed over while looking for an npm workspace root"),
"{err}"
);
}
#[test]
fn get_packages_lists_a_package_the_npm_root_above_does_not_list_alone() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/*\"]\n}\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("packages/a")).unwrap();
fs::write(
dir.path().join("packages/a/package.json"),
"{ \"name\": \"pkg-a\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
fs::create_dir_all(dir.path().join("examples/x")).unwrap();
fs::write(
dir.path().join("examples/x/package.json"),
"{ \"name\": \"example-x\", \"version\": \"1.0.0\" }\n",
)
.unwrap();
let output = changesette(&dir.path().join("examples/x"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"example-x\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\".\"}]\n"
);
}
#[test]
fn get_packages_warns_about_an_invalid_workspaces_type_under_yarn() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("yarn.lock"), "").unwrap();
fs::write(
dir.path().join("package.json"),
"{ \"name\": \"root\", \"version\": \"1.0.0\", \"workspaces\": \"packages/*\" }\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"root\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\".\"}]\n"
);
assert_eq!(
stderr(&output),
format!(
"warning: {}: \"workspaces\" must be an array or an object: ignored, as Yarn ignores it\n",
expected_path(dir.path(), "package.json")
)
);
}
const PKG_A_JSON: &str =
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\"packages/a\"}]\n";
#[test]
fn root_option_takes_a_relative_directory() {
let dir = workspace_dir();
let cwd = dir.path().join("packages/a");
let output = changesette(
&cwd,
&["add", "--patch", "pkg-a", "-m", "Fix", "--root", "../.."],
);
assert!(output.status.success(), "{}", stderr(&output));
let err = stderr(&output);
let path = added_path(&err);
assert!(
Path::new(path).starts_with(expected_path(dir.path(), ".changeset")),
"{path}"
);
assert_changeset_path(path);
assert!(Path::new(path).is_file());
}
#[cfg(unix)]
#[test]
fn root_option_resolves_a_symlink_to_the_physical_directory() {
let dir = workspace_dir();
std::os::unix::fs::symlink("packages/a", dir.path().join("link")).unwrap();
let output = changesette(dir.path(), &["init", "--root", "link"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
format!(
"Created {}\nCreated {}\n",
expected_path(dir.path(), "packages/a/.changeset/README.md"),
expected_path(dir.path(), "packages/a/.changeset/config.json")
)
);
}
#[test]
fn root_option_dot_makes_the_working_directory_the_root() {
let dir = workspace_dir();
let output = changesette(
&dir.path().join("packages/a"),
&["get-packages", "--root", "."],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-a\",\"version\":\"3.1.4\",\"private\":false,\"dir\":\".\"}]\n"
);
}
#[test]
fn root_option_takes_an_absolute_directory_outside_the_working_directory() {
let dir = workspace_dir();
let other = tempfile::tempdir().unwrap();
let output = changesette(
other.path(),
&["get-packages", "--root", &expected_path(dir.path(), "")],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
}
#[test]
fn root_env_takes_the_directory() {
let dir = workspace_dir();
let other = tempfile::tempdir().unwrap();
let output = command(other.path(), &["get-packages"])
.env("CHANGESETTE_ROOT", dir.path())
.output()
.unwrap();
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
}
#[test]
fn root_option_wins_over_the_env() {
let dir = workspace_dir();
let other = package_dir();
let output = command(dir.path(), &["get-packages", "--root", "."])
.env("CHANGESETTE_ROOT", other.path())
.output()
.unwrap();
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
}
#[test]
fn an_empty_root_env_is_unset() {
let dir = workspace_dir();
let output = command(&dir.path().join("packages/a"), &["get-packages"])
.env("CHANGESETTE_ROOT", "")
.output()
.unwrap();
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
}
#[test]
fn init_with_root_creates_the_directory_there() {
let dir = workspace_dir();
let output = changesette(dir.path(), &["init", "--root", "packages/a"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
format!(
"Created {}\nCreated {}\n",
expected_path(dir.path(), "packages/a/.changeset/README.md"),
expected_path(dir.path(), "packages/a/.changeset/config.json")
)
);
assert!(
dir.path()
.join("packages/a/.changeset/config.json")
.is_file()
);
assert!(!dir.path().join(".changeset").exists());
}
#[test]
fn root_option_rejects_a_missing_directory() {
let dir = workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--root", "missing"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(err.starts_with("error: invalid --root missing: "), "{err}");
}
#[test]
fn root_option_rejects_a_file() {
let dir = workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--root", "package.json"]);
assert!(!output.status.success());
assert_eq!(
stderr(&output),
"error: invalid --root package.json: not a directory\n"
);
}
#[test]
fn root_option_rejects_a_directory_without_a_manifest() {
let dir = workspace_dir();
fs::create_dir(dir.path().join("empty")).unwrap();
let output = changesette(dir.path(), &["get-packages", "--root", "empty"]);
assert!(!output.status.success());
assert_eq!(
stderr(&output),
format!(
"error: no package.json in {}; --root must name a workspace root or a package\n",
expected_path(dir.path(), "empty")
)
);
}
#[cfg(windows)]
#[test]
fn root_option_accepts_a_backslash_separator() {
let dir = workspace_dir();
let output = changesette(dir.path(), &["init", "--root", "packages\\a"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stderr(&output),
format!(
"Created {}\nCreated {}\n",
expected_path(dir.path(), "packages/a/.changeset/README.md"),
expected_path(dir.path(), "packages/a/.changeset/config.json")
)
);
}
fn write_config_packages(dir: &Path, packages: &[&str]) {
fs::create_dir_all(dir.join(".changeset")).unwrap();
let list: Vec<String> = packages.iter().map(|dir| format!("\"{dir}\"")).collect();
fs::write(
dir.join(".changeset/config.json"),
format!(
"{{ \"changesette\": {{ \"packages\": [{}] }} }}\n",
list.join(", ")
),
)
.unwrap();
}
#[test]
fn config_packages_round_trip_through_get_packages_all() {
let dir = mixed_workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--all"]);
assert!(output.status.success(), "{}", stderr(&output));
let expected = stdout(&output);
let packages: Vec<serde_json::Value> = serde_json::from_str(&expected).unwrap();
let dirs: Vec<&str> = packages
.iter()
.map(|package| package["dir"].as_str().unwrap())
.collect();
write_config_packages(dir.path(), &dirs);
fs::write(dir.path().join("package.json"), "{}\n").unwrap();
let output = changesette(dir.path(), &["get-packages", "--all"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), expected);
}
#[test]
fn config_packages_bypass_the_workspace_enumeration() {
let dir = workspace_dir();
fs::write(
dir.path().join("package.json"),
"{\n \"workspaces\": [\"packages/../*\"]\n}\n",
)
.unwrap();
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
write_config_packages(dir.path(), &["packages/a"]);
let output = changesette(dir.path(), &["get-packages", "--log-level", "debug"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
assert_eq!(
stderr(&output),
format!(
"debug: workspace {} (packages from config): members: pkg-a (packages/a)\n",
expected_path(dir.path(), "")
)
);
}
#[test]
fn config_packages_apply_from_a_member_directory() {
let dir = mixed_workspace_dir();
write_config_packages(dir.path(), &["packages/c"]);
let output = changesette(&dir.path().join("packages/a"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"pkg-c\",\"version\":\"2.0.0\",\"private\":false,\"dir\":\"packages/c\"}]\n"
);
}
#[test]
fn config_packages_are_read_from_the_forced_root() {
let dir = workspace_dir();
let other = tempfile::tempdir().unwrap();
write_config_packages(dir.path(), &["packages/a"]);
fs::remove_file(dir.path().join("package.json")).unwrap();
let output = changesette(
other.path(),
&["get-packages", "--root", &expected_path(dir.path(), "")],
);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
}
#[test]
fn config_packages_entry_errors_name_the_config() {
let dir = workspace_dir();
write_config_packages(dir.path(), &["/x"]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
assert_eq!(
stderr(&output),
format!(
"error: {}: invalid \"changesette.packages\" entry \"/x\": absolute paths are not supported\n",
expected_path(dir.path(), ".changeset/config.json")
)
);
}
#[test]
fn config_packages_missing_directory_is_an_error() {
let dir = workspace_dir();
write_config_packages(dir.path(), &["packages/a", "packages/zzz"]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
assert_eq!(
stderr(&output),
format!(
"error: {}: not found (listed in \"changesette.packages\")\n",
expected_path(dir.path(), "packages/zzz/package.json")
)
);
}
#[test]
fn config_packages_aliases_collapse_into_the_normal_spelling() {
let dir = workspace_dir();
write_config_packages(dir.path(), &["packages/a", "./packages/a"]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON);
for entry in ["./packages/a", "packages//a/", "packages/b/../a"] {
write_config_packages(dir.path(), &[entry]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), PKG_A_JSON, "{entry}");
}
}
fn sibling_workspace_dir() -> TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("ws")).unwrap();
fs::write(dir.path().join("ws/package.json"), "{}\n").unwrap();
write_config_packages(&dir.path().join("ws"), &["../shared"]);
fs::create_dir_all(dir.path().join("shared")).unwrap();
fs::write(
dir.path().join("shared/package.json"),
"{\n \"name\": \"shared\",\n \"version\": \"1.0.0\"\n}\n",
)
.unwrap();
dir
}
const SHARED_JSON: &str =
"[{\"name\":\"shared\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\"../shared\"}]\n";
#[test]
fn config_packages_reach_outside_the_root() {
let dir = sibling_workspace_dir();
let output = changesette(&dir.path().join("ws"), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), SHARED_JSON);
}
#[test]
fn config_packages_reach_outside_a_relative_root() {
let dir = sibling_workspace_dir();
let output = changesette(dir.path(), &["get-packages", "--root", "ws"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(stdout(&output), SHARED_JSON);
}
#[test]
fn config_packages_spell_the_root_as_a_dot() {
let dir = workspace_dir();
fs::write(
dir.path().join("package.json"),
"{\n \"name\": \"root\",\n \"version\": \"1.0.0\"\n}\n",
)
.unwrap();
for entry in [".", "./", "packages/.."] {
write_config_packages(dir.path(), &[entry]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output),
"[{\"name\":\"root\",\"version\":\"1.0.0\",\"private\":false,\"dir\":\".\"}]\n",
"{entry}"
);
}
fs::remove_file(dir.path().join("package.json")).unwrap();
let output = changesette(dir.path(), &["get-packages", "--root", "."]);
assert!(!output.status.success());
assert_eq!(
stderr(&output),
format!(
"error: {}: not found (listed in \"changesette.packages\")\n",
expected_path(dir.path(), "package.json")
)
);
}
#[cfg(windows)]
#[test]
fn config_packages_reject_a_drive_prefixed_segment() {
let dir = workspace_dir();
for entry in ["packages/C:x", "../C:/x", "C:/x"] {
write_config_packages(dir.path(), &[entry]);
let output = changesette(dir.path(), &["get-packages"]);
assert!(!output.status.success());
let err = stderr(&output);
assert!(
err.starts_with(&format!(
"error: invalid \"changesette.packages\" entry {entry:?}: "
)) && err.ends_with("is not a directory name\n"),
"{err}"
);
}
}