808 lines
27 KiB
TypeScript
808 lines
27 KiB
TypeScript
import * as chalk from 'chalk';
|
|
import { execSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { relative } from 'node:path';
|
|
import { Generator } from '../../config/misc-interfaces';
|
|
import {
|
|
NxJsonConfiguration,
|
|
NxReleaseConfiguration,
|
|
readNxJson,
|
|
} from '../../config/nx-json';
|
|
import {
|
|
ProjectGraph,
|
|
ProjectGraphProjectNode,
|
|
} from '../../config/project-graph';
|
|
import { FsTree, Tree, flushChanges } from '../../generators/tree';
|
|
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
|
|
import {
|
|
createProjectGraphAsync,
|
|
readProjectsConfigurationFromProjectGraph,
|
|
} from '../../project-graph/project-graph';
|
|
import { output } from '../../utils/output';
|
|
import { combineOptionsForGenerator } from '../../utils/params';
|
|
import { joinPathFragments } from '../../utils/path';
|
|
import { workspaceRoot } from '../../utils/workspace-root';
|
|
import { parseGeneratorString } from '../generate/generate';
|
|
import { getGeneratorInformation } from '../generate/generator-utils';
|
|
import { VersionOptions } from './command-object';
|
|
import {
|
|
NxReleaseConfig,
|
|
createNxReleaseConfig,
|
|
handleNxReleaseConfigError,
|
|
} from './config/config';
|
|
import { deepMergeJson } from './config/deep-merge-json';
|
|
import {
|
|
ReleaseGroupWithName,
|
|
filterReleaseGroups,
|
|
} from './config/filter-release-groups';
|
|
import {
|
|
readRawVersionPlans,
|
|
setResolvedVersionPlansOnGroups,
|
|
} from './config/version-plans';
|
|
import { batchProjectsByGeneratorConfig } from './utils/batch-projects-by-generator-config';
|
|
import { gitAdd, gitPush, gitTag } from './utils/git';
|
|
import { printDiff } from './utils/print-changes';
|
|
import { printConfigAndExit } from './utils/print-config';
|
|
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
|
|
import {
|
|
ReleaseVersionGeneratorResult,
|
|
VersionData,
|
|
commitChanges,
|
|
createCommitMessageValues,
|
|
createGitTagValues,
|
|
handleDuplicateGitTags,
|
|
} from './utils/shared';
|
|
import { handleErrors } from '../../utils/handle-errors';
|
|
|
|
const LARGE_BUFFER = 1024 * 1000000;
|
|
|
|
// Reexport some utils for use in plugin release-version generator implementations
|
|
export { deriveNewSemverVersion } from './utils/semver';
|
|
export type {
|
|
ReleaseVersionGeneratorResult,
|
|
VersionData,
|
|
} from './utils/shared';
|
|
|
|
export const validReleaseVersionPrefixes = ['auto', '', '~', '^', '='] as const;
|
|
|
|
export interface ReleaseVersionGeneratorSchema {
|
|
// The projects being versioned in the current execution
|
|
projects: ProjectGraphProjectNode[];
|
|
releaseGroup: ReleaseGroupWithName;
|
|
projectGraph: ProjectGraph;
|
|
specifier?: string;
|
|
specifierSource?: 'prompt' | 'conventional-commits' | 'version-plans';
|
|
preid?: string;
|
|
packageRoot?: string;
|
|
currentVersionResolver?: 'registry' | 'disk' | 'git-tag';
|
|
currentVersionResolverMetadata?: Record<string, unknown>;
|
|
fallbackCurrentVersionResolver?: 'disk';
|
|
firstRelease?: boolean;
|
|
// auto means the existing prefix will be preserved, and is the default behavior
|
|
versionPrefix?: (typeof validReleaseVersionPrefixes)[number];
|
|
skipLockFileUpdate?: boolean;
|
|
installArgs?: string;
|
|
installIgnoreScripts?: boolean;
|
|
conventionalCommitsConfig?: NxReleaseConfig['conventionalCommits'];
|
|
deleteVersionPlans?: boolean;
|
|
/**
|
|
* 'auto' is the default and will cause dependents to be updated (a patch version bump) when a dependency is versioned.
|
|
* This is only applicable to independently released projects. 'never' will cause dependents to not be updated.
|
|
*/
|
|
updateDependents?: 'auto' | 'never';
|
|
/**
|
|
* Whether or not to completely omit project logs when that project has no applicable changes. This can be useful for
|
|
* large monorepos which have a large number of projects, especially when only a subset are released together.
|
|
*/
|
|
logUnchangedProjects?: boolean;
|
|
/**
|
|
* Whether or not to keep local dependency protocols (e.g. file:, workspace:) when updating dependencies in
|
|
* package.json files. This is `false` by default as not all package managers support publishing with these protocols
|
|
* still present in the package.json.
|
|
*/
|
|
preserveLocalDependencyProtocols?: boolean;
|
|
}
|
|
|
|
export interface NxReleaseVersionResult {
|
|
/**
|
|
* In one specific (and very common) case, an overall workspace version is relevant, for example when there is
|
|
* only a single release group in which all projects have a fixed relationship to each other. In this case, the
|
|
* overall workspace version is the same as the version of the release group (and every project within it). This
|
|
* version could be a `string`, or it could be `null` if using conventional commits and no changes were detected.
|
|
*
|
|
* In all other cases (independent versioning, multiple release groups etc), the overall workspace version is
|
|
* not applicable and will be `undefined` here. If a user attempts to use this value later when it is `undefined`
|
|
* (for example in the changelog command), we will throw an appropriate error.
|
|
*/
|
|
workspaceVersion: (string | null) | undefined;
|
|
projectsVersionData: VersionData;
|
|
}
|
|
|
|
export const releaseVersionCLIHandler = (args: VersionOptions) =>
|
|
handleErrors(args.verbose, () => createAPI({})(args));
|
|
|
|
export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
|
|
/**
|
|
* NOTE: This function is also exported for programmatic usage and forms part of the public API
|
|
* of Nx. We intentionally do not wrap the implementation with handleErrors because users need
|
|
* to have control over their own error handling when using the API.
|
|
*/
|
|
return async function releaseVersion(
|
|
args: VersionOptions
|
|
): Promise<NxReleaseVersionResult> {
|
|
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
|
const { projects } =
|
|
readProjectsConfigurationFromProjectGraph(projectGraph);
|
|
const nxJson = readNxJson();
|
|
const userProvidedReleaseConfig = deepMergeJson(
|
|
nxJson.release ?? {},
|
|
overrideReleaseConfig ?? {}
|
|
);
|
|
|
|
// Apply default configuration to any optional user configuration
|
|
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
|
|
projectGraph,
|
|
await createProjectFileMapUsingProjectGraph(projectGraph),
|
|
userProvidedReleaseConfig
|
|
);
|
|
if (configError) {
|
|
return await handleNxReleaseConfigError(configError);
|
|
}
|
|
// --print-config exits directly as it is not designed to be combined with any other programmatic operations
|
|
if (args.printConfig) {
|
|
return printConfigAndExit({
|
|
userProvidedReleaseConfig,
|
|
nxReleaseConfig,
|
|
isDebug: args.printConfig === 'debug',
|
|
});
|
|
}
|
|
|
|
// The nx release top level command will always override these three git args. This is how we can tell
|
|
// if the top level release command was used or if the user is using the changelog subcommand.
|
|
// If the user explicitly overrides these args, then it doesn't matter if the top level config is set,
|
|
// as all of the git options would be overridden anyway.
|
|
if (
|
|
(args.gitCommit === undefined ||
|
|
args.gitTag === undefined ||
|
|
args.stageChanges === undefined) &&
|
|
userProvidedReleaseConfig.git
|
|
) {
|
|
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
|
|
'release',
|
|
'git',
|
|
]);
|
|
output.error({
|
|
title: `The "release.git" property in nx.json may not be used with the "nx release version" subcommand or programmatic API. Instead, configure git options for subcommands directly with "release.version.git" and "release.changelog.git".`,
|
|
bodyLines: [nxJsonMessage],
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
const {
|
|
error: filterError,
|
|
filterLog,
|
|
releaseGroups,
|
|
releaseGroupToFilteredProjects,
|
|
} = filterReleaseGroups(
|
|
projectGraph,
|
|
nxReleaseConfig,
|
|
args.projects,
|
|
args.groups
|
|
);
|
|
if (filterError) {
|
|
output.error(filterError);
|
|
process.exit(1);
|
|
}
|
|
if (
|
|
filterLog &&
|
|
process.env.NX_RELEASE_INTERNAL_SUPPRESS_FILTER_LOG !== 'true'
|
|
) {
|
|
output.note(filterLog);
|
|
}
|
|
|
|
if (!args.specifier) {
|
|
const rawVersionPlans = await readRawVersionPlans();
|
|
await setResolvedVersionPlansOnGroups(
|
|
rawVersionPlans,
|
|
releaseGroups,
|
|
Object.keys(projectGraph.nodes),
|
|
args.verbose
|
|
);
|
|
} else {
|
|
if (args.verbose && releaseGroups.some((g) => !!g.versionPlans)) {
|
|
console.log(
|
|
`Skipping version plan discovery as a specifier was provided`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (args.deleteVersionPlans === undefined) {
|
|
// default to not delete version plans after versioning as they may be needed for changelog generation
|
|
args.deleteVersionPlans = false;
|
|
}
|
|
|
|
runPreVersionCommand(nxReleaseConfig.version.preVersionCommand, {
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
|
|
const tree = new FsTree(workspaceRoot, args.verbose);
|
|
|
|
const versionData: VersionData = {};
|
|
const commitMessage: string | undefined =
|
|
args.gitCommitMessage || nxReleaseConfig.version.git.commitMessage;
|
|
const generatorCallbacks: (() => Promise<void>)[] = [];
|
|
|
|
/**
|
|
* additionalChangedFiles are files which need to be updated as a side-effect of versioning (such as package manager lock files),
|
|
* and need to get staged and committed as part of the existing commit, if applicable.
|
|
*/
|
|
const additionalChangedFiles = new Set<string>();
|
|
const additionalDeletedFiles = new Set<string>();
|
|
|
|
if (args.projects?.length) {
|
|
/**
|
|
* Run versioning for all remaining release groups and filtered projects within them
|
|
*/
|
|
for (const releaseGroup of releaseGroups) {
|
|
const releaseGroupName = releaseGroup.name;
|
|
const releaseGroupProjectNames = Array.from(
|
|
releaseGroupToFilteredProjects.get(releaseGroup)
|
|
);
|
|
const projectBatches = batchProjectsByGeneratorConfig(
|
|
projectGraph,
|
|
releaseGroup,
|
|
// Only batch based on the filtered projects within the release group
|
|
releaseGroupProjectNames
|
|
);
|
|
|
|
for (const [
|
|
generatorConfigString,
|
|
projectNames,
|
|
] of projectBatches.entries()) {
|
|
const [generatorName, generatorOptions] = JSON.parse(
|
|
generatorConfigString
|
|
);
|
|
// Resolve the generator for the batch and run versioning on the projects within the batch
|
|
const generatorData = resolveGeneratorData({
|
|
...extractGeneratorCollectionAndName(
|
|
`batch "${JSON.stringify(
|
|
projectNames
|
|
)}" for release-group "${releaseGroupName}"`,
|
|
generatorName
|
|
),
|
|
configGeneratorOptions: generatorOptions,
|
|
// all project data from the project graph (not to be confused with projectNamesToRunVersionOn)
|
|
projects,
|
|
});
|
|
const generatorCallback = await runVersionOnProjects(
|
|
projectGraph,
|
|
nxJson,
|
|
args,
|
|
tree,
|
|
generatorData,
|
|
args.generatorOptionsOverrides,
|
|
projectNames,
|
|
releaseGroup,
|
|
versionData,
|
|
nxReleaseConfig.conventionalCommits
|
|
);
|
|
// Capture the callback so that we can run it after flushing the changes to disk
|
|
generatorCallbacks.push(async () => {
|
|
const result = await generatorCallback(tree, {
|
|
dryRun: !!args.dryRun,
|
|
verbose: !!args.verbose,
|
|
generatorOptions: {
|
|
...generatorOptions,
|
|
...args.generatorOptionsOverrides,
|
|
},
|
|
});
|
|
const { changedFiles, deletedFiles } =
|
|
parseGeneratorCallbackResult(result);
|
|
changedFiles.forEach((f) => additionalChangedFiles.add(f));
|
|
deletedFiles.forEach((f) => additionalDeletedFiles.add(f));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Resolve any git tags as early as possible so that we can hard error in case of any duplicates before reaching the actual git command
|
|
const gitTagValues: string[] =
|
|
args.gitTag ?? nxReleaseConfig.version.git.tag
|
|
? createGitTagValues(
|
|
releaseGroups,
|
|
releaseGroupToFilteredProjects,
|
|
versionData
|
|
)
|
|
: [];
|
|
handleDuplicateGitTags(gitTagValues);
|
|
|
|
printAndFlushChanges(tree, !!args.dryRun);
|
|
|
|
for (const generatorCallback of generatorCallbacks) {
|
|
await generatorCallback();
|
|
}
|
|
|
|
const changedFiles = [
|
|
...tree.listChanges().map((f) => f.path),
|
|
...additionalChangedFiles,
|
|
];
|
|
|
|
// No further actions are necessary in this scenario (e.g. if conventional commits detected no changes)
|
|
if (!changedFiles.length) {
|
|
return {
|
|
// An overall workspace version cannot be relevant when filtering to independent projects
|
|
workspaceVersion: undefined,
|
|
projectsVersionData: versionData,
|
|
};
|
|
}
|
|
|
|
if (args.gitCommit ?? nxReleaseConfig.version.git.commit) {
|
|
await commitChanges({
|
|
changedFiles,
|
|
deletedFiles: Array.from(additionalDeletedFiles),
|
|
isDryRun: !!args.dryRun,
|
|
isVerbose: !!args.verbose,
|
|
gitCommitMessages: createCommitMessageValues(
|
|
releaseGroups,
|
|
releaseGroupToFilteredProjects,
|
|
versionData,
|
|
commitMessage
|
|
),
|
|
gitCommitArgs:
|
|
args.gitCommitArgs || nxReleaseConfig.version.git.commitArgs,
|
|
});
|
|
} else if (
|
|
args.stageChanges ??
|
|
nxReleaseConfig.version.git.stageChanges
|
|
) {
|
|
output.logSingleLine(`Staging changed files with git`);
|
|
await gitAdd({
|
|
changedFiles,
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
}
|
|
|
|
if (args.gitTag ?? nxReleaseConfig.version.git.tag) {
|
|
output.logSingleLine(`Tagging commit with git`);
|
|
for (const tag of gitTagValues) {
|
|
await gitTag({
|
|
tag,
|
|
message:
|
|
args.gitTagMessage || nxReleaseConfig.version.git.tagMessage,
|
|
additionalArgs:
|
|
args.gitTagArgs || nxReleaseConfig.version.git.tagArgs,
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (args.gitPush ?? nxReleaseConfig.version.git.push) {
|
|
output.logSingleLine(`Pushing to git remote "${args.gitRemote}"`);
|
|
await gitPush({
|
|
gitRemote: args.gitRemote,
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
}
|
|
|
|
return {
|
|
// An overall workspace version cannot be relevant when filtering to independent projects
|
|
workspaceVersion: undefined,
|
|
projectsVersionData: versionData,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run versioning for all remaining release groups
|
|
*/
|
|
for (const releaseGroup of releaseGroups) {
|
|
const releaseGroupName = releaseGroup.name;
|
|
|
|
runPreVersionCommand(
|
|
releaseGroup.version.groupPreVersionCommand,
|
|
{
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
},
|
|
releaseGroup
|
|
);
|
|
|
|
const projectBatches = batchProjectsByGeneratorConfig(
|
|
projectGraph,
|
|
releaseGroup,
|
|
// Batch based on all projects within the release group
|
|
releaseGroup.projects
|
|
);
|
|
|
|
for (const [
|
|
generatorConfigString,
|
|
projectNames,
|
|
] of projectBatches.entries()) {
|
|
const [generatorName, generatorOptions] = JSON.parse(
|
|
generatorConfigString
|
|
);
|
|
// Resolve the generator for the batch and run versioning on the projects within the batch
|
|
const generatorData = resolveGeneratorData({
|
|
...extractGeneratorCollectionAndName(
|
|
`batch "${JSON.stringify(
|
|
projectNames
|
|
)}" for release-group "${releaseGroupName}"`,
|
|
generatorName
|
|
),
|
|
configGeneratorOptions: generatorOptions,
|
|
// all project data from the project graph (not to be confused with projectNamesToRunVersionOn)
|
|
projects,
|
|
});
|
|
const generatorCallback = await runVersionOnProjects(
|
|
projectGraph,
|
|
nxJson,
|
|
args,
|
|
tree,
|
|
generatorData,
|
|
args.generatorOptionsOverrides,
|
|
projectNames,
|
|
releaseGroup,
|
|
versionData,
|
|
nxReleaseConfig.conventionalCommits
|
|
);
|
|
// Capture the callback so that we can run it after flushing the changes to disk
|
|
generatorCallbacks.push(async () => {
|
|
const result = await generatorCallback(tree, {
|
|
dryRun: !!args.dryRun,
|
|
verbose: !!args.verbose,
|
|
generatorOptions: {
|
|
...generatorOptions,
|
|
...args.generatorOptionsOverrides,
|
|
},
|
|
});
|
|
const { changedFiles, deletedFiles } =
|
|
parseGeneratorCallbackResult(result);
|
|
changedFiles.forEach((f) => additionalChangedFiles.add(f));
|
|
deletedFiles.forEach((f) => additionalDeletedFiles.add(f));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Resolve any git tags as early as possible so that we can hard error in case of any duplicates before reaching the actual git command
|
|
const gitTagValues: string[] =
|
|
args.gitTag ?? nxReleaseConfig.version.git.tag
|
|
? createGitTagValues(
|
|
releaseGroups,
|
|
releaseGroupToFilteredProjects,
|
|
versionData
|
|
)
|
|
: [];
|
|
handleDuplicateGitTags(gitTagValues);
|
|
|
|
printAndFlushChanges(tree, !!args.dryRun);
|
|
|
|
for (const generatorCallback of generatorCallbacks) {
|
|
await generatorCallback();
|
|
}
|
|
|
|
// Only applicable when there is a single release group with a fixed relationship
|
|
let workspaceVersion: string | null | undefined = undefined;
|
|
if (releaseGroups.length === 1) {
|
|
const releaseGroup = releaseGroups[0];
|
|
if (releaseGroup.projectsRelationship === 'fixed') {
|
|
const releaseGroupProjectNames = Array.from(
|
|
releaseGroupToFilteredProjects.get(releaseGroup)
|
|
);
|
|
workspaceVersion = versionData[releaseGroupProjectNames[0]].newVersion; // all projects have the same version so we can just grab the first
|
|
}
|
|
}
|
|
|
|
const changedFiles = [
|
|
...tree.listChanges().map((f) => f.path),
|
|
...additionalChangedFiles,
|
|
];
|
|
const deletedFiles = Array.from(additionalDeletedFiles);
|
|
|
|
// No further actions are necessary in this scenario (e.g. if conventional commits detected no changes)
|
|
if (!changedFiles.length && !deletedFiles.length) {
|
|
return {
|
|
workspaceVersion,
|
|
projectsVersionData: versionData,
|
|
};
|
|
}
|
|
|
|
if (args.gitCommit ?? nxReleaseConfig.version.git.commit) {
|
|
await commitChanges({
|
|
changedFiles,
|
|
deletedFiles,
|
|
isDryRun: !!args.dryRun,
|
|
isVerbose: !!args.verbose,
|
|
gitCommitMessages: createCommitMessageValues(
|
|
releaseGroups,
|
|
releaseGroupToFilteredProjects,
|
|
versionData,
|
|
commitMessage
|
|
),
|
|
gitCommitArgs:
|
|
args.gitCommitArgs || nxReleaseConfig.version.git.commitArgs,
|
|
});
|
|
} else if (args.stageChanges ?? nxReleaseConfig.version.git.stageChanges) {
|
|
output.logSingleLine(`Staging changed files with git`);
|
|
await gitAdd({
|
|
changedFiles,
|
|
deletedFiles,
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
}
|
|
|
|
if (args.gitTag ?? nxReleaseConfig.version.git.tag) {
|
|
output.logSingleLine(`Tagging commit with git`);
|
|
for (const tag of gitTagValues) {
|
|
await gitTag({
|
|
tag,
|
|
message: args.gitTagMessage || nxReleaseConfig.version.git.tagMessage,
|
|
additionalArgs:
|
|
args.gitTagArgs || nxReleaseConfig.version.git.tagArgs,
|
|
dryRun: args.dryRun,
|
|
verbose: args.verbose,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
workspaceVersion,
|
|
projectsVersionData: versionData,
|
|
};
|
|
};
|
|
}
|
|
|
|
function appendVersionData(
|
|
existingVersionData: VersionData,
|
|
newVersionData: VersionData
|
|
): VersionData {
|
|
// Mutate the existing version data
|
|
for (const [key, value] of Object.entries(newVersionData)) {
|
|
if (existingVersionData[key]) {
|
|
throw new Error(
|
|
`Version data key "${key}" already exists in version data. This is likely a bug, please report your use-case on https://github.com/nrwl/nx`
|
|
);
|
|
}
|
|
existingVersionData[key] = value;
|
|
}
|
|
return existingVersionData;
|
|
}
|
|
|
|
async function runVersionOnProjects(
|
|
projectGraph: ProjectGraph,
|
|
nxJson: NxJsonConfiguration,
|
|
args: VersionOptions,
|
|
tree: Tree,
|
|
generatorData: GeneratorData,
|
|
generatorOverrides: Record<string, unknown> | undefined,
|
|
projectNames: string[],
|
|
releaseGroup: ReleaseGroupWithName,
|
|
versionData: VersionData,
|
|
conventionalCommitsConfig: NxReleaseConfig['conventionalCommits']
|
|
): Promise<ReleaseVersionGeneratorResult['callback']> {
|
|
const generatorOptions: ReleaseVersionGeneratorSchema = {
|
|
// Always ensure a string to avoid generator schema validation errors
|
|
specifier: args.specifier ?? '',
|
|
preid: args.preid ?? '',
|
|
...generatorData.configGeneratorOptions,
|
|
...(generatorOverrides ?? {}),
|
|
// The following are not overridable by user config
|
|
projects: projectNames.map((p) => projectGraph.nodes[p]),
|
|
projectGraph,
|
|
releaseGroup,
|
|
firstRelease: args.firstRelease ?? false,
|
|
conventionalCommitsConfig,
|
|
deleteVersionPlans: args.deleteVersionPlans,
|
|
};
|
|
|
|
// Apply generator defaults from schema.json file etc
|
|
const combinedOpts = await combineOptionsForGenerator(
|
|
generatorOptions as any,
|
|
generatorData.collectionName,
|
|
generatorData.normalizedGeneratorName,
|
|
readProjectsConfigurationFromProjectGraph(projectGraph),
|
|
nxJson,
|
|
generatorData.schema,
|
|
false,
|
|
null,
|
|
relative(process.cwd(), workspaceRoot),
|
|
args.verbose
|
|
);
|
|
|
|
const releaseVersionGenerator = generatorData.implementationFactory();
|
|
|
|
// We expect all version generator implementations to return a ReleaseVersionGeneratorResult object, rather than a GeneratorCallback
|
|
const versionResult = (await releaseVersionGenerator(
|
|
tree,
|
|
combinedOpts
|
|
)) as unknown as ReleaseVersionGeneratorResult;
|
|
|
|
if (typeof versionResult === 'function') {
|
|
throw new Error(
|
|
`The version generator ${generatorData.collectionName}:${generatorData.normalizedGeneratorName} returned a function instead of an expected ReleaseVersionGeneratorResult`
|
|
);
|
|
}
|
|
|
|
// Merge the extra version data into the existing
|
|
appendVersionData(versionData, versionResult.data);
|
|
|
|
return versionResult.callback;
|
|
}
|
|
|
|
function printAndFlushChanges(tree: Tree, isDryRun: boolean) {
|
|
const changes = tree.listChanges();
|
|
|
|
console.log('');
|
|
|
|
// Print the changes
|
|
changes.forEach((f) => {
|
|
if (f.type === 'CREATE') {
|
|
console.error(
|
|
`${chalk.green('CREATE')} ${f.path}${
|
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
}`
|
|
);
|
|
printDiff('', f.content?.toString() || '');
|
|
} else if (f.type === 'UPDATE') {
|
|
console.error(
|
|
`${chalk.white('UPDATE')} ${f.path}${
|
|
isDryRun ? chalk.keyword('orange')(' [dry-run]') : ''
|
|
}`
|
|
);
|
|
const currentContentsOnDisk = readFileSync(
|
|
joinPathFragments(tree.root, f.path)
|
|
).toString();
|
|
printDiff(currentContentsOnDisk, f.content?.toString() || '');
|
|
} else if (f.type === 'DELETE' && !f.path.includes('.nx')) {
|
|
throw new Error(
|
|
'Unexpected DELETE change, please report this as an issue'
|
|
);
|
|
}
|
|
});
|
|
|
|
if (!isDryRun) {
|
|
flushChanges(workspaceRoot, changes);
|
|
}
|
|
}
|
|
|
|
function extractGeneratorCollectionAndName(
|
|
description: string,
|
|
generatorString: string
|
|
) {
|
|
let collectionName: string;
|
|
let generatorName: string;
|
|
const parsedGeneratorString = parseGeneratorString(generatorString);
|
|
collectionName = parsedGeneratorString.collection;
|
|
generatorName = parsedGeneratorString.generator;
|
|
|
|
if (!collectionName || !generatorName) {
|
|
throw new Error(
|
|
`Invalid generator string: ${generatorString} used for ${description}. Must be in the format of [collectionName]:[generatorName]`
|
|
);
|
|
}
|
|
|
|
return { collectionName, generatorName };
|
|
}
|
|
|
|
interface GeneratorData {
|
|
collectionName: string;
|
|
generatorName: string;
|
|
configGeneratorOptions: NxJsonConfiguration['release']['groups'][number]['version']['generatorOptions'];
|
|
normalizedGeneratorName: string;
|
|
schema: any;
|
|
implementationFactory: () => Generator<unknown>;
|
|
}
|
|
|
|
function resolveGeneratorData({
|
|
collectionName,
|
|
generatorName,
|
|
configGeneratorOptions,
|
|
projects,
|
|
}): GeneratorData {
|
|
try {
|
|
const { normalizedGeneratorName, schema, implementationFactory } =
|
|
getGeneratorInformation(
|
|
collectionName,
|
|
generatorName,
|
|
workspaceRoot,
|
|
projects
|
|
);
|
|
|
|
return {
|
|
collectionName,
|
|
generatorName,
|
|
configGeneratorOptions,
|
|
normalizedGeneratorName,
|
|
schema,
|
|
implementationFactory,
|
|
};
|
|
} catch (err) {
|
|
if (err.message.startsWith('Unable to resolve')) {
|
|
// See if it is because the plugin is not installed
|
|
try {
|
|
require.resolve(collectionName);
|
|
// is installed
|
|
throw new Error(
|
|
`Unable to resolve the generator called "${generatorName}" within the "${collectionName}" package`
|
|
);
|
|
} catch {
|
|
/**
|
|
* Special messaging for the most common case (especially as the user is unlikely to explicitly have
|
|
* the @nx/js generator config in their nx.json so we need to be clear about what the problem is)
|
|
*/
|
|
if (collectionName === '@nx/js') {
|
|
throw new Error(
|
|
'The @nx/js plugin is required in order to version your JavaScript packages. Run "nx add @nx/js" to add it to your workspace.'
|
|
);
|
|
}
|
|
throw new Error(
|
|
`Unable to resolve the package ${collectionName} in order to load the generator called ${generatorName}. Is the package installed?`
|
|
);
|
|
}
|
|
}
|
|
// Unexpected error, rethrow
|
|
throw err;
|
|
}
|
|
}
|
|
function runPreVersionCommand(
|
|
preVersionCommand: string,
|
|
{ dryRun, verbose }: { dryRun: boolean; verbose: boolean },
|
|
releaseGroup?: ReleaseGroupWithName
|
|
) {
|
|
if (!preVersionCommand) {
|
|
return;
|
|
}
|
|
|
|
output.logSingleLine(
|
|
releaseGroup
|
|
? `Executing release group pre-version command for "${releaseGroup.name}"`
|
|
: `Executing pre-version command`
|
|
);
|
|
if (verbose) {
|
|
console.log(`Executing the following pre-version command:`);
|
|
console.log(preVersionCommand);
|
|
}
|
|
|
|
let env: Record<string, string> = {
|
|
...process.env,
|
|
};
|
|
if (dryRun) {
|
|
env.NX_DRY_RUN = 'true';
|
|
}
|
|
|
|
const stdio = verbose ? 'inherit' : 'pipe';
|
|
try {
|
|
execSync(preVersionCommand, {
|
|
encoding: 'utf-8',
|
|
maxBuffer: LARGE_BUFFER,
|
|
stdio,
|
|
env,
|
|
windowsHide: false,
|
|
});
|
|
} catch (e) {
|
|
const title = verbose
|
|
? `The pre-version command failed. See the full output above.`
|
|
: `The pre-version command failed. Retry with --verbose to see the full output of the pre-version command.`;
|
|
output.error({
|
|
title,
|
|
bodyLines: [preVersionCommand, e],
|
|
});
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function parseGeneratorCallbackResult(
|
|
result: string[] | { changedFiles: string[]; deletedFiles: string[] }
|
|
): { changedFiles: string[]; deletedFiles: string[] } {
|
|
if (Array.isArray(result)) {
|
|
return {
|
|
changedFiles: result,
|
|
deletedFiles: [],
|
|
};
|
|
} else {
|
|
return result;
|
|
}
|
|
}
|