import { ReleasePlan, Config, NewChangeset, ComprehensiveRelease, } from "@changesets/types"; import * as git from "@changesets/git"; import fs from "fs-extra"; import path from "path"; import outdent from "outdent"; import spawn from "spawndamnit"; import { defaultConfig } from "@changesets/config"; import applyReleasePlan from "./"; import { getPackages } from "@manypkg/get-packages"; import { temporarilySilenceLogs, testdir, Fixture, } from "@changesets/test-utils"; class FakeReleasePlan { changesets: NewChangeset[]; releases: ComprehensiveRelease[]; config: Config; constructor( changesets: NewChangeset[] = [], releases: ComprehensiveRelease[] = [], config: Partial = {} ) { const baseChangeset: NewChangeset = { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [{ name: "pkg-a", type: "minor" }], }; const baseRelease: ComprehensiveRelease = { name: "pkg-a", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }; this.config = { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, ...config, }; this.changesets = [baseChangeset, ...changesets]; this.releases = [baseRelease, ...releases]; } getReleasePlan(): ReleasePlan { return { changesets: this.changesets, releases: this.releases, preState: undefined, }; } } async function testSetup( fixture: Fixture, releasePlan: ReleasePlan, config?: Config, snapshot?: string | undefined, setupFunc?: (tempDir: string) => Promise ) { if (!config) { config = { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, }; } let tempDir = await testdir(fixture); if (setupFunc) { await setupFunc(tempDir); } if (config.commit) { await spawn("git", ["init"], { cwd: tempDir }); await git.add(".", tempDir); await git.commit("first commit", tempDir); } return { changedFiles: await applyReleasePlan( releasePlan, await getPackages(tempDir), config, snapshot ), tempDir, }; } describe("apply release plan", () => { describe("versioning", () => { describe("formatting", () => { it("should not reformat a small array in a package.json", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": `{ "name": "pkg-a", "version": "1.0.0", "files": [ "lib" ] }`, }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", "version": "1.1.0", "files": [ "lib" ] }`); }); it("should not change tab indentation in a package.json", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify( { name: "pkg-a", version: "1.0.0", }, null, "\t" ), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ \t"name": "pkg-a", \t"version": "1.1.0" }`); }); it("should not add trailing newlines in a package.json if they don't exist", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", "version": "1.1.0" }`); }); it("should not remove trailing newlines in a package.json if they exist", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }) + "\n", }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`package.json`)); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readFile(pkgPath, { encoding: "utf-8" }); expect(pkgJSON).toStrictEqual(`{ "name": "pkg-a", "version": "1.1.0" }\n`); }); }); it("should update a version for one package", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readJSON(pkgPath); expect(pkgJSON).toMatchObject({ name: "pkg-a", version: "1.1.0", }); }); it("should not update ranges set to *", async () => { const releasePlan = new FakeReleasePlan( [ { id: "some-id", releases: [{ name: "pkg-b", type: "minor" }], summary: "a very useful summary", }, ], [ { changesets: ["some-id"], name: "pkg-b", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "*", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readJSON(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "*", }, }); }); it("should update workspace ranges", async () => { const releasePlan = new FakeReleasePlan( [ { id: "some-id", releases: [{ name: "pkg-b", type: "minor" }], summary: "a very useful summary", }, ], [ { changesets: ["some-id"], name: "pkg-b", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "workspace:1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readJSON(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "workspace:1.1.0", }, }); }); it("should not update workspace version aliases", async () => { const releasePlan = new FakeReleasePlan( [ { id: "some-id", releases: [{ name: "pkg-b", type: "minor" }], summary: "a very useful summary", }, { id: "some-id", releases: [{ name: "pkg-c", type: "minor" }], summary: "a very useful summary", }, { id: "some-id", releases: [{ name: "pkg-d", type: "minor" }], summary: "a very useful summary", }, ], [ { changesets: ["some-id"], name: "pkg-b", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, { changesets: ["some-id"], name: "pkg-c", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, { changesets: ["some-id"], name: "pkg-d", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "workspace:*", "pkg-c": "workspace:^", "pkg-d": "workspace:~", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "1.0.0", }), "packages/pkg-d/package.json": JSON.stringify({ name: "pkg-d", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readJSON(pkgPath); expect(pkgJSON).toEqual({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "workspace:*", "pkg-c": "workspace:^", "pkg-d": "workspace:~", }, }); }); it("should update workspace ranges only with bumpVersionsWithWorkspaceProtocolOnly", async () => { const releasePlan = new FakeReleasePlan( [ { id: "some-id", releases: [ { name: "pkg-b", type: "minor" }, { name: "pkg-c", type: "minor" }, ], summary: "a very useful summary", }, ], [ { changesets: ["some-id"], name: "pkg-b", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, { changesets: ["some-id"], name: "pkg-c", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, ], { bumpVersionsWithWorkspaceProtocolOnly: true, } ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "workspace:1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgAPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgAPath) throw new Error(`could not find an updated package json`); let pkgAJSON = await fs.readJSON(pkgAPath); expect(pkgAJSON).toEqual({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "workspace:1.1.0", }, }); let pkgCPath = changedFiles.find((a) => a.endsWith(`pkg-c${path.sep}package.json`) ); if (!pkgCPath) throw new Error(`could not find an updated package json`); let pkgCJSON = await fs.readJSON(pkgCPath); expect(pkgCJSON).toEqual({ name: "pkg-c", version: "1.1.0", dependencies: { "pkg-b": "1.0.0", }, }); }); it("should update a version for two packages with different new versions", async () => { const releasePlan = new FakeReleasePlan( [], [ { name: "pkg-b", type: "major", oldVersion: "1.0.0", newVersion: "2.0.0", changesets: [], }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.1.0", }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "2.0.0", }); }); it("should not update the version of the dependent package if the released dep is a dev dep", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", devDependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "none" }, { name: "pkg-b", type: "minor" }, ], }, ], releases: [ { name: "pkg-a", type: "none", oldVersion: "1.0.0", newVersion: "1.0.0", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", baseBranch: "main", changedFilePatterns: ["**"], updateInternalDependencies: "patch", privatePackages: { version: true, tag: false }, ignore: [], ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.0.0", devDependencies: { "pkg-b": "1.1.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.1.0", }); }); it("should skip dependencies that have the same name as the package", async () => { let { tempDir } = await testSetup( { "package.json": JSON.stringify({ name: "self-referenced", version: "1.0.0", devDependencies: { "self-referenced": "file:", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [{ name: "self-referenced", type: "minor" }], }, ], releases: [ { name: "self-referenced", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", baseBranch: "main", changedFilePatterns: ["**"], updateInternalDependencies: "patch", privatePackages: { version: true, tag: false }, ignore: [], ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgJSON = await fs.readJSON(path.join(tempDir, "package.json")); expect(pkgJSON).toMatchObject({ name: "self-referenced", version: "1.1.0", devDependencies: { "self-referenced": "file:", }, }); }); it("should not update dependent versions when a package has a changeset type of none", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "^1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [{ name: "pkg-b", type: "none" }], }, ], releases: [ { name: "pkg-b", type: "none", oldVersion: "1.0.0", newVersion: "1.0.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { ...defaultConfig, changelog: false } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); expect(pkgPathA).toBeUndefined(); if (!pkgPathB) throw new Error(`could not find an updated package json`); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.0.0", }); }); it("should not update workspace dependent versions when a package has a changeset type of none", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "workspace:1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [{ name: "pkg-b", type: "none" }], }, ], releases: [ { name: "pkg-b", type: "none", oldVersion: "1.0.0", newVersion: "1.0.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { ...defaultConfig, changelog: false } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); expect(pkgPathA).toBeUndefined(); if (!pkgPathB) throw new Error(`could not find an updated package json`); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.0.0", }); }); it("should use exact versioning when snapshot release is applied, and ignore any range modifiers", async () => { const releasePlan = new FakeReleasePlan( [ { id: "some-id", releases: [{ name: "pkg-b", type: "minor" }], summary: "a very useful summary", }, ], [ { changesets: ["some-id"], name: "pkg-b", newVersion: "1.1.0", oldVersion: "1.0.0", type: "minor", }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "^1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config, "canary" ); let pkgPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); if (!pkgPath) throw new Error(`could not find an updated package json`); let pkgJSON = await fs.readJSON(pkgPath); expect(pkgJSON).toMatchObject({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "1.1.0", }, }); }); describe("internal dependency bumping", () => { describe("updateInternalDependencies set to patch", () => { const updateInternalDependencies = "patch"; it("should update min version ranges of patch bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.0.4", dependencies: { "pkg-b": "~1.2.1", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^1.0.4", }, }); }); it("should still update min version ranges of patch bumped internal dependencies that have left semver range", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.0", "pkg-a": "^1.0.3", }, }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "2.0.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, { name: "pkg-c", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "none", oldVersion: "1.2.0", newVersion: "1.2.0", changesets: ["quick-lions-devour"], }, { name: "pkg-c", type: "patch", oldVersion: "2.0.0", newVersion: "2.0.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.0.4", dependencies: { "pkg-b": "~1.2.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.1", "pkg-a": "^1.0.4", }, }); }); it("should update min version ranges of minor bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "minor" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "minor", oldVersion: "1.0.3", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "~1.2.1", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^1.1.0", }, }); }); it("should update min version ranges of major bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "major" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "major", oldVersion: "1.0.3", newVersion: "2.0.0", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "2.0.0", dependencies: { "pkg-b": "~1.2.1", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^2.0.0", }, }); }); }); describe("updateInternalDependencies set to minor", () => { const updateInternalDependencies = "minor"; it("should NOT update min version ranges of patch bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.0.4", dependencies: { "pkg-b": "~1.2.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^1.0.3", }, }); }); it("should still update min version ranges of patch bumped internal dependencies that have left semver range", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.0", "pkg-a": "^1.0.3", }, }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "2.0.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, { name: "pkg-c", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, { name: "pkg-c", type: "patch", oldVersion: "2.0.0", newVersion: "2.0.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.0.4", dependencies: { "pkg-b": "~1.2.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-c": "2.0.1", "pkg-a": "^1.0.3", }, }); }); it("should update min version ranges of minor bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.0", "pkg-a": "^1.0.3", }, }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "2.0.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "minor" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "minor", oldVersion: "1.0.3", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "1.1.0", dependencies: { "pkg-b": "~1.2.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^1.1.0", }, }); }); it("should update min version ranges of major bumped internal dependencies", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "major" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "major", oldVersion: "1.0.3", newVersion: "2.0.0", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathA = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}package.json`) ); let pkgPathB = changedFiles.find((b) => b.endsWith(`pkg-b${path.sep}package.json`) ); if (!pkgPathA || !pkgPathB) { throw new Error(`could not find an updated package json`); } let pkgJSONA = await fs.readJSON(pkgPathA); let pkgJSONB = await fs.readJSON(pkgPathB); expect(pkgJSONA).toMatchObject({ name: "pkg-a", version: "2.0.0", dependencies: { "pkg-b": "~1.2.0", }, }); expect(pkgJSONB).toMatchObject({ name: "pkg-b", version: "1.2.1", dependencies: { "pkg-a": "^2.0.0", }, }); }); }); }); describe("onlyUpdatePeerDependentsWhenOutOfRange set to true", () => { it("should not bump peerDependencies if they are still in range", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/depended-upon/package.json": JSON.stringify({ name: "depended-upon", version: "1.0.0", }), "packages/has-peer-dep/package.json": JSON.stringify({ name: "has-peer-dep", version: "1.0.0", peerDependencies: { "depended-upon": "^1.0.0", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "depended-upon", type: "patch" }, { name: "has-peer-dep", type: "patch" }, ], }, ], releases: [ { name: "has-peer-dep", type: "patch", oldVersion: "1.0.0", newVersion: "1.0.1", changesets: ["quick-lions-devour"], }, { name: "depended-upon", type: "patch", oldVersion: "1.0.0", newVersion: "1.0.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: false, commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: true, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgPathDependent = changedFiles.find((a) => a.endsWith(`has-peer-dep${path.sep}package.json`) ); let pkgPathDepended = changedFiles.find((b) => b.endsWith(`depended-upon${path.sep}package.json`) ); if (!pkgPathDependent || !pkgPathDepended) { throw new Error(`could not find an updated package json`); } let pkgJSONDependent = await fs.readJSON(pkgPathDependent); let pkgJSONDepended = await fs.readJSON(pkgPathDepended); expect(pkgJSONDependent).toMatchObject({ name: "has-peer-dep", version: "1.0.1", peerDependencies: { "depended-upon": "^1.0.0", }, }); expect(pkgJSONDepended).toMatchObject({ name: "depended-upon", version: "1.0.1", }); }); }); }); describe("changelogs", () => { it("should not generate any changelogs", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, changelog: false, } ); expect( changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`)) ).toBeUndefined(); }); it("should update a changelog for one package", async () => { const releasePlan = new FakeReleasePlan(); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); if (!readmePath) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.1.0 ### Minor Changes - Hey, let's have fun with testing!`); }); it("should update a changelog for two packages", async () => { const releasePlan = new FakeReleasePlan( [], [ { name: "pkg-b", type: "major", oldVersion: "1.0.0", newVersion: "2.0.0", changesets: [], }, ] ); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); let readmePathB = changedFiles.find((a) => a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); let readmeB = await fs.readFile(readmePathB, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.1.0 ### Minor Changes - Hey, let's have fun with testing! ### Patch Changes - pkg-b@2.0.0`); expect(readmeB.trim()).toEqual(outdent`# pkg-b ## 2.0.0`); }); it("should not update the changelog if only devDeps changed", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", devDependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "none" }, { name: "pkg-b", type: "minor" }, ], }, ], releases: [ { name: "pkg-a", type: "none", oldVersion: "1.0.0", newVersion: "1.0.0", changesets: [], }, { name: "pkg-b", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { commit: false, fixed: [], linked: [], access: "restricted", baseBranch: "main", changedFilePatterns: ["**"], changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let pkgAChangelogPath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); expect(pkgAChangelogPath).toBeUndefined(); }); test("should list multi-line same-type summaries correctly", async () => { const releasePlan = new FakeReleasePlan([ { id: "some-id-1", summary: "Random stuff\n\nget it while it's hot!", releases: [{ name: "pkg-a", type: "minor" }], }, { id: "some-id-2", summary: "New feature, much wow\n\nlook at this shiny stuff!", releases: [{ name: "pkg-a", type: "minor" }], }, ]); releasePlan.releases[0].changesets.push("some-id-1", "some-id-2"); let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); if (!readmePath) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); expect(readme.trim()).toEqual( [ "# pkg-a\n", "## 1.1.0\n", "### Minor Changes\n", "- Hey, let's have fun with testing!", "- Random stuff\n", " get it while it's hot!\n", "- New feature, much wow\n", " look at this shiny stuff!", ].join("\n") ); }); it("should add an updated dependencies line when dependencies have been updated", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); let readmePathB = changedFiles.find((a) => a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); let readmeB = await fs.readFile(readmePathB, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.0.4 ### Patch Changes - Hey, let's have fun with testing! - Updated dependencies - pkg-b@1.2.1`); expect(readmeB.trim()).toEqual(outdent`# pkg-b ## 1.2.1 ### Patch Changes - Hey, let's have fun with testing! - Updated dependencies - pkg-a@1.0.4`); }); it("should NOT add updated dependencies line if dependencies have NOT been updated", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); let readmePathB = changedFiles.find((a) => a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) ); if (!readmePath || !readmePathB) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); let readmeB = await fs.readFile(readmePathB, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.0.4 ### Patch Changes - Hey, let's have fun with testing!`); expect(readmeB.trim()).toEqual(outdent`# pkg-b ## 1.2.1 ### Patch Changes - Hey, let's have fun with testing!`); }); it("should only add updated dependencies line for dependencies that have been updated", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.0", "pkg-a": "^1.0.3", }, }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "2.0.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, { name: "pkg-c", type: "minor" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, { name: "pkg-c", type: "minor", oldVersion: "2.0.0", newVersion: "2.1.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); let readmePathB = changedFiles.find((a) => a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) ); let readmePathC = changedFiles.find((a) => a.endsWith(`pkg-c${path.sep}CHANGELOG.md`) ); if (!readmePath || !readmePathB || !readmePathC) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); let readmeB = await fs.readFile(readmePathB, "utf-8"); let readmeC = await fs.readFile(readmePathC, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.0.4 ### Patch Changes - Hey, let's have fun with testing!`); expect(readmeB.trim()).toEqual(outdent`# pkg-b ## 1.2.1 ### Patch Changes - Hey, let's have fun with testing! - Updated dependencies - pkg-c@2.1.0`); expect(readmeC.trim()).toEqual(outdent`# pkg-c ## 2.1.0 ### Minor Changes - Hey, let's have fun with testing!`); }); it("should still add updated dependencies line for dependencies that have a bump type less than the minimum internal bump range but leave semver range", async () => { let { changedFiles } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.3", dependencies: { "pkg-b": "~1.2.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.2.0", dependencies: { "pkg-c": "2.0.0", "pkg-a": "^1.0.3", }, }), "packages/pkg-c/package.json": JSON.stringify({ name: "pkg-c", version: "2.0.0", dependencies: { "pkg-a": "^1.0.3", }, }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [ { name: "pkg-a", type: "patch" }, { name: "pkg-b", type: "patch" }, { name: "pkg-c", type: "patch" }, ], }, ], releases: [ { name: "pkg-a", type: "patch", oldVersion: "1.0.3", newVersion: "1.0.4", changesets: ["quick-lions-devour"], }, { name: "pkg-b", type: "patch", oldVersion: "1.2.0", newVersion: "1.2.1", changesets: ["quick-lions-devour"], }, { name: "pkg-c", type: "patch", oldVersion: "2.0.0", newVersion: "2.0.1", changesets: ["quick-lions-devour"], }, ], preState: undefined, }, { changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], commit: false, fixed: [], linked: [], access: "restricted", changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], privatePackages: { version: true, tag: false }, ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { onlyUpdatePeerDependentsWhenOutOfRange: false, updateInternalDependents: "out-of-range", }, snapshot: { useCalculatedVersion: false, prereleaseTemplate: null, }, } ); let readmePath = changedFiles.find((a) => a.endsWith(`pkg-a${path.sep}CHANGELOG.md`) ); let readmePathB = changedFiles.find((a) => a.endsWith(`pkg-b${path.sep}CHANGELOG.md`) ); let readmePathC = changedFiles.find((a) => a.endsWith(`pkg-c${path.sep}CHANGELOG.md`) ); if (!readmePath || !readmePathB || !readmePathC) throw new Error(`could not find an updated changelog`); let readme = await fs.readFile(readmePath, "utf-8"); let readmeB = await fs.readFile(readmePathB, "utf-8"); let readmeC = await fs.readFile(readmePathC, "utf-8"); expect(readme.trim()).toEqual(outdent`# pkg-a ## 1.0.4 ### Patch Changes - Hey, let's have fun with testing!`); expect(readmeB.trim()).toEqual(outdent`# pkg-b ## 1.2.1 ### Patch Changes - Hey, let's have fun with testing! - Updated dependencies - pkg-c@2.0.1`); expect(readmeC.trim()).toEqual(outdent`# pkg-c ## 2.0.1 ### Patch Changes - Hey, let's have fun with testing!`); }); }); describe("should error and not write if", () => { // This is skipped as *for now* we are assuming we have been passed // valid releasePlans - this may get work done on it in the future it.skip("a package appears twice", async () => { let changedFiles; try { let testResults = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, { changesets: [ { id: "quick-lions-devour", summary: "Hey, let's have fun with testing!", releases: [{ name: "pkg-a", type: "minor" }], }, ], releases: [ { name: "pkg-a", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, { name: "pkg-a", type: "minor", oldVersion: "1.0.0", newVersion: "1.1.0", changesets: ["quick-lions-devour"], }, ], preState: undefined, } ); changedFiles = testResults.changedFiles; } catch (e) { expect((e as Error).message).toEqual("some string probably"); return; } throw new Error( `expected error but instead got changed files: \n${changedFiles.join( "\n" )}` ); }); it("a package cannot be found", async () => { let releasePlan = new FakeReleasePlan( [], [ { name: "impossible-package", type: "minor", oldVersion: "1.0.0", newVersion: "1.0.0", changesets: [], }, ] ); let tempDir = await testdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }); await spawn("git", ["init"], { cwd: tempDir }); await git.add(".", tempDir); await git.commit("first commit", tempDir); try { await applyReleasePlan( releasePlan.getReleasePlan(), await getPackages(tempDir), releasePlan.config ); } catch (e) { expect((e as Error).message).toEqual( "Could not find matching package for release of: impossible-package" ); let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); expect(gitCmd.stdout.toString().includes("nothing to commit")).toEqual( true ); return; } throw new Error("Expected test to exit before this point"); }); it( "a provided changelog function fails", temporarilySilenceLogs(async () => { let releasePlan = new FakeReleasePlan(); let tempDir = await testdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }); await spawn("git", ["init"], { cwd: tempDir }); await git.add(".", tempDir); await git.commit("first commit", tempDir); try { await applyReleasePlan( releasePlan.getReleasePlan(), await getPackages(tempDir), { ...releasePlan.config, changelog: [ path.resolve(__dirname, "test-utils/failing-functions"), null, ], } ); } catch (e) { expect((e as Error).message).toEqual("no chance"); let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); expect( gitCmd.stdout.toString().includes("nothing to commit") ).toEqual(true); expect((console.error as any).mock.calls).toMatchInlineSnapshot(` [ [ "The following error was encountered while generating changelog entries", ], [ "We have escaped applying the changesets, and no files should have been affected", ], ] `); return; } throw new Error("Expected test to exit before this point"); }) ); }); describe("changesets", () => { it("should delete one changeset after it is applied", async () => { const releasePlan = new FakeReleasePlan(); let changesetPath: string; const setupFunc = (tempDir: string) => Promise.all( releasePlan.getReleasePlan().changesets.map(({ id, summary }) => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; return fs.outputFile(thisPath, content); }) ); await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config, undefined, setupFunc ); // @ts-ignore this is possibly bad let pathExists = await fs.pathExists(changesetPath); expect(pathExists).toEqual(false); }); it("should NOT delete changesets for ignored packages", async () => { const releasePlan = new FakeReleasePlan(); let changesetPath: string; const setupFunc = (tempDir: string) => Promise.all( releasePlan.getReleasePlan().changesets.map(({ id, summary }) => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; return fs.outputFile(thisPath, content); }) ); await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, ignore: ["pkg-a"] }, undefined, setupFunc ); // @ts-ignore this is possibly bad let pathExists = await fs.pathExists(changesetPath); expect(pathExists).toEqual(true); }); it("should delete an old format changeset if it is applied", async () => { const releasePlan = new FakeReleasePlan(); let changesetMDPath: string; let changesetJSONPath: string; const setupFunc = (tempDir: string) => Promise.all( releasePlan .getReleasePlan() .changesets.map(async ({ id, summary }) => { changesetMDPath = path.resolve( tempDir, ".changeset", id, `changes.md` ); changesetJSONPath = path.resolve( tempDir, ".changeset", id, `changes.json` ); await fs.outputFile(changesetMDPath, summary); await fs.outputFile( changesetJSONPath, JSON.stringify({ id, summary }) ); }) ); await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), releasePlan.config, undefined, setupFunc ); // @ts-ignore this is possibly bad let mdPathExists = await fs.pathExists(changesetMDPath); // @ts-ignore this is possibly bad let JSONPathExists = await fs.pathExists(changesetMDPath); expect(mdPathExists).toEqual(false); expect(JSONPathExists).toEqual(false); }); }); it("should get the commit for an old changeset", async () => { const releasePlan = new FakeReleasePlan(); let changesetMDPath: string; let changesetJSONPath: string; const setupFunc = (tempDir: string) => Promise.all( releasePlan.changesets.map(async ({ id, summary }) => { changesetMDPath = path.resolve( tempDir, ".changeset", id, `changes.md` ); changesetJSONPath = path.resolve( tempDir, ".changeset", id, `changes.json` ); await fs.outputFile(changesetMDPath, summary); await fs.outputFile( changesetJSONPath, JSON.stringify({ id, summary }) ); }) ); let { tempDir } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, commit: [ path.resolve(__dirname, "test-utils/simple-get-commit-entry"), null, ], changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, ], }, undefined, setupFunc ); let thing = await spawn("git", ["rev-list", "HEAD"], { cwd: tempDir }); let commits = thing.stdout .toString("utf8") .split("\n") .filter((x) => x); let lastCommit = commits[commits.length - 1].substring(0, 7); expect( await fs.readFile( path.join(tempDir, "packages", "pkg-a", "CHANGELOG.md"), "utf8" ) ).toBe(`# pkg-a ## 1.1.0 ### Minor Changes - ${lastCommit}: Hey, let's have fun with testing! `); }); describe("files", () => { it("shouldn't commit updated files from packages", async () => { const releasePlan = new FakeReleasePlan(); let { tempDir } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, commit: [ path.resolve(__dirname, "test-utils/simple-get-commit-entry"), null, ], } ); let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); expect(gitCmd.stdout.toString()).toContain( "Changes not staged for commit" ); expect(gitCmd.stdout.toString()).toContain( "modified: packages/pkg-a/package.json" ); let lastCommit = await spawn("git", ["log", "-1"], { cwd: tempDir }); expect(lastCommit.stdout.toString()).toContain("first commit"); }); it("should remove applied changesets", async () => { const releasePlan = new FakeReleasePlan(); let changesetPath: string; const setupFunc = (tempDir: string) => Promise.all( releasePlan.changesets.map(({ id, summary }) => { const thisPath = path.resolve(tempDir, ".changeset", `${id}.md`); changesetPath = thisPath; const content = `---\n---\n${summary}`; return fs.outputFile(thisPath, content); }) ); let { tempDir } = await testSetup( { "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "pkg-a", version: "1.0.0", dependencies: { "pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", }), }, releasePlan.getReleasePlan(), { ...releasePlan.config, commit: [ path.resolve(__dirname, "test-utils/simple-get-commit-entry"), null, ], }, undefined, setupFunc ); // @ts-ignore this is possibly bad let pathExists = await fs.pathExists(changesetPath); expect(pathExists).toEqual(false); let gitCmd = await spawn("git", ["status"], { cwd: tempDir }); const changesetsDeleted = releasePlan.changesets.reduce( (prev, { id }) => { return ( prev && gitCmd.stdout.toString().includes(`deleted: .changeset/${id}.md`) ); }, true ); expect(releasePlan.changesets.length).toBeGreaterThan(0); expect(changesetsDeleted).toBe(true); }); }); });