@jsenv/package-publish
Version:
Publish package to one or many registry.
186 lines (180 loc) • 6.1 kB
JavaScript
import { removeEntry } from "@jsenv/filesystem";
import { createTaskLog } from "@jsenv/humanize";
import { exec } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { setNpmConfig } from "./set_npm_config.js";
export const publish = async ({
logger,
packageSlug,
logNpmPublishOutput,
rootDirectoryUrl,
registryUrl,
token,
}) => {
const publishTask = createTaskLog(`publish ${packageSlug} on ${registryUrl}`);
try {
// process.env.NODE_AUTH_TOKEN
const previousValue = process.env.NODE_AUTH_TOKEN;
const restoreProcessEnv = () => {
process.env.NODE_AUTH_TOKEN = previousValue;
};
process.env.NODE_AUTH_TOKEN = token;
// updating package.json to publish on the correct registry
let restorePackageFile = () => {};
const rootPackageFileUrl = new URL("./package.json", rootDirectoryUrl);
const rootPackageFileContent = readFileSync(rootPackageFileUrl);
const packageObject = JSON.parse(String(rootPackageFileContent));
const { publishConfig } = packageObject;
const registerUrlFromPackage = publishConfig
? publishConfig.registry || "https://registry.npmjs.org"
: "https://registry.npmjs.org";
if (registryUrl !== registerUrlFromPackage) {
restorePackageFile = () =>
writeFileSync(rootPackageFileUrl, rootPackageFileContent);
packageObject.publishConfig = packageObject.publishConfig || {};
packageObject.publishConfig.registry = registryUrl;
writeFileSync(
rootPackageFileUrl,
JSON.stringify(packageObject, null, " "),
);
}
// updating .npmrc to add the token
const npmConfigFileUrl = new URL("./.npmrc", rootDirectoryUrl);
let restoreNpmConfigFile;
let npmConfigFileContent;
try {
npmConfigFileContent = String(readFileSync(npmConfigFileUrl));
restoreNpmConfigFile = () =>
writeFileSync(npmConfigFileUrl, npmConfigFileContent);
} catch (e) {
if (e.code === "ENOENT") {
restoreNpmConfigFile = () => removeEntry(npmConfigFileUrl);
npmConfigFileContent = "";
} else {
throw e;
}
}
writeFileSync(
npmConfigFileUrl,
setNpmConfig(npmConfigFileContent, {
[computeRegistryTokenKey(registryUrl)]: token,
[computeRegistryKey(packageObject.name)]: registryUrl,
}),
);
try {
const reason = await new Promise((resolve, reject) => {
const command = exec(
"npm publish --no-workspaces",
{
cwd: fileURLToPath(rootDirectoryUrl),
stdio: "silent",
},
(error) => {
if (error) {
// publish conflict generally occurs because servers
// returns 200 after npm publish
// but returns previous version if asked immediatly
// after for the last published version.
// TODO: ideally we should catch 404 error returned from npm
// it happens it the token is not allowed to publish
// a repository. And when we detect this we display a more useful message
// suggesting the token rights are insufficient to publish the package
// npm publish conclit
if (error.message.includes("EPUBLISHCONFLICT")) {
resolve({
success: true,
reason: "already-published",
});
} else if (
error.message.includes("Cannot publish over existing version")
) {
resolve({
success: true,
reason: "already-published",
});
} else if (
error.message.includes(
"You cannot publish over the previously published versions",
)
) {
resolve({
success: true,
reason: "already-published",
});
}
// github publish conflict
else if (
error.message.includes(
"ambiguous package version in package.json",
)
) {
resolve({
success: true,
reason: "already-published",
});
} else {
reject(error);
}
} else {
resolve({
success: true,
reason: "published",
});
}
},
);
if (logNpmPublishOutput) {
command.stdout.on("data", (data) => {
logger.debug(data);
});
command.stderr.on("data", (data) => {
// debug because this output is part of
// the error message generated by a failing npm publish
logger.debug(data);
});
}
});
if (reason === "already-published") {
publishTask.setRightText(`(already published)`);
}
publishTask.done();
return {
success: true,
reason,
};
} finally {
restoreProcessEnv();
restorePackageFile();
restoreNpmConfigFile();
}
} catch (e) {
publishTask.fail();
console.error(e.stack);
return {
success: false,
reason: e,
};
}
};
const computeRegistryTokenKey = (registryUrl) => {
if (registryUrl.startsWith("http://")) {
return `${registryUrl.slice("http:".length)}/:_authToken`;
}
if (registryUrl.startsWith("https://")) {
return `${registryUrl.slice("https:".length)}/:_authToken`;
}
if (registryUrl.startsWith("//")) {
return `${registryUrl}/:_authToken`;
}
throw new Error(
`registryUrl must start with http or https, got ${registryUrl}`,
);
};
const computeRegistryKey = (packageName) => {
if (packageName[0] === "@") {
const packageScope = packageName.slice(0, packageName.indexOf("/"));
return `${packageScope}:registry`;
}
return `registry`;
};