@unconfig/changeset-config
Version:
🚀 The package offer changeset-config for @unconfig!
346 lines (341 loc) • 11.6 kB
JavaScript
// src/changelog-generate.ts
import { readFileSync, writeFileSync } from "node:fs";
import assembleReleasePlan from "@changesets/assemble-release-plan";
import { read } from "@changesets/config";
import { readPreState } from "@changesets/pre";
import readChangesets from "@changesets/read";
import { getPackages } from "@manypkg/get-packages";
import { PROJECT_CHANGELOG_TEMP_FILE } from "@unconfig/meta";
function getReleaseSummary(changesets, release) {
const formattedChangesets = release.changesets.map((changeset) => {
const { summary = "" } = changesets.find((cs) => cs.id === changeset) ?? {};
const changes = summary.split("\n");
return changes.map(
(change) => !change || change?.trim().startsWith("-") ? change : `- ${change}
`
).join("");
});
const displayName = `**${release.name}** \`v${release.newVersion}\``;
return {
...release,
changesets: formattedChangesets,
displayName: displayName.replace(/,\s*$/, "")
};
}
async function getChangesetEntries(options) {
const {
packages: changelogPackges,
ignorePackages: ignoreChangelogPackages,
cwd
} = options;
const packages = await getPackages(cwd);
const preState = await readPreState(cwd);
const config = await read(cwd, packages);
const changesets = await readChangesets(cwd);
const releasePlan = assembleReleasePlan(
changesets,
packages,
config,
preState
);
const releases = releasePlan.releases.filter((release) => release.changesets.length > 0).filter((release) => !ignoreChangelogPackages.includes(release.name)).map((release) => getReleaseSummary(releasePlan.changesets, release)).sort((a, b) => {
if (changelogPackges.includes(a.name)) return -1;
if (changelogPackges.includes(b.name)) return 1;
return a.name < b.name ? -1 : 1;
});
return releases;
}
async function changesetsChangelogGenerate(options) {
const {
packages,
ignorePackages = [],
cwd,
projectChangelogTempPath = PROJECT_CHANGELOG_TEMP_FILE
} = options;
if (!cwd) return;
const releases = await getChangesetEntries({
packages,
ignorePackages,
cwd
});
if (!releases.length) return;
const content = JSON.parse(readFileSync(`${cwd}/${projectChangelogTempPath}`).toString()) || {};
releases.forEach(({ displayName, changesets }) => {
const prevState = content[displayName] || [];
content[displayName] = [.../* @__PURE__ */ new Set([...prevState, ...changesets])];
});
writeFileSync(`${cwd}/${projectChangelogTempPath}`, JSON.stringify(content));
}
// src/changelog-write.ts
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
import prettier from "prettier";
import {
PROJECT_CHANGELOG_FILE,
PROJECT_CHANGELOG_TEMP_FILE as PROJECT_CHANGELOG_TEMP_FILE2,
PROJECT_WEBSITE_CHANGELOG_FILE
} from "@unconfig/meta";
function getCurrentDate() {
const date = /* @__PURE__ */ new Date();
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
return [
`## ${day}-${month}-${year}`,
"\n\n",
`### ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`
].join("");
}
async function getContent(releases, prettierConfig) {
const releaseEntries = Object.entries(releases).map((release) => {
const [displayName, changesets] = release;
return [displayName, "\n\n", ...changesets].join("");
});
let content = [getCurrentDate(), ...releaseEntries].join("\n");
content = await prettier.format(content, prettierConfig);
return content;
}
async function changesetsChangelogWrite(options) {
const {
projectChangelogPath = PROJECT_CHANGELOG_FILE,
projectChangelogTempPath = PROJECT_CHANGELOG_TEMP_FILE2,
websiteChangelogPath = PROJECT_WEBSITE_CHANGELOG_FILE,
cwd,
prettierConfig = {
parser: "markdown",
singleQuote: true,
trailingComma: "es5"
}
} = options;
if (!cwd) return;
const releases = JSON.parse(
readFileSync2(`${cwd}/${projectChangelogTempPath}`).toString()
);
if (!Object.entries(releases).length) return;
const content = await getContent(releases, prettierConfig);
const changelog = readFileSync2(`${cwd}/${projectChangelogPath}`, "utf8");
const newChangelog = changelog.replace(
"<!-- CHANGELOG:INSERT -->",
`<!-- CHANGELOG:INSERT -->
${content}`
);
writeFileSync2(`${cwd}/${projectChangelogPath}`, newChangelog);
writeFileSync2(
`${cwd}/${websiteChangelogPath}`,
newChangelog.replace("<!-- CHANGELOG:INSERT -->\n\n", "")
);
writeFileSync2(`${cwd}/${projectChangelogTempPath}`, "{}");
}
// src/generate-releases.ts
import { resolve } from "node:path";
import { readFileSync as readFileSync3 } from "node:fs";
import { execSync } from "node:child_process";
import process from "node:process";
import { Octokit } from "@octokit/core";
import semver from "semver";
import { getPackages as getPackages2 } from "@manypkg/get-packages";
import {
PKG_PREFIX,
PROJECT_CHANGELOG_FILE as PROJECT_CHANGELOG_FILE2,
REPO_NAME,
REPO_OWNER
} from "@unconfig/meta";
async function createRelease(octokit, {
pkg,
tagName,
repoName,
repoOwner,
projectChangelogPath
}) {
const changelogPath = resolve(pkg.dir, projectChangelogPath);
const changelog = readFileSync3(changelogPath, "utf8");
const changelogArr = changelog.split("\n");
const releaseNotes = [];
for (const line of changelogArr) {
if (/^#{3}\s/.test(line)) releaseNotes.push(line);
else if (/^#{1,3}\s/.test(line) && releaseNotes.length > 0) break;
else if (releaseNotes.length > 0) releaseNotes.push(line);
}
const prereleaseParts = semver.prerelease(tagName.replace(`${pkg.packageJson.name}@`, "")) || [];
try {
const params = {
owner: repoOwner,
repo: repoName,
name: tagName,
tag_name: tagName,
body: releaseNotes.join("\n"),
prerelease: prereleaseParts.length > 0
};
await octokit.request("POST /repos/{owner}/{repo}/releases", params);
} catch (error) {
console.warn(
"[octokit 'POST /repos/{owner}/{repo}/releases'] has error.",
error
);
}
}
function getReleasedPackages(csOutput, pkgs, pkgPrefix) {
const tagNameRegex = new RegExp(
// eslint-disable-next-line no-useless-escape
`(${pkgPrefix}/[^@]+)@([^s]+)`
);
return csOutput.split("\n").reduce((acc, line) => {
const newTagPkgContent = line.includes("New tag:") ? line.split("New tag:")[1].trim() : "";
if (!newTagPkgContent) return acc;
const match = newTagPkgContent.match(tagNameRegex);
if (match === null) return acc;
const tagName = [match[1], match[2]].join("@");
const pkg = pkgs.find((p) => p.packageJson?.name === match[1]);
return [...acc, { tagName, pkg }];
}, []);
}
async function changesetsGenerateReleases(options) {
const {
repoName = REPO_NAME,
repoOwner = REPO_OWNER,
pkgPrefix = PKG_PREFIX,
projectChangelogPath = PROJECT_CHANGELOG_FILE2,
cwd
} = options;
if (!cwd) return;
const env = process.env;
const octokit = new Octokit({
auth: env.GITHUB_TOKEN
});
const publishCommandOutput = execSync("pnpm changeset publish").toString();
console.log(
`
\u{1F680}\u{1F680}\u{1F680} Run changesets publish and get stdout. \u{1F680}\u{1F680}\u{1F680}
${publishCommandOutput}
`
);
const gitPushCommand = `git pull && git add . && git diff --staged --quiet || git commit -m "docs: \u{1F4DD} add changelogs for $(git rev-parse --short HEAD) [skip ci]" && git push origin ${env.GITHUB_BRANCH} --follow-tags`;
const gitPushCommandOutput = execSync(gitPushCommand).toString();
console.log(
`
\u{1F680}\u{1F680}\u{1F680} Push updated packages to github with tags. \u{1F680}\u{1F680}\u{1F680}
${gitPushCommandOutput}
`
);
const { packages: pkgs } = await getPackages2(cwd);
const releasedPkgs = getReleasedPackages(
publishCommandOutput,
pkgs,
pkgPrefix
);
for (const pkg of releasedPkgs) {
await createRelease(octokit, {
...pkg,
repoName,
repoOwner,
projectChangelogPath
});
}
console.log(`\u{1F680}\u{1F680}\u{1F680} Create release for each published package. \u{1F680}\u{1F680}\u{1F680}`);
}
// src/manual-generate-prereleases.ts
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
import { resolve as resolve2 } from "node:path";
import { exec } from "node:child_process";
import inquirer from "inquirer";
import semver2 from "semver";
import { getPackages as getPackages3 } from "@manypkg/get-packages";
import {
NPM_REGISTRY_URL,
PROJECT_CHANGESETS_CONFIG_FILE
} from "@unconfig/meta";
function updatePackageJson(pkgJsonPath, version) {
const pkgRaw = readFileSync4(pkgJsonPath, { encoding: "utf-8" });
const stringified = pkgRaw.replace(
/("version".*?) (".*?")/i,
`$1 "${version}"`
);
writeFileSync3(pkgJsonPath, stringified);
}
async function ignorePackage(pkgName, cwd, projectChangesetsPath) {
const changesetConfigPath = resolve2(cwd, projectChangesetsPath);
const rawConfig = readFileSync4(changesetConfigPath, {
encoding: "utf-8"
});
const jsonConfig = JSON.parse(rawConfig);
const ignorePkgs = jsonConfig.ignore || [];
if (!ignorePkgs.includes(pkgName)) {
jsonConfig.ignore = [pkgName, ...ignorePkgs];
const stringified = JSON.stringify(jsonConfig, null, 2);
await writeFileSync3(changesetConfigPath, stringified, { encoding: "utf-8" });
}
}
async function manualGeneratePrereleases(options) {
const {
npmRegistryUrl = NPM_REGISTRY_URL,
projectChangesetsPath = PROJECT_CHANGESETS_CONFIG_FILE,
cwd
} = options;
if (!cwd) return;
const { packages } = await getPackages3(cwd);
const choices = packages.map(({ packageJson: packageJson2 }) => ({
name: `${packageJson2.name} (${packageJson2.version})`,
value: packageJson2.name
})).concat(new inquirer.Separator());
const { pkgName } = await inquirer.prompt([
{
pageSize: 12,
name: "pkgName",
message: "Which package to make a pre-release?",
type: "list",
choices
}
]);
const { packageJson, dir } = packages.find(
({ packageJson: packageJson2 }) => packageJson2.name === pkgName
);
const { version, name } = packageJson;
const prereleaseTag = semver2.prerelease(version)?.[0];
const { tag, publish } = await inquirer.prompt([
{
name: "tag",
message: "Which tag should be used for the pre-release?",
type: "list",
choices: [
{
name: "alpha"
},
{
name: "beta"
}
],
default: prereleaseTag
},
{
name: "publish",
message: "Should the package be published?",
type: "confirm"
}
]);
const increase = prereleaseTag === tag ? "prerelease" : "preminor";
const newVersion = semver2.inc(version, increase, tag);
await updatePackageJson(resolve2(dir, "package.json"), newVersion);
await ignorePackage(name, cwd, projectChangesetsPath);
if (publish) {
await exec(`npm publish ${dir} --tag ${tag}`, (error, stdout, stderr) => {
if (!error) {
console.log(stdout);
console.log(
`${name}@${newVersion} published:
${npmRegistryUrl}/${name}
`
);
} else {
console.error(error);
console.error(stderr);
}
});
} else {
console.log(`Version for ${name} updated on package.json.`);
}
}
export {
changesetsChangelogGenerate,
changesetsChangelogWrite,
changesetsGenerateReleases,
manualGeneratePrereleases
};