UNPKG

@alexaegis/standard-version

Version:
176 lines (175 loc) 6.26 kB
import { join, relative, dirname, normalize } from "node:path"; import { globSync } from "glob"; import { load } from "js-yaml"; import { readFileSync, statSync, existsSync } from "node:fs"; const PACKAGE_JSON_NAME = "package.json"; const PACKAGE_JSON_DEPENDENCY_FIELDS = [ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" ]; const collectPackages = () => { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { throw new Error("not in a workspace"); } const pnpmWorkspace = load( readFileSync(join(workspaceRoot, "pnpm-workspace.yaml"), { encoding: "utf8" }) ); const workspacePackageJsonPath = join(workspaceRoot, PACKAGE_JSON_NAME); const workspacePackageJson = JSON.parse( readFileSync(workspacePackageJsonPath, { encoding: "utf8" }) ); let workspaces = normalizePackageJsonWorkspacesField(workspacePackageJson.workspaces); if (pnpmWorkspace.packages) { workspaces = [...workspaces, ...pnpmWorkspace.packages]; } const packagePaths = globSync(workspaces, { ignore: ["node_modules"], cwd: workspaceRoot, absolute: true }).filter((path) => statSync(path).isDirectory()); const workspacePackage = { packageKind: "root", packagePath: workspaceRoot, packageJson: workspacePackageJson, packageJsonPath: workspacePackageJsonPath, workspacePackagePatterns: workspaces, packagePathFromRootPackage: "." }; const subPackages = packagePaths.map((packagePath) => { const packageJsonPath = join(packagePath, PACKAGE_JSON_NAME); try { return { packageKind: "regular", packagePath: packagePath.toString(), packageJsonPath, packageJson: JSON.parse( readFileSync(packageJsonPath, { encoding: "utf8" }) ), packagePathFromRootPackage: relative(workspaceRoot, dirname(packageJsonPath)) }; } catch { return void 0; } }).filter((pkg) => !!pkg); return { workspacePackage, subPackages }; }; const getWorkspaceRoot = (cwd = process.cwd()) => { return collectPackageJsonPathsUpDirectoryTree(cwd)[0]; }; const collectPackageJsonPathsUpDirectoryTree = (cwd = process.cwd()) => { return collectPackageJsonPathsUpDirectoryTreeInternal(cwd); }; const collectPackageJsonPathsUpDirectoryTreeInternal = (cwd, collection = []) => { const path = normalize(cwd); if (existsSync(join(path, "package.json"))) { collection.unshift(path); } const parentPath = join(path, ".."); if (parentPath !== path) { return collectPackageJsonPathsUpDirectoryTreeInternal(parentPath, collection); } return collection; }; const normalizePackageJsonWorkspacesField = (packageJsonWorkspaces) => { if (Array.isArray(packageJsonWorkspaces)) { return packageJsonWorkspaces; } else if (packageJsonWorkspaces) { return [ ...packageJsonWorkspaces.packages ?? [], ...packageJsonWorkspaces.nohoist ?? [] ]; } else { return []; } }; const createGenericUpdater = (packages) => { return { readVersion: (contents) => { const results = [...contents.matchAll(/"version": "(.*)"/g)]; return results[0]?.[1] ?? "0.0.0"; }, writeVersion: (contents, version) => { return packages.map((localPackage) => localPackage.packageJson.name).filter((name) => !!name).reduce( (r, localPackageName) => r.replaceAll( new RegExp(`"${localPackageName}": "(workspace:)([~^])?.*"`, "g"), `"${localPackageName}": "$1$2"` // the workspace protocol does not include versions but keeps the specifier. This is how pnpm leaves them on install. ).replaceAll( new RegExp(`"${localPackageName}": "(?!workspace:)([~^])?.*"`, "g"), `"${localPackageName}": "$1${version}"` ).replaceAll( new RegExp(`${localPackageName}@([^s \r]+)`, "g"), `${localPackageName}@${version}` ), contents.replace(/"version": ".*"/, `"version": "${version}"`) ); } }; }; const workspaceDependencyVersionRegexp = /^(workspace:)([\^~])?.*$/g; const nonWorkspaceDependencyVersionRegexp = /^(?!workspace:)([\^~])?.*$/g; const createPackageJsonUpdater = (packages) => { return { readVersion: (contents) => { const results = [...contents.matchAll(/"version": "(.*)"/g)]; return results[0]?.[1] ?? "0.0.0"; }, writeVersion: (contents, version) => { const packageJson = JSON.parse(contents); const indent = " "; const newline = contents.includes("\r\n") ? "\r\n" : "\n"; packageJson.version = version; for (const localPackageName of packages.map((localPackage) => localPackage.packageJson.name).filter((name) => !!name)) { for (const dependencyField of PACKAGE_JSON_DEPENDENCY_FIELDS) { const dependencies = packageJson[dependencyField]; const localDependency = dependencies?.[localPackageName]; if (dependencies && localDependency) { dependencies[localPackageName] = localDependency.replaceAll(workspaceDependencyVersionRegexp, "$1$2").replaceAll(nonWorkspaceDependencyVersionRegexp, `$1${version}`); } } } const json = JSON.stringify(packageJson, void 0, indent); if (newline === "\r\n") { return json.replaceAll("\n", "\r\n") + "\r\n"; } return json + "\n"; } }; }; const createStandardVersionConfig = () => { const { workspacePackage, subPackages } = collectPackages(); const packageJsonUpdater = createPackageJsonUpdater(subPackages); const genericUpdater = createGenericUpdater(subPackages); return { scripts: { postbump: "pnpm install", prechangelog: "git add pnpm-lock.yaml" }, bumpFiles: [ { filename: workspacePackage.packageJsonPath, updater: packageJsonUpdater }, { filename: join(workspacePackage.packagePath, "readme.md"), updater: genericUpdater }, ...subPackages.map((pkg) => ({ filename: pkg.packageJsonPath, updater: packageJsonUpdater })), ...subPackages.map((pkg) => ({ filename: join(pkg.packagePath, "readme.md"), updater: genericUpdater })) ] }; }; export { collectPackages, createStandardVersionConfig };