@changesets/cli
Version:
A tool to manage versioning and changelogs with a focus on monorepos
679 lines (678 loc) • 22.6 kB
JavaScript
import { t as src_default } from "./src.mjs";
import { n as splitByTagStatus } from "./gitTags.mjs";
import { stripVTControlCharacters } from "node:util";
import { ExitError } from "@changesets/errors";
import { log } from "@clack/prompts";
import path from "node:path";
import { shouldSkipPackage } from "@changesets/should-skip-package";
import { getPackages } from "@manypkg/get-packages";
import fs from "node:fs/promises";
import { getDependentsGraph } from "@changesets/get-dependents-graph";
import { readPreState } from "@changesets/pre";
import { graphSequencer } from "@pnpm/deps.graph-sequencer";
import semverParse from "semver/functions/parse.js";
import { detect } from "package-manager-detector/detect";
import { exec } from "tinyexec";
import assert from "node:assert/strict";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region src/ponyfills/promise-try.ts
const promiseTryPolyfill = (fn, ...args) => new Promise((resolve) => resolve(fn(...args)));
const promiseTry = "try" in Promise ? Promise.try.bind(Promise) : promiseTryPolyfill;
//#endregion
//#region src/utils/createPromiseQueue.ts
function createPromiseQueue(concurrency) {
const jobs = [];
let active = 0;
function run() {
if (active >= concurrency) return;
const job = jobs.shift();
if (!job) return;
active++;
promiseTry(job.fn).then((result) => {
active--;
job.resolve(result);
run();
}, (error) => {
active--;
job.reject(error);
run();
});
}
return { add: (fn) => {
const { promise, resolve, reject } = Promise.withResolvers();
jobs.push({
fn,
resolve,
reject
});
run();
return promise;
} };
}
const npmRequestQueue = createPromiseQueue(40);
const npmPublishQueue = createPromiseQueue(10);
function isAlreadyPublishedError(message) {
return message.includes("cannot publish over the previously published");
}
const isPublishSuccessful = (result) => result.result === "published";
const isPublishFailure = (result) => result.result.startsWith("failed");
//#endregion
//#region src/utils/getLastJsonObjectFromString.ts
const getLastJsonObjectFromString = (str) => {
str = str.replace(/[^}]*$/, "");
while (str) {
str = str.replace(/[^{]*/, "");
try {
return JSON.parse(str);
} catch {
str = str.slice(1);
}
}
return null;
};
//#endregion
//#region src/lib/npm.ts
var npm_exports = /* @__PURE__ */ __exportAll({
getOtpCode: () => getOtpCode$2,
handlePublishError: () => handlePublishError,
info: () => info$2,
name: () => "npm",
pack: () => pack$2,
publish: () => publish$2
});
function isNpmPublishError(err) {
return err != null && typeof err === "object" && "error" in err && err.error != null && typeof err.error === "object" && "summary" in err.error;
}
function sanitizeEnv$1(env) {
return {
...env,
NPM_CONFIG_OTP: void 0,
npm_config_otp: void 0
};
}
function isJsonObject$1(value) {
return typeof value === "object" && value != null && !Array.isArray(value);
}
function formatJsonError(error) {
if (!isJsonObject$1(error)) return;
let message = typeof error.message === "string" ? error.message : typeof error.summary === "string" ? error.summary : void 0;
if (typeof error.detail === "string" && error.detail) message = message ? `${message}\n${error.detail}` : error.detail;
return {
code: typeof error.code === "string" ? error.code : void 0,
message
};
}
function getNpmError(stdout, stderr) {
const json = getLastJsonObjectFromString(stdout);
if (json?.error) {
const jsonError = formatJsonError(json.error);
if (jsonError) return jsonError;
}
return { message: stderr || stdout || void 0 };
}
function parseInfoResult$1({ exitCode, stdout, stderr }) {
if (exitCode !== 0) return { error: getNpmError(stdout, stderr) };
if (!stdout) return;
const parsed = JSON.parse(stdout);
if (Array.isArray(parsed)) {
assert(parsed.length === 1, "Unexpected empty array output from npm info --json");
return { info: parsed[0] };
}
return { info: parsed };
}
function getRegistryOverrides(packageJson) {
const registryOverrides = [];
if (packageJson.name.startsWith("@")) {
const scope = packageJson.name.split("/")[0];
if (packageJson.publishConfig?.[`${scope}:registry`]) registryOverrides.push(`--${scope}:registry=${packageJson.publishConfig[`${scope}:registry`]}`);
if (packageJson.publishConfig?.registry) registryOverrides.push(`--registry=${packageJson.publishConfig.registry}`);
} else if (packageJson.publishConfig?.registry) registryOverrides.push(`--registry=${packageJson.publishConfig.registry}`);
return registryOverrides;
}
const info$2 = async ({ cwd, pkg }) => {
const { packageJson } = pkg;
const flags = [...getRegistryOverrides(packageJson), "--json"];
let info = parseInfoResult$1(await exec("npm", [
"info",
packageJson.name,
...flags
], {
nodePath: false,
nodeOptions: { cwd }
}));
if (!info) info = parseInfoResult$1(await exec("npm", [
"info",
`${packageJson.name}@${packageJson.version}`,
...flags
], {
nodePath: false,
nodeOptions: { cwd }
})) ?? { error: { code: "E404" } };
if ("error" in info) return info.error.code === "E404" ? { published: false } : { error: info.error };
return {
published: true,
info: info.info
};
};
const pack$2 = async ({ pkg, packDir, outputDir }) => {
const { exitCode, stdout, stderr } = await exec("npm", [
"pack",
packDir,
"--pack-destination",
outputDir,
"--json"
], {
nodePath: false,
nodeOptions: { cwd: pkg.dir }
});
if (exitCode !== 0) return { error: getNpmError(stdout, stderr) };
const json = getLastJsonObjectFromString(stdout);
let filename = Array.isArray(json) ? json[0]?.filename : json?.filename;
if (!filename) {
const pkgOutput = Object.values(json ?? {})[0];
filename = pkgOutput && typeof pkgOutput === "object" && "filename" in pkgOutput && pkgOutput.filename;
}
assert(typeof filename === "string", "Failed to determine tarball filename");
return { tarballPath: path.join(outputDir, path.basename(filename)) };
};
function getOtpCode$2(otp) {
return otp || process.env.NPM_CONFIG_OTP || process.env.npm_config_otp || null;
}
function handlePublishError(resultBase, json, processOutput) {
if (!isNpmPublishError(json)) return {
...resultBase,
result: "failed",
message: processOutput || void 0
};
if (isAlreadyPublishedError(json.error.summary)) return {
...resultBase,
result: "failed:already-published",
code: json.error.code
};
const message = `
${json.error.summary ?? ""}
${json.error.detail ?? ""}
`.trim();
if (json.error.code === "EOTP") {
const result = {
...resultBase,
result: "failed:needs-2fa",
code: json.error.code,
message: message || void 0
};
if ("authUrl" in json.error) {
result.authUrl = json.error.authUrl;
result.doneUrl = json.error.doneUrl;
}
return result;
}
return {
...resultBase,
result: "failed",
code: json.error.code,
message: message || void 0
};
}
const publish$2 = async ({ pkg, release, tarballPath, interactive, otpCode }) => {
const cwd = pkg.dir;
const args = [
"--access",
release.access,
"--tag",
release.tag
];
if (!interactive) args.unshift("--json");
if (otpCode) args.push("--otp", otpCode);
if (tarballPath) args.unshift(path.relative(cwd, tarballPath));
else if (pkg.packageJson.publishConfig?.directory != null) args.unshift(path.resolve(cwd, pkg.packageJson.publishConfig.directory));
const { exitCode, stdout, stderr } = await exec("npm", ["publish", ...args], {
nodePath: false,
nodeOptions: {
stdio: interactive ? "inherit" : "pipe",
env: sanitizeEnv$1(process.env),
cwd
}
});
const resultBase = {
name: release.name,
version: release.version
};
if (exitCode === 0) return {
...resultBase,
result: "published"
};
const json = getLastJsonObjectFromString(stdout);
if (!json) return {
...resultBase,
result: "failed",
message: stderr || stdout
};
return handlePublishError(resultBase, json, stderr || stdout);
};
//#endregion
//#region src/lib/pnpm.ts
var pnpm_exports = /* @__PURE__ */ __exportAll({
getOtpCode: () => getOtpCode$1,
info: () => info$1,
name: () => name$1,
pack: () => pack$1,
publish: () => publish$1
});
function isPnpmPublishError(error) {
return error != null && typeof error === "object" && "error" in error && typeof error.error === "object" && error.error != null && "code" in error.error && "message" in error.error;
}
function sanitizeEnv(env) {
return {
...env,
PNPM_CONFIG_OTP: void 0,
pnpm_config_otp: void 0,
NPM_CONFIG_OTP: void 0,
npm_config_otp: void 0
};
}
function getPnpmError(stderr, stdout) {
const error = getLastJsonObjectFromString(stdout)?.error;
if (error && typeof error === "object" && !Array.isArray(error)) return {
code: typeof error.code === "string" ? error.code : void 0,
message: typeof error.message === "string" ? error.message : void 0
};
return { message: stderr || stdout || void 0 };
}
const name$1 = "pnpm";
function parseInfoResult({ exitCode, stdout, stderr }) {
if (exitCode !== 0) return { error: getPnpmError(stderr, stdout) };
return stdout ? { info: JSON.parse(stdout) } : void 0;
}
const info$1 = async ({ cwd, pkg }) => {
const { packageJson } = pkg;
let info = parseInfoResult(await exec("pnpm", [
"info",
packageJson.name,
"--json"
], {
nodePath: false,
nodeOptions: { cwd }
}));
if (!info || "error" in info && info.error.code === "ERR_PNPM_PACKAGE_NOT_FOUND") info = parseInfoResult(await exec("pnpm", [
"info",
`${packageJson.name}@${packageJson.version}`,
"--json"
], {
nodePath: false,
nodeOptions: { cwd }
})) ?? { error: { code: "E404" } };
if ("error" in info) return info.error.code === "E404" || info.error.code === "ERR_PNPM_FETCH_404" || info.error.code === "ERR_PNPM_PACKAGE_NOT_FOUND" ? { published: false } : { error: info.error };
return {
published: true,
info: info.info
};
};
const pack$1 = async ({ pkg, tarballPath }) => {
const { exitCode, stdout, stderr } = await exec("pnpm", [
"pack",
"--out",
tarballPath,
"--json"
], {
nodePath: false,
nodeOptions: { cwd: pkg.dir }
});
if (exitCode !== 0) return { error: getPnpmError(stderr, stdout) };
return { tarballPath };
};
const getOtpCode$1 = (otp) => otp || process.env.PNPM_CONFIG_OTP || process.env.pnpm_config_otp || process.env.NPM_CONFIG_OTP || process.env.npm_config_otp || null;
const publish$1 = async ({ pkg, release, tarballPath, interactive, otpCode }) => {
const cwd = pkg.dir;
const args = [
"--access",
release.access,
"--tag",
release.tag,
"--no-git-checks"
];
if (!interactive) args.unshift("--json");
if (otpCode) args.push("--otp", otpCode);
if (tarballPath) args.unshift(path.relative(cwd, tarballPath));
const { exitCode, stdout, stderr } = await exec("pnpm", ["publish", ...args], {
nodePath: false,
nodeOptions: {
stdio: interactive ? "inherit" : "pipe",
env: sanitizeEnv(process.env),
cwd
}
});
const resultBase = {
name: release.name,
version: release.version
};
if (exitCode === 0) return {
...resultBase,
result: "published"
};
const json = getLastJsonObjectFromString(stdout);
if (!json) return {
...resultBase,
result: "failed",
message: stderr || stdout || void 0
};
if (!isPnpmPublishError(json)) return handlePublishError(resultBase, json, stderr || stdout);
if (isAlreadyPublishedError(json.error.message)) return {
...resultBase,
result: "failed:already-published",
code: json.error.code
};
const message = json.error.message.trim();
if (json.error.code === "ERR_PNPM_OTP_NON_INTERACTIVE") return {
...resultBase,
result: "failed:needs-2fa",
code: json.error.code,
message: message || void 0,
authUrl: json.error.authUrl,
doneUrl: json.error.doneUrl
};
return {
...resultBase,
result: "failed",
code: json.error.code,
message: message || void 0
};
};
//#endregion
//#region src/utils/streamNdjson.ts
function* streamNdjson(output) {
let lineStart = 0;
while (lineStart <= output.length) {
let lineEnd = output.indexOf("\n", lineStart);
if (lineEnd === -1) lineEnd = output.length;
const line = output.slice(lineStart, lineEnd);
lineStart = lineEnd + 1;
if (/^\s*$/.test(line)) continue;
try {
yield JSON.parse(line);
} catch {
continue;
}
}
}
//#endregion
//#region src/lib/yarn.ts
var yarn_exports = /* @__PURE__ */ __exportAll({
getOtpCode: () => getOtpCode,
getYarnBerryReporterError: () => getYarnBerryReporterError,
info: () => info,
name: () => name,
pack: () => pack,
publish: () => publish
});
const name = "yarn";
function isJsonObject(value) {
return typeof value === "object" && value != null && !Array.isArray(value);
}
function isYarnBerryReporterEvent(event) {
if (!isJsonObject(event) || event.type !== "error") return false;
return typeof event.name === "number" && typeof event.data === "string";
}
function getYarnBerryReporterError(output) {
const errors = [];
let code;
for (const event of streamNdjson(output)) {
if (!isYarnBerryReporterEvent(event)) continue;
const error = {
code: typeof event.displayName === "string" && event.displayName ? event.displayName : `YN${String(event.name).padStart(4, "0")}`,
message: stripVTControlCharacters(event.data)
};
if (error.message.startsWith("Failed with errors")) continue;
if (errors.length > 0 && error.code !== code) break;
code = error.code;
errors.push(error);
}
if (!errors.length) return;
return {
code: errors[0].code,
message: errors.map((error) => error.message).join("\n")
};
}
const info = async ({ cwd, pkg }) => {
const { packageJson } = pkg;
const result = await exec("yarn", [
"npm",
"info",
packageJson.name,
"--json"
], {
nodePath: false,
nodeOptions: { cwd }
});
if (result.exitCode !== 0) {
const error = getYarnBerryReporterError(result.stdout) ?? { message: result.stderr || result.stdout };
return error.code === "YN0035" && error.message?.includes("Response Code: 404") === true ? { published: false } : { error };
}
let info;
for (const entry of streamNdjson(result.stdout)) info = entry;
return info ? {
published: true,
info
} : { error: {} };
};
const pack = async ({ packDir, tarballPath }) => {
const { exitCode, stdout, stderr } = await exec("yarn", [
"pack",
"--out",
tarballPath,
"--json"
], {
nodePath: false,
nodeOptions: { cwd: packDir }
});
if (exitCode !== 0) return { error: getYarnBerryReporterError(stdout) || { message: stderr || stdout } };
return { tarballPath };
};
const getOtpCode = (otp) => otp || null;
const publish = async ({ pkg, release, tarballPath, interactive, otpCode }) => {
const resultBase = {
name: release.name,
version: release.version
};
if (pkg.packageJson.publishConfig?.directory) return {
...resultBase,
result: "failed",
message: `Package has publishConfig.directory configured in its package.json, which is not supported when using Yarn.`
};
if (tarballPath) return {
...resultBase,
result: "failed",
message: "Yarn does not support publishing from a tarball."
};
const args = [
"npm",
"publish",
"--access",
release.access,
"--tag",
release.tag
];
if (!interactive) args.push("--json");
if (otpCode) args.push("--otp", otpCode);
const { exitCode, stdout, stderr } = await exec("yarn", args, {
nodePath: false,
...!interactive && { stdin: "not-otp\n" },
nodeOptions: {
cwd: pkg.dir,
stdio: interactive ? "inherit" : "pipe"
}
});
if (exitCode === 0) return {
...resultBase,
result: "published"
};
const reporterError = getYarnBerryReporterError(stdout);
if (!reporterError) return {
...resultBase,
result: "failed",
message: stderr || stdout
};
if (reporterError.code === "YN0035" && isAlreadyPublishedError(reporterError.message)) return {
...resultBase,
result: "failed:already-published",
code: reporterError.code
};
if (reporterError.code === "YN0033" || /\b(otp|one-time password|authentication)\b/i.test(reporterError.message)) return {
...resultBase,
result: "failed:needs-2fa",
code: reporterError.code,
message: reporterError.message
};
return {
...resultBase,
result: "failed",
code: reporterError.code,
message: reporterError.message
};
};
//#endregion
//#region src/commands/publish/getPublishTool.ts
async function getYarnVersion(packages) {
const { stdout } = await exec("yarn", ["--version"], {
nodePath: false,
nodeOptions: { cwd: packages.rootDir }
});
const major = Number(stdout.toString().trim().split(".")[0]);
return Number.isNaN(major) || major >= 2 ? "berry" : "classic";
}
async function getPublishTool(packages) {
let packageManager = packages.tool.type;
if (![
"npm",
"pnpm",
"yarn"
].includes(packageManager)) packageManager = (await detect({ cwd: packages.rootDir }))?.name ?? "npm";
if (packageManager === "pnpm") return pnpm_exports;
if (packageManager === "yarn") {
if (await getYarnVersion(packages) === "classic") throw new Error("Yarn Classic is not supported. Please upgrade to Yarn Berry or another maintained package manager.");
return yarn_exports;
}
return npm_exports;
}
async function readPlanFile(filePath) {
const json = JSON.parse(await fs.readFile(filePath, "utf8"));
if (!json || typeof json !== "object") throw new Error("Invalid publish plan file");
if (!Array.isArray(json.plan)) throw new Error("Invalid publish plan file");
if (json.version !== 1) throw new Error(`Invalid publish plan file version: expected 1, received ${String(json.version)}`);
return json.plan;
}
function getReleaseTag(publishedState, preState, tag) {
if (tag) return tag;
if (preState != null && publishedState !== "only-pre") return preState.tag;
return "latest";
}
async function getUnpublishedPackages(packages, preState, access, options) {
const publishTool = await getPublishTool(packages);
const results = await Promise.all(packages.packages.filter((pkg) => !pkg.packageJson.private && !shouldSkipPackage(pkg, options)).map(async (pkg) => {
const response = await npmRequestQueue.add(() => publishTool.info({
cwd: packages.rootDir,
pkg
}));
if ("error" in response) {
log.error(`
Received an unexpected error for ${src_default.cyan(pkg.packageJson.name)}: ${response.error.code || "(no code)"}
${response.error.message || "Unknown error"}
`.trim());
throw new ExitError(1);
}
if (!response.published) log.warn(`Package ${src_default.cyan(pkg.packageJson.name)} was not found in the registry.`);
let publishedState = "never";
let publishedVersions = [];
if (response.published) {
publishedState = "published";
publishedVersions = response.info.versions;
if (preState != null && response.info["dist-tags"].latest && response.info.versions.every((version) => semverParse(version).prerelease[0] === preState.tag)) publishedState = "only-pre";
}
return {
pkg,
publishedState,
publishedVersions
};
}));
const packagesToPublish = [];
const previewLines = [];
let alreadyPublishedCount = 0;
for (const result of results) {
const { pkg, publishedState, publishedVersions } = result;
const localVersion = pkg.packageJson.version;
if (!publishedVersions.includes(localVersion)) {
const release = {
kind: "publish",
name: pkg.packageJson.name,
version: localVersion,
access: pkg.packageJson.publishConfig?.access || access,
tag: getReleaseTag(publishedState, preState, options.tag)
};
packagesToPublish.push(release);
previewLines.push(`${src_default.blue(release.name)}@${src_default.green(release.version)}`);
if (preState != null && publishedState === "only-pre") previewLines.push(`${src_default.gray("└")} will be published to ${src_default.cyan("latest")} rather than ${src_default.cyan(preState.tag)} as it will be its first published version.`);
} else alreadyPublishedCount++;
}
if (packagesToPublish.length !== 0) log.info(`
These packages will be published as they were not found in the registry:
${previewLines.join("\n")}
${src_default.gray(`${alreadyPublishedCount} packages are already published.`)}
`.trim());
return packagesToPublish;
}
async function getUntaggedPrivatePackages(cwd, packages, tool, options) {
return (await splitByTagStatus(cwd, tool, packages.filter((pkg) => pkg.packageJson.private && !shouldSkipPackage(pkg, options)).map((pkg) => ({
kind: "tag-only",
name: pkg.packageJson.name,
version: pkg.packageJson.version
})))).untagged;
}
function sortReleases(packages, releases, opts) {
const dependentsGraph = getDependentsGraph(packages, {
bumpVersionsWithWorkspaceProtocolOnly: opts.bumpVersionsWithWorkspaceProtocolOnly,
ignoreDevDependencies: true
});
const releasesByName = new Map(releases.map((release) => {
if (!dependentsGraph.has(release.name)) throw new Error(`Package referenced by release entry not found: ${release.name}`);
return [release.name, release];
}));
const graph = new Map(releases.map((release) => [release, []]));
for (const [dependencyName, dependents] of dependentsGraph) {
const release = releasesByName.get(dependencyName);
if (!release) continue;
for (const dependentName of dependents) {
const dependentRelease = releasesByName.get(dependentName);
if (!dependentRelease) continue;
graph.get(dependentRelease).push(release);
}
}
const result = graphSequencer(graph);
if (result.cycles.length > 0) log.warn(`Publish plan contains cyclic dependencies: ${result.cycles.map((cycle) => cycle.map((release) => release.name).join(" -> ")).join("; ")}`);
return result.chunks;
}
async function getPublishPlan(rootDir, config, options) {
const packages = await getPackages(rootDir);
const releases = await getUnpublishedPackages(packages, await readPreState(rootDir), config.access, {
tag: options?.tag,
ignore: config.ignore,
allowPrivatePackages: config.privatePackages.tag
});
const tagReleases = config.privatePackages.tag ? await getUntaggedPrivatePackages(packages.rootDir, packages.packages, packages.tool, {
ignore: config.ignore,
allowPrivatePackages: config.privatePackages.tag
}) : [];
if (releases.length === 0 && tagReleases.length === 0) return [];
return sortReleases(packages, [...releases, ...tagReleases], config);
}
//#endregion
export { isPublishSuccessful as a, isPublishFailure as i, readPlanFile as n, npmPublishQueue as o, getPublishTool as r, createPromiseQueue as s, getPublishPlan as t };