@storm-software/workspace-tools
Version:
Tools for managing a Storm workspace, including various Nx generators and executors for common development tasks.
549 lines (528 loc) • 18.2 kB
JavaScript
import {
addPackageJsonGitHead
} from "./chunk-OVTQ7MFY.mjs";
import {
getConfig
} from "./chunk-4PKTZSV2.mjs";
import {
findWorkspaceRoot
} from "./chunk-3J2CP54B.mjs";
import {
joinPaths
} from "./chunk-TBW5MCN6.mjs";
// ../npm-tools/src/helpers/get-registry.ts
import { exec } from "node:child_process";
// ../npm-tools/src/constants.ts
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
// ../npm-tools/src/helpers/get-registry.ts
async function getRegistry(executable = "npm") {
return new Promise((resolve, reject) => {
exec(`${executable} config get registry`, (error, stdout, stderr) => {
if (error && !error.message.toLowerCase().trim().startsWith("npm warn")) {
return reject(error);
}
if (stderr && !stderr.toLowerCase().trim().startsWith("npm warn")) {
return reject(stderr);
}
return resolve(stdout.trim());
});
});
}
async function getNpmRegistry() {
if (process.env.STORM_REGISTRY_NPM) {
return process.env.STORM_REGISTRY_NPM;
}
const workspaceConfig = await getConfig();
if (workspaceConfig?.registry?.npm) {
return workspaceConfig?.registry?.npm;
}
return DEFAULT_NPM_REGISTRY;
}
// ../pnpm-tools/src/helpers/replace-deps-aliases.ts
import {
createProjectGraphAsync,
readCachedProjectGraph
} from "@nx/devkit";
import { existsSync as existsSync2 } from "node:fs";
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
import { format } from "prettier";
// ../npm-tools/src/helpers/get-version.ts
import { exec as exec2 } from "node:child_process";
// ../pnpm-tools/src/helpers/catalog.ts
import { coerce, gt, valid } from "semver";
// ../pnpm-tools/src/helpers/pnpm-workspace.ts
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { parse, stringify } from "yaml";
function getPnpmWorkspaceFilePath(workspaceRoot = findWorkspaceRoot(process.cwd())) {
const pnpmWorkspacePath = joinPaths(workspaceRoot, "pnpm-workspace.yaml");
if (!existsSync(pnpmWorkspacePath)) {
throw new Error(
`No \`pnpm-workspace.yaml\` file found in workspace root (searched in: ${pnpmWorkspacePath}).`
);
}
return pnpmWorkspacePath;
}
async function readPnpmWorkspaceFile(workspaceRoot = findWorkspaceRoot(process.cwd())) {
const result = await readFile(
getPnpmWorkspaceFilePath(workspaceRoot),
"utf8"
);
if (!result) {
return void 0;
}
return parse(result);
}
// ../pnpm-tools/src/helpers/catalog.ts
async function getCatalogSafe(workspaceRoot = findWorkspaceRoot(process.cwd())) {
const pnpmWorkspaceFile = await readPnpmWorkspaceFile(workspaceRoot);
if (!pnpmWorkspaceFile) {
throw new Error("No pnpm-workspace.yaml file found");
}
if (pnpmWorkspaceFile?.catalog) {
return Object.fromEntries(
Object.entries(pnpmWorkspaceFile.catalog).map(([key, value]) => {
return [key, value.replaceAll('"', "").replaceAll("'", "")];
})
);
} else {
console.warn(
`No catalog found in pnpm-workspace.yaml file located in workspace root: ${workspaceRoot}
File content: ${JSON.stringify(
pnpmWorkspaceFile,
null,
2
)}`
);
}
return void 0;
}
async function getCatalog(workspaceRoot = findWorkspaceRoot(process.cwd())) {
const catalog = await getCatalogSafe(workspaceRoot);
if (!catalog) {
throw new Error("No catalog entries found in pnpm-workspace.yaml file");
}
return catalog;
}
// ../pnpm-tools/src/helpers/replace-deps-aliases.ts
async function replaceDepsAliases(packageRoot = process.cwd(), workspaceRoot = findWorkspaceRoot(packageRoot)) {
const packageJsonPath = joinPaths(packageRoot, "package.json");
const packageJsonFile = await readFile2(packageJsonPath, "utf8");
if (!packageJsonFile) {
throw new Error(
"No package.json file found in package root: " + packageRoot
);
}
const catalog = await getCatalog(workspaceRoot);
const packageJson = JSON.parse(packageJsonFile);
const pnpmWorkspacePath = joinPaths(workspaceRoot, "pnpm-workspace.yaml");
if (!existsSync2(pnpmWorkspacePath)) {
console.warn(
`No \`pnpm-workspace.yaml\` file found in workspace root (searching in: ${pnpmWorkspacePath}). Skipping pnpm catalog read for now.`
);
return packageJson;
}
if (!catalog) {
console.warn(
`No pnpm catalog found. Skipping dependencies replacement for now.`
);
return;
}
for (const dependencyType of [
"dependencies",
"devDependencies",
"peerDependencies"
]) {
const dependencies = packageJson[dependencyType];
if (!dependencies) {
continue;
}
for (const dependencyName of Object.keys(dependencies)) {
if (dependencies[dependencyName] === "catalog:") {
if (!catalog) {
throw new Error(
`Dependency ${dependencyName} is marked as \`catalog:\`, but no catalog exists in the workspace root's \`pnpm-workspace.yaml\` file.`
);
}
const catalogVersion = catalog[dependencyName];
if (!catalogVersion) {
throw new Error("Missing pnpm catalog version for " + dependencyName);
}
dependencies[dependencyName] = catalogVersion;
} else if (dependencies[dependencyName].startsWith("catalog:")) {
throw new Error("multiple named catalogs not supported");
}
}
}
let projectGraph;
try {
projectGraph = readCachedProjectGraph();
} catch {
await createProjectGraphAsync();
projectGraph = readCachedProjectGraph();
}
const workspacePackages = {};
if (projectGraph) {
await Promise.all(
Object.keys(projectGraph.nodes).map(async (node) => {
const projectNode = projectGraph.nodes[node];
if (projectNode?.data.root) {
const projectPackageJsonPath = joinPaths(
workspaceRoot,
projectNode.data.root,
"package.json"
);
if (existsSync2(projectPackageJsonPath)) {
const projectPackageJsonContent = await readFile2(
projectPackageJsonPath,
"utf8"
);
const projectPackageJson = JSON.parse(projectPackageJsonContent);
if (projectPackageJson.private !== true) {
workspacePackages[projectPackageJson.name] = projectPackageJson.version;
}
}
}
})
);
}
for (const dependencyType of [
"dependencies",
"devDependencies",
"peerDependencies"
]) {
const dependencies = packageJson[dependencyType];
if (!dependencies) {
continue;
}
for (const dependencyName of Object.keys(dependencies)) {
if (dependencies[dependencyName].startsWith("workspace:")) {
if (workspacePackages[dependencyName]) {
dependencies[dependencyName] = `^${workspacePackages[dependencyName]}`;
} else {
throw new Error(
`Workspace dependency ${dependencyName} not found in workspace packages.`
);
}
}
}
}
return writeFile2(
packageJsonPath,
await format(JSON.stringify(packageJson), {
parser: "json",
proseWrap: "preserve",
trailingComma: "none",
tabWidth: 2,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
insertPragma: false,
bracketSameLine: true,
printWidth: 80,
bracketSpacing: true,
arrowParens: "avoid",
endOfLine: "lf",
plugins: ["prettier-plugin-packagejson"]
})
);
}
// src/executors/npm-publish/executor.ts
import { execSync } from "node:child_process";
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
import { format as format2 } from "prettier";
var LARGE_BUFFER = 1024 * 1e6;
async function npmPublishExecutorFn(options, context) {
const isDryRun = process.env.NX_DRY_RUN === "true" || options.dryRun || false;
if (!context.projectName) {
throw new Error("The `npm-publish` executor requires a `projectName`.");
}
const projectConfig = context.projectsConfigurations?.projects?.[context.projectName];
if (!projectConfig) {
throw new Error(
`Could not find project configuration for \`${context.projectName}\``
);
}
const packageRoot = joinPaths(
context.root,
options.packageRoot || joinPaths("dist", projectConfig.root)
);
const projectRoot = context.projectsConfigurations.projects[context.projectName]?.root ? joinPaths(
context.root,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
context.projectsConfigurations.projects[context.projectName].root
) : packageRoot;
const packageJsonPath = joinPaths(packageRoot, "package.json");
const packageJsonFile = await readFile3(packageJsonPath, "utf8");
if (!packageJsonFile) {
throw new Error(`Could not find \`package.json\` at ${packageJsonPath}`);
}
const packageJson = JSON.parse(packageJsonFile);
const projectPackageJsonPath = joinPaths(projectRoot, "package.json");
const projectPackageJsonFile = await readFile3(projectPackageJsonPath, "utf8");
if (!projectPackageJsonFile) {
throw new Error(
`Could not find \`package.json\` at ${projectPackageJsonPath}`
);
}
const projectPackageJson = JSON.parse(projectPackageJsonFile);
if (packageJson.version !== projectPackageJson.version) {
console.warn(
`The version in the package.json file at ${packageJsonPath} (current: ${packageJson.version}) does not match the version in the package.json file at ${projectPackageJsonPath} (current: ${projectPackageJson.version}). This file will be updated to match the version in the project package.json file.`
);
if (projectPackageJson.version) {
packageJson.version = projectPackageJson.version;
await writeFile3(
packageJsonPath,
await format2(JSON.stringify(packageJson), {
parser: "json",
proseWrap: "preserve",
trailingComma: "none",
tabWidth: 2,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
insertPragma: false,
bracketSameLine: true,
printWidth: 80,
bracketSpacing: true,
arrowParens: "avoid",
endOfLine: "lf",
plugins: ["prettier-plugin-packagejson"]
})
);
}
}
const packageName = packageJson.name;
console.info(
`\u{1F680} Running Storm NPM Publish executor on the ${packageName} package`
);
const packageTxt = packageName === context.projectName ? `package "${packageName}"` : `package "${packageName}" from project "${context.projectName}"`;
if (packageJson.private === true) {
console.warn(
`Skipped ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}`
);
return { success: true };
}
await replaceDepsAliases(packageRoot, context.root);
await addPackageJsonGitHead(packageRoot);
const npmPublishCommandSegments = [`npm publish --json`];
const npmViewCommandSegments = [
`npm view ${packageName} versions dist-tags --json`
];
const registry = options.registry ?? (await getRegistry() || getNpmRegistry());
if (registry) {
npmPublishCommandSegments.push(`--registry="${registry}" `);
npmViewCommandSegments.push(`--registry="${registry}" `);
}
if (options.otp) {
npmPublishCommandSegments.push(`--otp="${options.otp}" `);
}
if (isDryRun) {
npmPublishCommandSegments.push("--dry-run");
}
npmPublishCommandSegments.push("--provenance --access=public ");
const tag = options.tag || execSync("npm config get tag", {
cwd: packageRoot,
env: {
...process.env,
FORCE_COLOR: "true"
},
maxBuffer: LARGE_BUFFER,
killSignal: "SIGTERM"
}).toString().trim();
if (tag) {
npmPublishCommandSegments.push(`--tag="${tag}" `);
}
if (!isDryRun) {
const currentVersion = options.version || packageJson.version;
try {
try {
const result = execSync(npmViewCommandSegments.join(" "), {
cwd: packageRoot,
env: {
...process.env,
FORCE_COLOR: "true"
},
maxBuffer: LARGE_BUFFER,
killSignal: "SIGTERM"
});
const resultJson = JSON.parse(result.toString());
const distTags = resultJson["dist-tags"] || {};
if (distTags[tag] === currentVersion) {
console.warn(
`Skipped ${packageTxt} because v${currentVersion} already exists in ${registry} with tag "${tag}"`
);
return { success: true };
}
} catch (err) {
console.warn("\n ********************** \n");
console.warn(
`An error occurred while checking for existing dist-tags
${JSON.stringify(err)}
Note: If this is the first time this package has been published to NPM, this can be ignored.
`
);
console.info("");
}
try {
if (!isDryRun) {
const command = `npm dist-tag add ${packageName}@${currentVersion} ${tag} --registry="${registry}" `;
console.info(
`Adding the dist-tag ${tag} - preparing to run the following:
${command}
`
);
const result = execSync(command, {
cwd: packageRoot,
env: {
...process.env,
FORCE_COLOR: "true"
},
maxBuffer: LARGE_BUFFER,
killSignal: "SIGTERM"
});
console.info(
`Added the dist-tag ${tag} to v${currentVersion} for registry "${registry}"
Execution response: ${result.toString()}
`
);
} else {
console.info(
`Would add the dist-tag ${tag} to v${currentVersion} for registry "${registry}", but [dry-run] was set.
`
);
}
return { success: true };
} catch (err) {
try {
console.warn("\n ********************** \n");
let error = err;
if (Buffer.isBuffer(error)) {
error = error.toString();
}
console.warn(
`An error occurred while adding dist-tags:
${error}
Note: If this is the first time this package has been published to NPM, this can be ignored.
`
);
console.info("");
const stdoutData = JSON.parse(err.stdout?.toString() || "{}");
if (stdoutData?.error && !(stdoutData.error?.code?.includes("E404") && stdoutData.error?.summary?.includes("no such package available")) && !(err.stderr?.toString().includes("E404") && err.stderr?.toString().includes("no such package available"))) {
console.error(
"npm dist-tag add error please see below for more information:"
);
if (stdoutData.error.summary) {
console.error(stdoutData.error?.summary);
}
if (stdoutData.error.detail) {
console.error(stdoutData.error?.detail);
}
if (context.isVerbose) {
console.error(
`npm dist-tag add stdout: ${JSON.stringify(stdoutData, null, 2)}`
);
}
return { success: false };
}
} catch (err2) {
console.error(
`Something unexpected went wrong when processing the npm dist-tag add output
${JSON.stringify(err2)}`
);
return { success: false };
}
}
} catch (err) {
let error = err;
if (Buffer.isBuffer(error)) {
error = error.toString();
}
console.error("\n ********************** \n");
console.info("");
console.error(
"An error occured trying to run the npm dist-tag add command."
);
console.error(error);
console.info("");
const stdoutData = JSON.parse(err.stdout?.toString() || "{}");
if (!(stdoutData.error?.code?.includes("E404") && stdoutData.error?.summary?.toLowerCase().includes("not found")) && !(err.stderr?.toString().includes("E404") && err.stderr?.toString().toLowerCase().includes("not found"))) {
console.error(
`Something unexpected went wrong when checking for existing dist-tags.
Error: ${JSON.stringify(err)}
`
);
return { success: false };
}
}
}
try {
const cwd = packageRoot;
const command = npmPublishCommandSegments.join(" ");
console.info(
`Running publish command "${command}" in current working directory: "${cwd}" `
);
const result = execSync(command, {
cwd,
env: {
...process.env,
FORCE_COLOR: "true"
},
maxBuffer: LARGE_BUFFER,
killSignal: "SIGTERM"
});
if (isDryRun) {
console.info(
`Would publish to ${registry} with tag "${tag}", but [dry-run] was set ${result ? `
Execution response: ${result.toString()}` : ""}
`
);
} else {
console.info(`Published to ${registry} with tag "${tag}" ${result ? `
Execution response: ${result.toString()}` : ""}
`);
}
return { success: true };
} catch (err) {
try {
console.error("\n ********************** \n");
console.info("");
console.error("An error occured running npm publish.");
console.error("Please see below for more information:");
console.info("");
const stdoutData = JSON.parse(err.stdout?.toString() || "{}");
if (stdoutData.error.summary) {
console.error(stdoutData.error.summary);
console.error(stdoutData.error.summary);
}
if (stdoutData.error.detail) {
console.error(stdoutData.error.detail);
}
if (context.isVerbose) {
console.error(
`npm publish stdout:
${JSON.stringify(stdoutData, null, 2)}`
);
}
console.error("\n ********************** \n");
return { success: false };
} catch (err2) {
let error = err2;
if (Buffer.isBuffer(error)) {
error = error.toString();
}
console.error(
`Something unexpected went wrong when processing the npm publish output
Error: ${JSON.stringify(error)}
`
);
console.error("\n ********************** \n");
return { success: false };
}
}
}
export {
LARGE_BUFFER,
npmPublishExecutorFn
};