UNPKG

@storm-software/workspace-tools

Version:

Tools for managing a Storm workspace, including various Nx generators and executors for common development tasks.

525 lines (504 loc) • 18.5 kB
import { getGitHubTools } from "./chunk-54BBSJHK.mjs"; import { addPackageJsonGitHead } from "./chunk-EYO6EPEG.mjs"; import { getConfig } from "./chunk-35G4LHK2.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 workspaceConfig = await getConfig(context.root); const github = await getGitHubTools(workspaceConfig); const isDryRun = process.env.NX_DRY_RUN === "true" || options.dryRun || false; if (!context.projectName) { github.error("The `npm-publish` executor requires a `projectName`."); return { success: false }; } const projectConfig = context.projectsConfigurations?.projects?.[context.projectName]; if (!projectConfig) { github.error( `Could not find project configuration for \`${context.projectName}\`` ); return { success: false }; } 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) { github.error(`Could not find \`package.json\` at ${packageJsonPath}`); return { success: false }; } const packageJson = JSON.parse(packageJsonFile); const projectPackageJsonPath = joinPaths(projectRoot, "package.json"); const projectPackageJsonFile = await readFile3(projectPackageJsonPath, "utf8"); if (!projectPackageJsonFile) { github.error( `Could not find \`package.json\` at ${projectPackageJsonPath}` ); return { success: false }; } 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 = await Promise.resolve( options.registry ?? (await getRegistry() || getNpmRegistry()) ); if (registry) { npmPublishCommandSegments.push(`--registry="${registry}" `); npmViewCommandSegments.push(`--registry="${registry}" `); } if (options.otp) { npmPublishCommandSegments.push(`--otp="${options.otp}" `); } let token; if (!options.otp) { token = await github.getIDToken( `npm:${registry.replace(/^https?:\/\//, "")}` ); if (!token) { github.warning( `Either a One time password (OTP) or an OpenID Connect (OIDC) token is generally required to publish ${packageTxt} to NPM. Usually the OIDC token should be provided automatically via GitHub Actions (see: https://github.com/actions/toolkit/tree/main/packages/core#oidc-token); however, the release process was unable to retrieve it. Please provide a \`otp\` executor option, or investigate why the OIDC token could not be retrieved.` ); } } npmPublishCommandSegments.push("--provenance --access=public "); if (isDryRun) { npmPublishCommandSegments.push("--dry-run"); } const tag = options.tag || execSync("npm config get tag", { cwd: packageRoot, env: { NPM_ID_TOKEN: token, ...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: { NPM_ID_TOKEN: token, ...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.debug( `An error occurred while checking for existing dist-tags. Please note: if this is the first time this package has been published to npm, this can be ignored. Error: ${JSON.stringify( err, null, 2 )}` ); } try { if (!isDryRun) { const command = `npm dist-tag add ${packageName}@${currentVersion} ${tag} --registry="${registry}" `; console.debug( `Adding the dist-tag ${tag} - preparing to run the following: ${command}` ); const result = execSync(command, { cwd: packageRoot, env: { NPM_ID_TOKEN: token, ...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 have added the dist-tag ${tag} to v${currentVersion} for registry "${registry}", but [dry-run] was set. ` ); } return { success: true }; } catch (err) { try { 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"))) { const errorMessage = `An unexpected error occured while running the npm dist-tag add command: ${stdoutData?.error?.summary ? `Summary: ${stdoutData?.error?.summary}${stdoutData?.error?.code ? ` (${stdoutData?.error?.code})` : ""} ` : ""}${stdoutData?.error?.detail ? `Detail: ${stdoutData?.error?.detail} ` : ""}`; github.error(errorMessage); return { success: false }; } } catch (err2) { const stdoutData = JSON.parse(err2.stdout?.toString() || "{}"); const errorMessage = `An unexpected error occured while processing the npm dist-tag add output: ${stdoutData?.error?.summary ? `Summary: ${stdoutData?.error?.summary}${stdoutData?.error?.code ? ` (${stdoutData?.error?.code})` : ""} ` : ""}${stdoutData?.error?.detail ? `Detail: ${stdoutData?.error?.detail} ` : ""}`; github.error(errorMessage); return { success: false }; } } } catch (err) { 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"))) { const errorMessage = `An unexpected error occured while checking for existing dist-tags: ${stdoutData?.error?.summary ? `Summary: ${stdoutData?.error?.summary}${stdoutData?.error?.code ? ` (${stdoutData?.error?.code})` : ""} ` : ""}${stdoutData?.error?.detail ? `Detail: ${stdoutData?.error?.detail} ` : ""}`; github.error(errorMessage); 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: { NPM_ID_TOKEN: token, ...process.env, FORCE_COLOR: "true" }, maxBuffer: LARGE_BUFFER, killSignal: "SIGTERM" }); if (isDryRun) { console.info( `Would publish tag "${tag}" to ${registry}, but [dry-run] was set. ${result ? ` Execution response: ${result.toString()}` : ""}` ); } else { console.info( `Published tag "${tag}" to ${registry}. ${result ? ` Execution response: ${result.toString()}` : ""}` ); } return { success: true }; } catch (err) { try { const stdoutData = JSON.parse(err.stdout?.toString() || "{}"); const errorMessage = `An error occurred while publishing the npm package: ${stdoutData?.error?.summary ? `Summary: ${stdoutData?.error?.summary}${stdoutData?.error?.code ? ` (${stdoutData?.error?.code})` : ""} ` : ""}${stdoutData?.error?.detail ? `Detail: ${stdoutData?.error?.detail} ` : ""}`; github.error(errorMessage); return { success: false }; } catch (err2) { const errorMessage = `Something unexpected went wrong when processing the npm publish output. Error: ${JSON.stringify( Buffer.isBuffer(err2) ? err2.toString() : err2, null, 2 )}`; github.error(errorMessage); return { success: false }; } } } export { LARGE_BUFFER, npmPublishExecutorFn };