@suin/semantic-release-yarn
Version:
semantic-release plugin to publish a npm package with yarn@berry
181 lines (165 loc) • 5.63 kB
text/typescript
import SemanticReleaseError from "@semantic-release/error";
import readPackage from "read-pkg";
import semver from "semver";
import { PluginConfig } from "./pluginConfig.js";
import { Yarn } from "./yarn.js";
let verified = false;
let prepared = false;
const yarn = new Yarn();
export type { PluginConfig };
export async function verifyConditions(
config: PluginConfig | undefined = undefined,
context: Context
): Promise<void> {
config = PluginConfig.normalize(config);
context.logger.log(`read ${context.cwd}/package.json`);
const pkg = await getPackage(context.cwd);
if (pkg.private === true) {
context.logger.log("skipping to verify npm auth since package is private");
return;
}
if (config.npmPublish === false) {
context.logger.log("skipping to verify npm auth since npmPublish is false");
return;
}
context.logger.log('set npmRegistryServer: "https://registry.npmjs.org"');
await yarn.setNpmRegistryServer("https://registry.npmjs.org");
context.logger.log("set NPM_TOKEN to yarn config npmAuthToken");
await yarn.setNpmAuthToken(getNpmToken(context.env));
context.logger.log("verify npm auth");
if (!(await yarn.authenticated())) {
throw PluginError.EINVALIDNPMTOKEN();
}
context.logger.log("install version plugin");
await yarn.pluginImportVersion();
verified = true;
}
export async function prepare(
config: PluginConfig | undefined = undefined,
context: PrepareContext
) {
config = PluginConfig.normalize(config);
if (!verified) {
return;
}
// Reload package.json in case a previous external step updated it
context.logger.log(`read ${context.cwd}/package.json`);
const pkg = await getPackage(context.cwd);
if (pkg.private === true) {
context.logger.log("skipping to prepare since package is private");
return;
}
if (config.npmPublish === false) {
context.logger.log("skipping to prepare since npmPublish is false");
return;
}
context.logger.log(
`rewrite the "version" field in the package.json: ${context.nextRelease.version}`
);
const tarballDir = config.tarballDir ?? ".";
await yarn.version(context.nextRelease.version);
if (typeof tarballDir === "string") {
const tarballName = tarballDir + "/package.tgz";
context.logger.log(`creating a tarball: ${tarballName}`);
await yarn.pack(tarballName);
}
context.logger.log(`package contents:`);
const tarballContents = await yarn.packDryRun();
for (const tarballContent of tarballContents) {
context.logger.log(` ${tarballContent}`);
}
prepared = true;
}
export async function publish(
config: PluginConfig | undefined = undefined,
context: PrepareContext
) {
config = PluginConfig.normalize(config);
if (!verified || !prepared) {
return;
}
// Reload package.json in case a previous external step updated it
context.logger.log(`read ${context.cwd}/package.json`);
const pkg = await getPackage(context.cwd);
if (pkg.private === true) {
context.logger.log("skipping to publish since package is private");
return;
}
if (config.npmPublish === false) {
context.logger.log("skipping to publish since npmPublish is false");
return;
}
const { version } = context.nextRelease;
const distTag = getChannel(context.nextRelease.channel);
context.logger.log(
`Publishing version ${version} to npm registry on dist-tag ${distTag}`
);
await yarn.publish(distTag);
context.logger.log(
`Published ${pkg.name}@${version} to dist-tag @${distTag}`
);
}
interface Context {
readonly cwd: string;
readonly env: NodeJS.ProcessEnv;
readonly stdout: NodeJS.WriteStream;
readonly stderr: NodeJS.WriteStream;
readonly logger: {
readonly log: (message: string, ...vars: any[]) => void;
readonly error: (message: string, ...vars: any[]) => void;
};
}
interface PrepareContext extends Context {
readonly nextRelease: {
readonly version: string;
readonly channel: string;
};
}
class PluginError extends SemanticReleaseError {
static ENOPKGNAME() {
return new PluginError(
"Missing `name` property in `package.json`.",
"ENOPKGNAME"
);
}
static ENOPKG() {
return new PluginError("Missing `package.json`.", "ENOPKG");
}
static EINVALIDNPMTOKEN() {
return new PluginError(
"Invalid npm token.",
"EINVALIDNPMTOKEN",
`The npm token configured in the \`NPM_TOKEN\` environment variable must be a valid [token](https://docs.npmjs.com/getting-started/working_with_tokens) allowing to publish to the registry \`\`.
If you are using Two Factor Authentication for your account, set its level to ["Authorization only"](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) in your account settings. **semantic-release** cannot publish with the default "
Authorization and writes" level.
Please make sure to set the \`NPM_TOKEN\` environment variable in your CI with the exact value of the npm token.`
);
}
}
async function getPackage(cwd: string) {
let pkg;
try {
pkg = await readPackage({ cwd });
} catch (error) {
if ((error as any).code === "ENOENT") {
throw PluginError.ENOPKG();
}
throw new AggregateError([error]);
}
if (!pkg.name) {
throw PluginError.ENOPKGNAME();
}
return pkg;
}
function getNpmToken(env: NodeJS.ProcessEnv): string {
if (typeof env["NPM_TOKEN"] !== "string") {
throw PluginError.EINVALIDNPMTOKEN();
}
return env["NPM_TOKEN"];
}
const getChannel = (channel: string) =>
channel
? semver.validRange(channel)
? `release-${channel}`
: channel
: "latest";