UNPKG

@unconfig/changeset-config

Version:

🚀 The package offer changeset-config for @unconfig!

346 lines (341 loc) • 11.6 kB
// 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 };