Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/chronus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"pluralize": "^8.0.0",
"prompts": "^2.4.2",
"semver": "^7.7.2",
"smol-toml": "^1.4.1",
"source-map-support": "^0.5.21",
"std-env": "^3.9.0",
"yaml": "^2.8.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { deleteChangeDescription, writeChangeDescription } from "../change/write
import { resolveChangelogGenerator, updateChangelog } from "../changelog/generate.js";
import type { ReleaseAction, ReleasePlan, ReleasePlanChangeApplication } from "../release-plan/types.js";
import type { ChronusHost } from "../utils/host.js";
import { getEcosystem } from "../workspace-manager/auto-discover.js";
import type { ChronusWorkspace } from "../workspace/types.js";
import { updatePackageJson, type VersionAction } from "./update-package-json.js";
import { getManifestPatchRequest, type VersionAction } from "./update-package-json.js";

export async function applyReleasePlan(
host: ChronusHost,
Expand Down Expand Up @@ -38,7 +39,8 @@ export async function updatePackageVersions(
dependencyUpdateMode?: "stable" | "prerelease",
) {
for (const pkg of workspace.allPackages.filter((x) => x.state !== "ignored")) {
await updatePackageJson(host, workspace, pkg, actionForPackage, dependencyUpdateMode);
const patch = getManifestPatchRequest(workspace, pkg, actionForPackage, dependencyUpdateMode);
await getEcosystem(workspace.workspace.type).updateVersionsForPackage(host, workspace.workspace, pkg, patch);
}
}
async function cleanChangeApplication(
Expand Down
50 changes: 14 additions & 36 deletions packages/chronus/src/apply-release-plan/update-package-json.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,36 @@
import type { ChronusHost } from "../utils/host.js";
import { resolvePath } from "../utils/path-utils.js";
import type { Mutable } from "../utils/types.js";
import type { PackageJson } from "../workspace-manager/types.js";
import type { PatchPackageVersion } from "../workspace-manager/types.js";
import type { ChronusPackage, ChronusWorkspace } from "../workspace/types.js";
import { updateDependencyVersion } from "./update-dependency-version.js";

export interface VersionAction {
readonly newVersion: string;
}

export async function updatePackageJson(
host: ChronusHost,
export function getManifestPatchRequest(
workspace: ChronusWorkspace,
pkg: ChronusPackage,
actionForPackage: Map<string, VersionAction>,
dependencyUpdateMode: "stable" | "prerelease" = "stable",
) {
const newPkgJson = getNewPackageJson(workspace, pkg, actionForPackage, dependencyUpdateMode);
await host.writeFile(
resolvePath(workspace.path, pkg.relativePath, "package.json"),
JSON.stringify(newPkgJson, null, 2) + "\n",
);
}

function getNewPackageJson(
workspace: ChronusWorkspace,
pkg: ChronusPackage,
actionForPackage: Map<string, VersionAction>,
dependencyUpdateMode: "stable" | "prerelease",
): PackageJson {
): PatchPackageVersion {
const patch: PatchPackageVersion = { dependenciesVersions: {} };
const action = actionForPackage.get(pkg.name);
const currentPkgJson: Mutable<PackageJson> = JSON.parse(JSON.stringify(pkg.manifest));
if (action) {
const newVersion = action.newVersion;
currentPkgJson.version = newVersion;
patch.newVersion = newVersion;
}

if (pkg.state !== "standalone") {
for (const depType of ["dependencies", "devDependencies", "peerDependencies"] as const) {
const depObj = currentPkgJson[depType];
if (depObj) {
for (const dep of Object.keys(depObj)) {
const depAction = actionForPackage.get(dep);
if (depAction) {
depObj[dep] = updateDependencyVersion(
depObj[dep],
{ newVersion: depAction.newVersion, oldVersion: workspace.getPackage(dep).version },
dependencyUpdateMode,
);
}
}
for (const dep of pkg.dependencies.values()) {
const depAction = actionForPackage.get(dep.name);
if (depAction) {
patch.dependenciesVersions[dep.name] = updateDependencyVersion(
patch.dependenciesVersions[dep.name],
{ newVersion: depAction.newVersion, oldVersion: workspace.getPackage(dep.name).version },
dependencyUpdateMode,
);
}
}
}

return currentPkgJson;
return patch;
}
3 changes: 1 addition & 2 deletions packages/chronus/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { VersionType } from "../types.js";
import type { WorkspaceType } from "../workspace-manager/types.js";
import type { YamlFile } from "../yaml/types.js";

export interface ChronusUserConfig {
Expand All @@ -9,7 +8,7 @@ export interface ChronusUserConfig {
/** Base remote to use to compare against. Set this to the upstream remote to support forks that don't point their main branch to the upstream remote */
readonly baseRemote?: string;
/** Workspace type: pnpm, npm, yarn or auto */
readonly workspaceType?: WorkspaceType | "auto";
readonly workspaceType?: string | "auto";
/** Additional packages that do not belong the workspace */
readonly additionalPackages?: string[];
readonly versionPolicies?: VersionPolicy[];
Expand Down
24 changes: 8 additions & 16 deletions packages/chronus/src/dependency-graph/get-dependency-graph.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import { Range } from "semver";
import { ChronusError } from "../utils/errors.js";
import type { Package, PackageJson } from "../workspace-manager/types.js";
import type { Package } from "../workspace-manager/types.js";

const DEPENDENCY_TYPES = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const;

const getAllDependencies = (config: PackageJson) => {
const getAllDependencies = (config: Package) => {
const allDependencies = new Map<string, string>();

for (const type of DEPENDENCY_TYPES) {
const deps = config[type];
if (!deps) continue;

for (const name of Object.keys(deps)) {
const depRange = deps[name];
if ((depRange.startsWith("link:") || depRange.startsWith("file:")) && type === "devDependencies") {
continue;
}

allDependencies.set(name, depRange);
for (const spec of config.dependencies.values()) {
if ((spec.version.startsWith("link:") || spec.version.startsWith("file:")) && spec.kind === "dev") {
continue;
}

allDependencies.set(spec.name, spec.version);
}

return allDependencies;
Expand Down Expand Up @@ -62,7 +54,7 @@ export function getDependencyGraph(
for (const pkg of queue) {
const { name } = pkg;
const dependencies = [];
const allDependencies = getAllDependencies(pkg.manifest);
const allDependencies = getAllDependencies(pkg);

for (const [depName, originalDepRange] of allDependencies) {
let repRange = originalDepRange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("Assemble Release Plan", () => {
}
function mkPkg(name: string, manifest: PackageJson): Package {
const version = manifest.version ?? "1.0.0";
return { name, manifest: { ...manifest, version }, relativePath: `packages/${name}`, version };
return { name, relativePath: `packages/${name}`, version, dependencies: new Map() };
}
const workspace: Workspace = mkWorkspace([
mkPkg("pkg-a", { version: "1.0.0" }),
Expand Down
16 changes: 8 additions & 8 deletions packages/chronus/src/publish/publish-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import pacote from "pacote";
import { isCI } from "std-env";
import { execAsync, type ExecResult } from "../utils/exec-async.js";
import { NodeChronusHost, getDirectoryPath, getLastJsonObject, lookup } from "../utils/index.js";
import { createPnpmWorkspaceManager } from "../workspace-manager/pnpm.js";
import type { PackageBase } from "../workspace-manager/types.js";
import { createPnpmWorkspaceManager } from "../workspace-manager/node/pnpm.js";
import type { PackageId } from "../workspace-manager/types.js";
import type { PublishPackageResult } from "./types.js";

export interface PublishPackageOptions {
Expand Down Expand Up @@ -37,7 +37,7 @@ async function isDir(path: string) {
}

export async function publishPackage(
pkg: PackageBase,
pkg: PackageId,
pkgDir: string,
options: PublishPackageOptions = {},
): Promise<PublishPackageResult> {
Expand All @@ -54,15 +54,15 @@ async function shouldUsePnpm(pkgDir: string, engine: "npm" | "pnpm" | undefined)
} else if (engine === "npm") {
return false;
}
const pnpmWs = createPnpmWorkspaceManager(NodeChronusHost);
const pnpmWs = createPnpmWorkspaceManager();
const root = await lookup(pkgDir, (current) => {
return pnpmWs.is(current);
return pnpmWs.is(NodeChronusHost, current);
});
return Boolean(root);
}

export async function publishPackageWithNpm(
pkg: PackageBase,
pkg: PackageId,
pkgDir: string,
options: PublishPackageOptions = {},
): Promise<PublishPackageResult> {
Expand All @@ -85,7 +85,7 @@ export async function publishPackageWithNpm(
}

export async function publishPackageWithPnpm(
pkg: PackageBase,
pkg: PackageId,
pkgDir: string,
options: PublishPackageOptions = {},
): Promise<PublishPackageResult> {
Expand Down Expand Up @@ -124,7 +124,7 @@ export async function publishPackageWithPnpm(
}
}

function processError(pkg: PackageBase, result: ExecResult): PublishPackageResult {
function processError(pkg: PackageId, result: ExecResult): PublishPackageResult {
const json = getLastJsonObject(result.stderr.toString()) ?? getLastJsonObject(result.stdout.toString());

if (json?.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ChangeDescription } from "../change/types.js";
import { addNameToChangeKinds, defaultChangeKinds } from "../config/resolve.js";
import type { ChronusResolvedConfig } from "../config/types.js";
import type { VersionType } from "../types.js";
import { createPackageFromPackageJson } from "../workspace-manager/node/utils.js";
import type { Package, PackageJson, Workspace } from "../workspace-manager/types.js";
import { createChronusWorkspace } from "../workspace/load.js";
import { assembleReleasePlan } from "./assemble-release-plan.js";
Expand All @@ -25,7 +26,7 @@ describe("Assemble Release Plan", () => {
}
function mkPkg(name: string, manifest: PackageJson): Package {
const version = manifest.version ?? "1.0.0";
return { name, manifest: { ...manifest, version }, relativePath: `packages/${name}`, version };
return { relativePath: `packages/${name}`, ...createPackageFromPackageJson({ ...manifest, name, version }) };
}
const workspace: Workspace = mkWorkspace([
mkPkg("pkg-a", {}),
Expand Down
70 changes: 13 additions & 57 deletions packages/chronus/src/release-plan/determine-dependents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import semverSatisfies from "semver/functions/satisfies.js";
import type { DependencyType, VersionType } from "../types.js";
import type { PackageJson } from "../workspace-manager/types.js";
import type { Package } from "../workspace-manager/types.js";
import type { ChronusWorkspace } from "../workspace/types.js";
import { incrementVersion } from "./increment-version.js";
import type { InternalReleaseAction } from "./types.internal.js";
Expand Down Expand Up @@ -53,39 +53,22 @@ export function applyDependents({
let type: VersionType | undefined;

const dependent = dependentPackage.name;
const dependencyVersionRanges = getDependencyVersionRanges(dependentPackage.manifest, nextRelease);
const dependencyVersionRanges = getDependencyVersionRanges(dependentPackage, nextRelease);

for (const { depType, versionRange } of dependencyVersionRanges) {
if (nextRelease.type === "none") {
continue;
} else if (
shouldBumpMajor({
dependent,
depType,
versionRange,
actions: actions,
nextRelease,
})
) {
// We only bump to minor for 0. version as they are already considered breaking in semver
if (dependentPackage.version.startsWith("0.")) {
type = "minor";
} else {
type = "major";
}
} else if (
(!actions.has(dependent) || actions.get(dependent)!.type === "none") &&
!willRangeBeValid(nextRelease, versionRange)
) {
switch (depType) {
case "dependencies":
case "optionalDependencies":
case "peerDependencies":
case "prod":
if (type !== "major" && type !== "minor") {
type = "patch";
}
break;
case "devDependencies": {
case "dev": {
// We don't need a version bump if the package is only in the devDependencies of the dependent package
if (type !== "major" && type !== "minor" && type !== "patch") {
type = "none";
Expand Down Expand Up @@ -165,62 +148,35 @@ function willRangeBeValid(release: InternalReleaseAction, versionRange: string)
dependency lists. For example, a package that is both a peerDependencies and a devDependency.
*/
function getDependencyVersionRanges(
dependentPkgJSON: PackageJson,
dependentPkg: Package,
dependencyRelease: InternalReleaseAction,
): {
depType: DependencyType;
versionRange: string;
}[] {
const DEPENDENCY_TYPES = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const;
const dependencyVersionRanges: {
depType: DependencyType;
versionRange: string;
}[] = [];
for (const type of DEPENDENCY_TYPES) {
const versionRange = dependentPkgJSON[type]?.[dependencyRelease.packageName];
if (!versionRange) continue;

if (versionRange.startsWith("workspace:")) {
const spec = dependentPkg.dependencies.get(dependencyRelease.packageName);
if (spec) {
if (spec.version.startsWith("workspace:")) {
dependencyVersionRanges.push({
depType: type,
depType: spec.kind,
versionRange:
// intentionally keep other workspace ranges untouched
// this has to be fixed but this should only be done when adding appropriate tests
versionRange === "workspace:*"
spec.version === "workspace:*"
? // workspace:* actually means the current exact version, and not a wildcard similar to a regular * range
dependencyRelease.oldVersion
: versionRange.replace(/^workspace:/, ""),
: spec.version.replace(/^workspace:/, ""),
});
} else {
dependencyVersionRanges.push({
depType: type,
versionRange,
depType: spec.kind, // TODO: ? just pass the entire spec?
versionRange: spec.version,
});
}
}
return dependencyVersionRanges;
}

function shouldBumpMajor({
dependent,
depType,
versionRange,
actions,
nextRelease,
}: {
dependent: string;
depType: DependencyType;
versionRange: string;
actions: Map<string, InternalReleaseAction>;
nextRelease: InternalReleaseAction;
}) {
// we check if it is a peerDependency because if it is, our dependent bump type might need to be major.
return (
depType === "peerDependencies" &&
nextRelease.type !== "none" &&
nextRelease.type !== "patch" &&
!semverSatisfies(incrementVersion(nextRelease), versionRange) &&
// bump major only if the dependent doesn't already has a major release.
(!actions.has(dependent) || (actions.has(dependent) && actions.get(dependent)!.type !== "major"))
);
}
2 changes: 1 addition & 1 deletion packages/chronus/src/testing/test-chronus-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function mkWorkspace(packages: Package[]): Workspace {
}
function mkPkg(name: string, manifest: Partial<PackageJson>): Package {
const version = manifest.version ?? "1.0.0";
return { name, manifest: { ...manifest, version }, relativePath: `packages/${name}`, version };
return { name, relativePath: `packages/${name}`, version, dependencies: new Map() };
}

const baseConfig: ChronusResolvedConfig = {
Expand Down
2 changes: 1 addition & 1 deletion packages/chronus/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Type of versioning that can happen in semver. */
export type VersionType = "major" | "minor" | "patch" | "none";

export type DependencyType = "dependencies" | "devDependencies" | "peerDependencies" | "optionalDependencies";
export type DependencyType = "prod" | "dev";
Loading
Loading