371 lines
11 KiB
Plaintext
371 lines
11 KiB
Plaintext
|
import {
|
||
|
ReleasePlan,
|
||
|
Config,
|
||
|
NewChangeset,
|
||
|
PreState,
|
||
|
PackageGroup,
|
||
|
} from "@changesets/types";
|
||
|
import determineDependents from "./determine-dependents";
|
||
|
import flattenReleases from "./flatten-releases";
|
||
|
import matchFixedConstraint from "./match-fixed-constraint";
|
||
|
import applyLinks from "./apply-links";
|
||
|
import { incrementVersion } from "./increment";
|
||
|
import semverParse from "semver/functions/parse";
|
||
|
import { InternalError } from "@changesets/errors";
|
||
|
import { Packages, Package } from "@manypkg/get-packages";
|
||
|
import { getDependentsGraph } from "@changesets/get-dependents-graph";
|
||
|
import { PreInfo, InternalRelease } from "./types";
|
||
|
|
||
|
type SnapshotReleaseParameters = {
|
||
|
tag?: string | undefined;
|
||
|
commit?: string | undefined;
|
||
|
};
|
||
|
|
||
|
function getPreVersion(version: string) {
|
||
|
let parsed = semverParse(version)!;
|
||
|
let preVersion =
|
||
|
parsed.prerelease[1] === undefined ? -1 : parsed.prerelease[1];
|
||
|
if (typeof preVersion !== "number") {
|
||
|
throw new InternalError("preVersion is not a number");
|
||
|
}
|
||
|
preVersion++;
|
||
|
return preVersion;
|
||
|
}
|
||
|
|
||
|
function getSnapshotSuffix(
|
||
|
template: Config["snapshot"]["prereleaseTemplate"],
|
||
|
snapshotParameters: SnapshotReleaseParameters
|
||
|
): string {
|
||
|
let snapshotRefDate = new Date();
|
||
|
|
||
|
const placeholderValues = {
|
||
|
commit: snapshotParameters.commit,
|
||
|
tag: snapshotParameters.tag,
|
||
|
timestamp: snapshotRefDate.getTime().toString(),
|
||
|
datetime: snapshotRefDate
|
||
|
.toISOString()
|
||
|
.replace(/\.\d{3}Z$/, "")
|
||
|
.replace(/[^\d]/g, ""),
|
||
|
};
|
||
|
|
||
|
// We need a special handling because we need to handle a case where `--snapshot` is used without any template,
|
||
|
// and the resulting version needs to be composed without a tag.
|
||
|
if (!template) {
|
||
|
return [placeholderValues.tag, placeholderValues.datetime]
|
||
|
.filter(Boolean)
|
||
|
.join("-");
|
||
|
}
|
||
|
|
||
|
const placeholders = Object.keys(placeholderValues) as Array<
|
||
|
keyof typeof placeholderValues
|
||
|
>;
|
||
|
|
||
|
if (!template.includes(`{tag}`) && placeholderValues.tag !== undefined) {
|
||
|
throw new Error(
|
||
|
`Failed to compose snapshot version: "{tag}" placeholder is missing, but the snapshot parameter is defined (value: '${placeholderValues.tag}')`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return placeholders.reduce((prev, key) => {
|
||
|
return prev.replace(new RegExp(`\\{${key}\\}`, "g"), () => {
|
||
|
const value = placeholderValues[key];
|
||
|
if (value === undefined) {
|
||
|
throw new Error(
|
||
|
`Failed to compose snapshot version: "{${key}}" placeholder is used without having a value defined!`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return value;
|
||
|
});
|
||
|
}, template);
|
||
|
}
|
||
|
|
||
|
function getSnapshotVersion(
|
||
|
release: InternalRelease,
|
||
|
preInfo: PreInfo | undefined,
|
||
|
useCalculatedVersion: boolean,
|
||
|
snapshotSuffix: string
|
||
|
): string {
|
||
|
if (release.type === "none") {
|
||
|
return release.oldVersion;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Using version as 0.0.0 so that it does not hinder with other version release
|
||
|
* For example;
|
||
|
* if user has a regular pre-release at 1.0.0-beta.0 and then you had a snapshot pre-release at 1.0.0-canary-git-hash
|
||
|
* and a consumer is using the range ^1.0.0-beta, most people would expect that range to resolve to 1.0.0-beta.0
|
||
|
* but it'll actually resolve to 1.0.0-canary-hash. Using 0.0.0 solves this problem because it won't conflict with other versions.
|
||
|
*
|
||
|
* You can set `snapshot.useCalculatedVersion` flag to true to use calculated versions if you don't care about the above problem.
|
||
|
*/
|
||
|
const baseVersion = useCalculatedVersion
|
||
|
? incrementVersion(release, preInfo)
|
||
|
: `0.0.0`;
|
||
|
|
||
|
return `${baseVersion}-${snapshotSuffix}`;
|
||
|
}
|
||
|
|
||
|
function getNewVersion(
|
||
|
release: InternalRelease,
|
||
|
preInfo: PreInfo | undefined
|
||
|
): string {
|
||
|
if (release.type === "none") {
|
||
|
return release.oldVersion;
|
||
|
}
|
||
|
|
||
|
return incrementVersion(release, preInfo);
|
||
|
}
|
||
|
|
||
|
type OptionalProp<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||
|
|
||
|
function assembleReleasePlan(
|
||
|
changesets: NewChangeset[],
|
||
|
packages: Packages,
|
||
|
config: OptionalProp<Config, "snapshot">,
|
||
|
// intentionally not using an optional parameter here so the result of `readPreState` has to be passed in here
|
||
|
preState: PreState | undefined,
|
||
|
// snapshot: undefined -> not using snaphot
|
||
|
// snapshot: { tag: undefined } -> --snapshot (empty tag)
|
||
|
// snapshot: { tag: "canary" } -> --snapshot canary
|
||
|
snapshot?: SnapshotReleaseParameters | string | boolean
|
||
|
): ReleasePlan {
|
||
|
// TODO: remove `refined*` in the next major version of this package
|
||
|
// just use `config` and `snapshot` parameters directly, typed as: `config: Config, snapshot?: SnapshotReleaseParameters`
|
||
|
const refinedConfig: Config = config.snapshot
|
||
|
? (config as Config)
|
||
|
: {
|
||
|
...config,
|
||
|
snapshot: {
|
||
|
prereleaseTemplate: null,
|
||
|
useCalculatedVersion: (
|
||
|
config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH as any
|
||
|
).useCalculatedVersionForSnapshots,
|
||
|
},
|
||
|
};
|
||
|
const refinedSnapshot: SnapshotReleaseParameters | undefined =
|
||
|
typeof snapshot === "string"
|
||
|
? { tag: snapshot }
|
||
|
: typeof snapshot === "boolean"
|
||
|
? { tag: undefined }
|
||
|
: snapshot;
|
||
|
|
||
|
let packagesByName = new Map(
|
||
|
packages.packages.map((x) => [x.packageJson.name, x])
|
||
|
);
|
||
|
|
||
|
const relevantChangesets = getRelevantChangesets(
|
||
|
changesets,
|
||
|
refinedConfig.ignore,
|
||
|
preState
|
||
|
);
|
||
|
|
||
|
const preInfo = getPreInfo(
|
||
|
changesets,
|
||
|
packagesByName,
|
||
|
refinedConfig,
|
||
|
preState
|
||
|
);
|
||
|
|
||
|
// releases is, at this point a list of all packages we are going to releases,
|
||
|
// flattened down to one release per package, having a reference back to their
|
||
|
// changesets, and with a calculated new versions
|
||
|
let releases = flattenReleases(
|
||
|
relevantChangesets,
|
||
|
packagesByName,
|
||
|
refinedConfig.ignore
|
||
|
);
|
||
|
|
||
|
let dependencyGraph = getDependentsGraph(packages, {
|
||
|
bumpVersionsWithWorkspaceProtocolOnly:
|
||
|
refinedConfig.bumpVersionsWithWorkspaceProtocolOnly,
|
||
|
});
|
||
|
|
||
|
let releasesValidated = false;
|
||
|
while (releasesValidated === false) {
|
||
|
// The map passed in to determineDependents will be mutated
|
||
|
let dependentAdded = determineDependents({
|
||
|
releases,
|
||
|
packagesByName,
|
||
|
dependencyGraph,
|
||
|
preInfo,
|
||
|
config: refinedConfig,
|
||
|
});
|
||
|
|
||
|
// `releases` might get mutated here
|
||
|
let fixedConstraintUpdated = matchFixedConstraint(
|
||
|
releases,
|
||
|
packagesByName,
|
||
|
refinedConfig
|
||
|
);
|
||
|
let linksUpdated = applyLinks(
|
||
|
releases,
|
||
|
packagesByName,
|
||
|
refinedConfig.linked
|
||
|
);
|
||
|
|
||
|
releasesValidated =
|
||
|
!linksUpdated && !dependentAdded && !fixedConstraintUpdated;
|
||
|
}
|
||
|
|
||
|
if (preInfo?.state.mode === "exit") {
|
||
|
for (let pkg of packages.packages) {
|
||
|
// If a package had a prerelease, but didn't trigger a version bump in the regular release,
|
||
|
// we want to give it a patch release.
|
||
|
// Detailed explanation at https://github.com/changesets/changesets/pull/382#discussion_r434434182
|
||
|
if (preInfo.preVersions.get(pkg.packageJson.name) !== 0) {
|
||
|
const existingRelease = releases.get(pkg.packageJson.name);
|
||
|
if (!existingRelease) {
|
||
|
releases.set(pkg.packageJson.name, {
|
||
|
name: pkg.packageJson.name,
|
||
|
type: "patch",
|
||
|
oldVersion: pkg.packageJson.version,
|
||
|
changesets: [],
|
||
|
});
|
||
|
} else if (
|
||
|
existingRelease.type === "none" &&
|
||
|
!refinedConfig.ignore.includes(pkg.packageJson.name)
|
||
|
) {
|
||
|
existingRelease.type = "patch";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Caching the snapshot version here and use this if it is snapshot release
|
||
|
const snapshotSuffix =
|
||
|
refinedSnapshot &&
|
||
|
getSnapshotSuffix(
|
||
|
refinedConfig.snapshot.prereleaseTemplate,
|
||
|
refinedSnapshot
|
||
|
);
|
||
|
|
||
|
return {
|
||
|
changesets: relevantChangesets,
|
||
|
releases: [...releases.values()].map((incompleteRelease) => {
|
||
|
return {
|
||
|
...incompleteRelease,
|
||
|
newVersion: snapshotSuffix
|
||
|
? getSnapshotVersion(
|
||
|
incompleteRelease,
|
||
|
preInfo,
|
||
|
refinedConfig.snapshot.useCalculatedVersion,
|
||
|
snapshotSuffix
|
||
|
)
|
||
|
: getNewVersion(incompleteRelease, preInfo),
|
||
|
};
|
||
|
}),
|
||
|
preState: preInfo?.state,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getRelevantChangesets(
|
||
|
changesets: NewChangeset[],
|
||
|
ignored: Readonly<string[]>,
|
||
|
preState: PreState | undefined
|
||
|
): NewChangeset[] {
|
||
|
for (const changeset of changesets) {
|
||
|
// Using the following 2 arrays to decide whether a changeset
|
||
|
// contains both ignored and not ignored packages
|
||
|
const ignoredPackages = [];
|
||
|
const notIgnoredPackages = [];
|
||
|
for (const release of changeset.releases) {
|
||
|
if (
|
||
|
ignored.find(
|
||
|
(ignoredPackageName) => ignoredPackageName === release.name
|
||
|
)
|
||
|
) {
|
||
|
ignoredPackages.push(release.name);
|
||
|
} else {
|
||
|
notIgnoredPackages.push(release.name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (ignoredPackages.length > 0 && notIgnoredPackages.length > 0) {
|
||
|
throw new Error(
|
||
|
`Found mixed changeset ${changeset.id}\n` +
|
||
|
`Found ignored packages: ${ignoredPackages.join(" ")}\n` +
|
||
|
`Found not ignored packages: ${notIgnoredPackages.join(" ")}\n` +
|
||
|
"Mixed changesets that contain both ignored and not ignored packages are not allowed"
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (preState && preState.mode !== "exit") {
|
||
|
let usedChangesetIds = new Set(preState.changesets);
|
||
|
return changesets.filter(
|
||
|
(changeset) => !usedChangesetIds.has(changeset.id)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return changesets;
|
||
|
}
|
||
|
|
||
|
function getHighestPreVersion(
|
||
|
packageGroup: PackageGroup,
|
||
|
packagesByName: Map<string, Package>
|
||
|
): number {
|
||
|
let highestPreVersion = 0;
|
||
|
for (let pkg of packageGroup) {
|
||
|
highestPreVersion = Math.max(
|
||
|
getPreVersion(packagesByName.get(pkg)!.packageJson.version),
|
||
|
highestPreVersion
|
||
|
);
|
||
|
}
|
||
|
return highestPreVersion;
|
||
|
}
|
||
|
|
||
|
function getPreInfo(
|
||
|
changesets: NewChangeset[],
|
||
|
packagesByName: Map<string, Package>,
|
||
|
config: Config,
|
||
|
preState: PreState | undefined
|
||
|
): PreInfo | undefined {
|
||
|
if (preState === undefined) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let updatedPreState = {
|
||
|
...preState,
|
||
|
changesets: changesets.map((changeset) => changeset.id),
|
||
|
initialVersions: {
|
||
|
...preState.initialVersions,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
for (const [, pkg] of packagesByName) {
|
||
|
if (updatedPreState.initialVersions[pkg.packageJson.name] === undefined) {
|
||
|
updatedPreState.initialVersions[pkg.packageJson.name] =
|
||
|
pkg.packageJson.version;
|
||
|
}
|
||
|
}
|
||
|
// Populate preVersion
|
||
|
// preVersion is the map between package name and its next pre version number.
|
||
|
let preVersions = new Map<string, number>();
|
||
|
for (const [, pkg] of packagesByName) {
|
||
|
preVersions.set(
|
||
|
pkg.packageJson.name,
|
||
|
getPreVersion(pkg.packageJson.version)
|
||
|
);
|
||
|
}
|
||
|
for (let fixedGroup of config.fixed) {
|
||
|
let highestPreVersion = getHighestPreVersion(fixedGroup, packagesByName);
|
||
|
for (let fixedPackage of fixedGroup) {
|
||
|
preVersions.set(fixedPackage, highestPreVersion);
|
||
|
}
|
||
|
}
|
||
|
for (let linkedGroup of config.linked) {
|
||
|
let highestPreVersion = getHighestPreVersion(linkedGroup, packagesByName);
|
||
|
for (let linkedPackage of linkedGroup) {
|
||
|
preVersions.set(linkedPackage, highestPreVersion);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
state: updatedPreState,
|
||
|
preVersions,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
export default assembleReleasePlan;
|