@alexaegis/standard-version
Version:
Standard version
176 lines (175 loc) • 6.26 kB
JavaScript
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
};