360 lines
9.9 KiB
Plaintext
360 lines
9.9 KiB
Plaintext
import {
|
|
ReleasePlan,
|
|
Config,
|
|
ChangelogFunctions,
|
|
NewChangeset,
|
|
ModCompWithPackage,
|
|
} from "@changesets/types";
|
|
|
|
import { defaultConfig } from "@changesets/config";
|
|
import * as git from "@changesets/git";
|
|
import resolveFrom from "resolve-from";
|
|
import { Packages } from "@manypkg/get-packages";
|
|
import detectIndent from "detect-indent";
|
|
|
|
import fs from "fs-extra";
|
|
import path from "path";
|
|
import prettier from "prettier";
|
|
|
|
import versionPackage from "./version-package";
|
|
import getChangelogEntry from "./get-changelog-entry";
|
|
|
|
function getPrettierInstance(cwd: string): typeof prettier {
|
|
try {
|
|
return require(require.resolve("prettier", { paths: [cwd] }));
|
|
} catch (err) {
|
|
if (!err || (err as any).code !== "MODULE_NOT_FOUND") {
|
|
throw err;
|
|
}
|
|
return prettier;
|
|
}
|
|
}
|
|
|
|
function stringDefined(s: string | undefined): s is string {
|
|
return !!s;
|
|
}
|
|
async function getCommitsThatAddChangesets(
|
|
changesetIds: string[],
|
|
cwd: string
|
|
) {
|
|
const paths = changesetIds.map((id) => `.changeset/${id}.md`);
|
|
const commits = await git.getCommitsThatAddFiles(paths, { cwd });
|
|
|
|
if (commits.every(stringDefined)) {
|
|
// We have commits for all files
|
|
return commits;
|
|
}
|
|
|
|
// Some files didn't exist. Try legacy filenames instead
|
|
const missingIds = changesetIds
|
|
.map((id, i) => (commits[i] ? undefined : id))
|
|
.filter(stringDefined);
|
|
|
|
const legacyPaths = missingIds.map((id) => `.changeset/${id}/changes.json`);
|
|
const commitsForLegacyPaths = await git.getCommitsThatAddFiles(legacyPaths, {
|
|
cwd,
|
|
});
|
|
|
|
// Fill in the blanks in the array of commits
|
|
changesetIds.forEach((id, i) => {
|
|
if (!commits[i]) {
|
|
const missingIndex = missingIds.indexOf(id);
|
|
commits[i] = commitsForLegacyPaths[missingIndex];
|
|
}
|
|
});
|
|
|
|
return commits;
|
|
}
|
|
|
|
export default async function applyReleasePlan(
|
|
releasePlan: ReleasePlan,
|
|
packages: Packages,
|
|
config: Config = defaultConfig,
|
|
snapshot?: string | boolean
|
|
) {
|
|
let cwd = packages.root.dir;
|
|
|
|
let touchedFiles = [];
|
|
|
|
const packagesByName = new Map(
|
|
packages.packages.map((x) => [x.packageJson.name, x])
|
|
);
|
|
|
|
let { releases, changesets } = releasePlan;
|
|
|
|
let releasesWithPackage = releases.map((release) => {
|
|
let pkg = packagesByName.get(release.name);
|
|
if (!pkg)
|
|
throw new Error(
|
|
`Could not find matching package for release of: ${release.name}`
|
|
);
|
|
return {
|
|
...release,
|
|
...pkg,
|
|
};
|
|
});
|
|
|
|
// I think this might be the wrong place to do this, but gotta do it somewhere - add changelog entries to releases
|
|
let releaseWithChangelogs = await getNewChangelogEntry(
|
|
releasesWithPackage,
|
|
changesets,
|
|
config,
|
|
cwd
|
|
);
|
|
|
|
if (releasePlan.preState !== undefined && snapshot === undefined) {
|
|
if (releasePlan.preState.mode === "exit") {
|
|
await fs.remove(path.join(cwd, ".changeset", "pre.json"));
|
|
} else {
|
|
await fs.writeFile(
|
|
path.join(cwd, ".changeset", "pre.json"),
|
|
JSON.stringify(releasePlan.preState, null, 2) + "\n"
|
|
);
|
|
}
|
|
}
|
|
|
|
let versionsToUpdate = releases.map(({ name, newVersion, type }) => ({
|
|
name,
|
|
version: newVersion,
|
|
type,
|
|
}));
|
|
|
|
// iterate over releases updating packages
|
|
let finalisedRelease = releaseWithChangelogs.map((release) => {
|
|
return versionPackage(release, versionsToUpdate, {
|
|
updateInternalDependencies: config.updateInternalDependencies,
|
|
onlyUpdatePeerDependentsWhenOutOfRange:
|
|
config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH
|
|
.onlyUpdatePeerDependentsWhenOutOfRange,
|
|
bumpVersionsWithWorkspaceProtocolOnly:
|
|
config.bumpVersionsWithWorkspaceProtocolOnly,
|
|
snapshot,
|
|
});
|
|
});
|
|
|
|
let prettierInstance = getPrettierInstance(cwd);
|
|
let prettierConfig = await prettierInstance.resolveConfig(cwd);
|
|
|
|
for (let release of finalisedRelease) {
|
|
let { changelog, packageJson, dir, name } = release;
|
|
|
|
const pkgJSONPath = path.resolve(dir, "package.json");
|
|
await updatePackageJson(pkgJSONPath, packageJson);
|
|
touchedFiles.push(pkgJSONPath);
|
|
|
|
if (changelog && changelog.length > 0) {
|
|
const changelogPath = path.resolve(dir, "CHANGELOG.md");
|
|
await updateChangelog(
|
|
changelogPath,
|
|
changelog,
|
|
name,
|
|
prettierInstance,
|
|
prettierConfig
|
|
);
|
|
touchedFiles.push(changelogPath);
|
|
}
|
|
}
|
|
|
|
if (
|
|
releasePlan.preState === undefined ||
|
|
releasePlan.preState.mode === "exit"
|
|
) {
|
|
let changesetFolder = path.resolve(cwd, ".changeset");
|
|
await Promise.all(
|
|
changesets.map(async (changeset) => {
|
|
let changesetPath = path.resolve(changesetFolder, `${changeset.id}.md`);
|
|
let changesetFolderPath = path.resolve(changesetFolder, changeset.id);
|
|
if (await fs.pathExists(changesetPath)) {
|
|
// DO NOT remove changeset for ignored packages
|
|
// Mixed changeset that contains both ignored packages and not ignored packages are disallowed
|
|
// At this point, we know there is no such changeset, because otherwise the program would've already failed,
|
|
// so we just check if any ignored package exists in this changeset, and only remove it if none exists
|
|
// Ignored list is added in v2, so we don't need to do it for v1 changesets
|
|
if (
|
|
!changeset.releases.find((release) =>
|
|
config.ignore.includes(release.name)
|
|
)
|
|
) {
|
|
touchedFiles.push(changesetPath);
|
|
await fs.remove(changesetPath);
|
|
}
|
|
// TO REMOVE LOGIC - this works to remove v1 changesets. We should be removed in the future
|
|
} else if (await fs.pathExists(changesetFolderPath)) {
|
|
touchedFiles.push(changesetFolderPath);
|
|
await fs.remove(changesetFolderPath);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
// We return the touched files to be committed in the cli
|
|
return touchedFiles;
|
|
}
|
|
|
|
async function getNewChangelogEntry(
|
|
releasesWithPackage: ModCompWithPackage[],
|
|
changesets: NewChangeset[],
|
|
config: Config,
|
|
cwd: string
|
|
) {
|
|
if (!config.changelog) {
|
|
return Promise.resolve(
|
|
releasesWithPackage.map((release) => ({
|
|
...release,
|
|
changelog: null,
|
|
}))
|
|
);
|
|
}
|
|
|
|
let getChangelogFuncs: ChangelogFunctions = {
|
|
getReleaseLine: () => Promise.resolve(""),
|
|
getDependencyReleaseLine: () => Promise.resolve(""),
|
|
};
|
|
|
|
const changelogOpts = config.changelog[1];
|
|
let changesetPath = path.join(cwd, ".changeset");
|
|
let changelogPath = resolveFrom(changesetPath, config.changelog[0]);
|
|
|
|
let possibleChangelogFunc = require(changelogPath);
|
|
if (possibleChangelogFunc.default) {
|
|
possibleChangelogFunc = possibleChangelogFunc.default;
|
|
}
|
|
if (
|
|
typeof possibleChangelogFunc.getReleaseLine === "function" &&
|
|
typeof possibleChangelogFunc.getDependencyReleaseLine === "function"
|
|
) {
|
|
getChangelogFuncs = possibleChangelogFunc;
|
|
} else {
|
|
throw new Error("Could not resolve changelog generation functions");
|
|
}
|
|
|
|
let commits = await getCommitsThatAddChangesets(
|
|
changesets.map((cs) => cs.id),
|
|
cwd
|
|
);
|
|
let moddedChangesets = changesets.map((cs, i) => ({
|
|
...cs,
|
|
commit: commits[i],
|
|
}));
|
|
|
|
return Promise.all(
|
|
releasesWithPackage.map(async (release) => {
|
|
let changelog = await getChangelogEntry(
|
|
release,
|
|
releasesWithPackage,
|
|
moddedChangesets,
|
|
getChangelogFuncs,
|
|
changelogOpts,
|
|
{
|
|
updateInternalDependencies: config.updateInternalDependencies,
|
|
onlyUpdatePeerDependentsWhenOutOfRange:
|
|
config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH
|
|
.onlyUpdatePeerDependentsWhenOutOfRange,
|
|
}
|
|
);
|
|
|
|
return {
|
|
...release,
|
|
changelog,
|
|
};
|
|
})
|
|
).catch((e) => {
|
|
console.error(
|
|
"The following error was encountered while generating changelog entries"
|
|
);
|
|
console.error(
|
|
"We have escaped applying the changesets, and no files should have been affected"
|
|
);
|
|
throw e;
|
|
});
|
|
}
|
|
|
|
async function updateChangelog(
|
|
changelogPath: string,
|
|
changelog: string,
|
|
name: string,
|
|
prettierInstance: typeof prettier,
|
|
prettierConfig: prettier.Options | null
|
|
) {
|
|
let templateString = `\n\n${changelog.trim()}\n`;
|
|
|
|
try {
|
|
if (fs.existsSync(changelogPath)) {
|
|
await prependFile(
|
|
changelogPath,
|
|
templateString,
|
|
name,
|
|
prettierInstance,
|
|
prettierConfig
|
|
);
|
|
} else {
|
|
await writeFormattedMarkdownFile(
|
|
changelogPath,
|
|
`# ${name}${templateString}`,
|
|
prettierInstance,
|
|
prettierConfig
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
}
|
|
|
|
async function updatePackageJson(
|
|
pkgJsonPath: string,
|
|
pkgJson: any
|
|
): Promise<void> {
|
|
const pkgRaw = await fs.readFile(pkgJsonPath, "utf-8");
|
|
const indent = detectIndent(pkgRaw).indent || " ";
|
|
const stringified =
|
|
JSON.stringify(pkgJson, null, indent) + (pkgRaw.endsWith("\n") ? "\n" : "");
|
|
|
|
return fs.writeFile(pkgJsonPath, stringified);
|
|
}
|
|
|
|
async function prependFile(
|
|
filePath: string,
|
|
data: string,
|
|
name: string,
|
|
prettierInstance: typeof prettier,
|
|
prettierConfig: prettier.Options | null
|
|
) {
|
|
const fileData = fs.readFileSync(filePath).toString();
|
|
// if the file exists but doesn't have the header, we'll add it in
|
|
if (!fileData) {
|
|
const completelyNewChangelog = `# ${name}${data}`;
|
|
await writeFormattedMarkdownFile(
|
|
filePath,
|
|
completelyNewChangelog,
|
|
prettierInstance,
|
|
prettierConfig
|
|
);
|
|
return;
|
|
}
|
|
const newChangelog = fileData.replace("\n", data);
|
|
|
|
await writeFormattedMarkdownFile(
|
|
filePath,
|
|
newChangelog,
|
|
prettierInstance,
|
|
prettierConfig
|
|
);
|
|
}
|
|
|
|
async function writeFormattedMarkdownFile(
|
|
filePath: string,
|
|
content: string,
|
|
prettierInstance: typeof prettier,
|
|
prettierConfig: prettier.Options | null
|
|
) {
|
|
await fs.writeFile(
|
|
filePath,
|
|
// Prettier v3 returns a promise
|
|
await prettierInstance.format(content, {
|
|
...prettierConfig,
|
|
filepath: filePath,
|
|
parser: "markdown",
|
|
})
|
|
);
|
|
}
|