Compare commits

...

1 Commits

Author SHA1 Message Date
JamesHenry d314e734f0 chore: wipp latest 2025-02-10 14:28:12 +04:00
16 changed files with 6034 additions and 485 deletions
+1
View File
@@ -53,6 +53,7 @@
"@jest/reporters": "29.7.0",
"@jest/test-result": "29.7.0",
"@jest/types": "29.6.3",
"@ltd/j-toml": "^1.38.0",
"@module-federation/enhanced": "^0.8.8",
"@module-federation/sdk": "^0.8.8",
"@monodon/rust": "2.1.1",
@@ -0,0 +1,109 @@
import { readJson, Tree, updateJson } from '@nx/devkit';
import { join } from 'node:path';
// TODO: Export from release API before merging
import {
ManifestActions,
ManifestData,
} from 'nx/src/command-line/release/version-utils/flexible-version-management';
import { resolveVersionSpec } from './utils/resolve-version-spec';
export default class JsManifestActions extends ManifestActions {
getPrimaryManifestPath(): string {
return join(this.projectGraphNode.data.root, 'package.json');
}
async ensureManifestExistsAtExpectedLocation(tree: Tree) {
const packageJsonPath = this.getPrimaryManifestPath();
if (!tree.exists(packageJsonPath)) {
throw new Error(
`The project "${this.projectGraphNode.name}" does not have a package.json available at ${packageJsonPath}.
To fix this you will either need to add a package.json file at that location, or configure "release" within your nx.json to exclude "${this.projectGraphNode.name}" from the current release group, or amend the packageRoot configuration to point to where the package.json should be.`
);
}
}
async readManifestData(tree: Tree): Promise<ManifestData> {
const packageJson = readJson(tree, this.getPrimaryManifestPath());
const dependencies = this.parseDependencies(packageJson);
return {
name: packageJson.name,
currentVersion: packageJson.version,
dependencies,
};
}
async resolveCurrentVersion(tree: Tree): Promise<string> {
const packageJsonPath = this.getPrimaryManifestPath();
try {
const packageJson = readJson(tree, packageJsonPath);
return packageJson.version;
} catch {
throw new Error(
`Unable to determine the current version for project "${this.projectGraphNode.name}" from ${packageJsonPath}, please ensure that the "version" field is set within the package.json file`
);
}
}
async writeVersionToManifest(tree: Tree, newVersion: string) {
updateJson(tree, this.getPrimaryManifestPath(), (json) => {
json.version = newVersion;
return json;
});
}
async updateDependencies(
tree: Tree,
dependenciesToUpdate: Record<string, string>
) {
updateJson(tree, this.getPrimaryManifestPath(), (json) => {
const dependencyTypes = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
for (const depType of dependencyTypes) {
if (json[depType]) {
for (const [dep, version] of Object.entries(dependenciesToUpdate)) {
if (json[depType][dep]) {
json[depType][dep] = version;
}
}
}
}
return json;
});
}
private parseDependencies(
packageJson: any
): Record<string, Record<string, string>> {
const result: Record<string, Record<string, string>> = {};
const dependencyTypes = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
for (const depType of dependencyTypes) {
if (packageJson[depType]) {
result[depType] = {};
for (const [dep, spec] of Object.entries(packageJson[depType])) {
const resolvedSpec = resolveVersionSpec(
dep,
packageJson.version,
spec as string,
this.projectGraphNode.data.root
);
result[depType][dep] = resolvedSpec;
}
}
}
return result;
}
}
@@ -8,6 +8,8 @@
"properties": {
"projects": {
"type": "array",
"hidden": true,
"$comment": "This is not configured/passed by the user. It is provided by the version command.",
"description": "The ProjectGraphProjectNodes being versioned in the current execution.",
"items": {
"type": "object"
@@ -15,16 +17,33 @@
},
"projectGraph": {
"type": "object",
"hidden": true,
"$comment": "This is not configured/passed by the user. It is provided by the version command.",
"description": "ProjectGraph instance"
},
"releaseGroup": {
"type": "object",
"hidden": true,
"$comment": "This is not configured/passed by the user. It is provided by the version command.",
"description": "The resolved release group configuration, including name, relevant to all projects in the current execution."
},
"conventionalCommitsConfig": {
"type": "object",
"hidden": true,
"$comment": "This is not configured/passed by the user. It is provided by the version command.",
"description": "The conventional commits configuration to use when determining the next version of the project.",
"default": {}
},
"firstRelease": {
"type": "boolean",
"hidden": true,
"$comment": "This is not configured/passed by the user. It is provided by the version command.",
"description": "Whether this is the first release of the project (which skips some validation within the generator)."
},
"specifier": {
"type": "string",
"description": "Exact version or semver keyword to apply to the selected release group. Overrides specifierSource."
},
"releaseGroup": {
"type": "object",
"description": "The resolved release group configuration, including name, relevant to all projects in the current execution."
},
"specifierSource": {
"type": "string",
"default": "prompt",
+92
View File
@@ -650,6 +650,7 @@
},
"NxReleaseVersionConfiguration": {
"type": "object",
"description": "Configuration for the versioning phase of releases.",
"properties": {
"conventionalCommits": {
"type": "boolean",
@@ -661,6 +662,7 @@
},
"generatorOptions": {
"type": "object",
"description": "These options will be passed to the configured \"release.version.generator\" (which will be \"@nx/js:release-version\" if not set explicitly).",
"additionalProperties": true
},
"git": {
@@ -671,6 +673,36 @@
"description": "A command to run after validation of nx release configuration, but before versioning begins. Useful for preparing build artifacts. If --dry-run is passed, the command is still executed, but with the NX_DRY_RUN environment variable set to 'true'."
}
},
"if": {
"$comment": "The generatorOptions are generic and dictated by the generator being used. Because usage with JS is so common, we default to the JS release version generator, so we correspondingly default to a schema for the JS release version generator options to provide the best DX.",
"properties": {
"generator": {
"anyOf": [
{
"const": "@nx/js:release-version"
},
{
"type": "null"
}
]
}
}
},
"then": {
"properties": {
"generatorOptions": {
"$ref": "#/definitions/NxJsReleaseVersionGeneratorOptions"
}
}
},
"else": {
"properties": {
"generatorOptions": {
"type": "object",
"additionalProperties": true
}
}
},
"additionalProperties": false
},
"NxReleaseGroupVersionConfiguration": {
@@ -825,6 +857,66 @@
}
}
}
},
"NxJsReleaseVersionGeneratorOptions": {
"type": "object",
"properties": {
"specifier": {
"type": "string"
},
"specifierSource": {
"type": "string",
"enum": ["prompt", "conventional-commits", "version-plans"]
},
"preid": {
"type": "string"
},
"packageRoot": {
"type": "string"
},
"currentVersionResolver": {
"type": "string",
"enum": ["registry", "disk", "git-tag"]
},
"currentVersionResolverMetadata": {
"type": "object",
"additionalProperties": true
},
"fallbackCurrentVersionResolver": {
"type": "string",
"enum": ["disk"]
},
"firstRelease": {
"type": "boolean"
},
"versionPrefix": {
"type": "string",
"enum": ["auto", "", "~", "^", "="]
},
"skipLockFileUpdate": {
"type": "boolean"
},
"installArgs": {
"type": "string"
},
"installIgnoreScripts": {
"type": "boolean"
},
"deleteVersionPlans": {
"type": "boolean"
},
"updateDependents": {
"type": "string",
"enum": ["never", "auto"]
},
"logUnchangedProjects": {
"type": "boolean"
},
"preserveLocalDependencyProtocols": {
"type": "boolean"
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,105 @@
import type {
ProjectGraph,
ProjectGraphProjectNode,
} from '../../../config/project-graph';
import { NxReleaseConfig } from '../config/config';
import { ReleaseGroupWithName } from '../config/filter-release-groups';
import { getFirstGitCommit, getLatestGitTagForPattern } from '../utils/git';
import { resolveSemverSpecifierFromConventionalCommits } from '../utils/resolve-semver-specifier';
import { SemverBumpType } from './flexible-version-management';
import { ProjectLogger } from './project-logger';
export async function deriveSpecifierFromConventionalCommits(
nxReleaseConfig: NxReleaseConfig,
projectGraph: ProjectGraph,
logger: ProjectLogger,
releaseGroup: ReleaseGroupWithName,
projectGraphNode: ProjectGraphProjectNode,
// TODO: reevaluate this prerelease logic/workflow for independent projects
// Always assume that if the current version is a prerelease, then the next version should be a prerelease.
// Users must manually graduate from a prerelease to a release by providing an explicit specifier.
isPrerelease: boolean,
latestMatchingGitTag:
| Awaited<ReturnType<typeof getLatestGitTagForPattern>>
| undefined,
preid?: string
): Promise<SemverBumpType> {
// TODO: Move this validation up a level?
if (
releaseGroup.version.generatorOptions.currentVersionResolver !== 'git-tag'
) {
throw new Error(
`Invalid currentVersionResolver "${releaseGroup.version.generatorOptions.currentVersionResolver}" provided for release group "${releaseGroup.name}". Must be "git-tag" when "specifierSource" is "conventional-commits"`
);
}
const affectedProjects =
releaseGroup.projectsRelationship === 'independent'
? [projectGraphNode.name]
: releaseGroup.projects;
// TODO: Confirm we don't want to populate project nodes on release groups like we seemingly did before?
// : releaseGroup.projects.map((p) => p.name);
// latestMatchingGitTag will be undefined if the current version was resolved from the disk fallback.
// In this case, we want to use the first commit as the ref to be consistent with the changelog command.
const previousVersionRef = latestMatchingGitTag
? latestMatchingGitTag.tag
: releaseGroup.version.generatorOptions.fallbackCurrentVersionResolver ===
'disk'
? await getFirstGitCommit()
: undefined;
if (!previousVersionRef) {
// This should never happen since the checks above should catch if the current version couldn't be resolved
throw new Error(
`Unable to determine previous version ref for the projects ${affectedProjects.join(
', '
)}. This is likely a bug in Nx.`
);
}
let specifier = await resolveSemverSpecifierFromConventionalCommits(
previousVersionRef,
projectGraph,
affectedProjects,
nxReleaseConfig.conventionalCommits
);
if (!specifier) {
// TODO: Equivalent logging for this case
// if (
// releaseGroup.version.generatorOptions.updateDependents !== 'never' &&
// projectToDependencyBumps.has(projectName)
// ) {
// // No applicable changes to the project directly by the user, but one or more dependencies have been bumped and updateDependents is enabled
// specifier = updateDependentsBump;
// logger.buffer(
// `📄 Resolved the specifier as "${specifier}" because "release.version.generatorOptions.updateDependents" is enabled`
// );
// return 'none';
// }
logger.buffer(
`🚫 No changes were detected using git history and the conventional commits standard.`
);
return 'none';
}
// TODO: reevaluate this prerelease logic/workflow for independent projects
if (isPrerelease) {
specifier = 'prerelease';
logger.buffer(
`📄 Resolved the specifier as "${specifier}" since the current version is a prerelease.`
);
} else {
let extraText = '';
if (preid && !specifier.startsWith('pre')) {
specifier = `pre${specifier}`;
extraText = `, combined with your given preid "${preid}"`;
}
logger.buffer(
`📄 Resolved the specifier as "${specifier}" using git history and the conventional commits standard${extraText}.`
);
}
return specifier as SemverBumpType;
}
@@ -0,0 +1,133 @@
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
import type { Tree } from '../../../generators/tree';
import { ReleaseGroupWithName } from '../config/filter-release-groups';
import { deriveNewSemverVersion } from '../utils/semver';
export type SpecifierSource =
| 'prompt'
| 'conventional-commits'
| 'version-plans';
export type SemverBumpType = 'major' | 'minor' | 'patch' | 'none';
export type SideEffectBumpType = SemverBumpType | 'same-as-dependency';
// TODO: Implement this
export function deriveSpecifierFromVersionPlan(
projectPath: string
): SemverBumpType {
return 'patch';
}
let JsManifestActions;
export async function resolveManifestActionsForProject(
tree: Tree,
releaseGroup: ReleaseGroupWithName,
projectGraphNode: ProjectGraphProjectNode
): Promise<ManifestActions> {
// Project level "release" config takes priority
// TODO: Update release config type to include manifestActions
if (
typeof (projectGraphNode.data.release as any)?.manifestActions === 'string'
) {
const ManifestActionsClass = require((projectGraphNode.data.release as any)
.manifestActions);
const manifestActions = new ManifestActionsClass(projectGraphNode);
await manifestActions.ensureManifestExistsAtExpectedLocation(tree);
return manifestActions;
}
// Then release group level
if (typeof (releaseGroup as any).manifestActions === 'string') {
const ManifestActionsClass = require((releaseGroup as any).manifestActions);
const manifestActions = new ManifestActionsClass(projectGraphNode);
await manifestActions.ensureManifestExistsAtExpectedLocation(tree);
return manifestActions;
}
// Otherwise, default to the JS implementation
if (!JsManifestActions) {
// TODO: This string should probably be applied in config.ts like with other things, so that it's always set if needed,
// although we still want to preserve some caching when we do that
const ManifestActionsClass =
// nx-ignore-next-line
require('@nx/js/src/generators/release-version/manifest-actions').default;
JsManifestActions = ManifestActionsClass;
}
const manifestActions = new JsManifestActions(projectGraphNode);
await manifestActions.ensureManifestExistsAtExpectedLocation(tree);
return manifestActions;
}
export type ManifestData = {
name: string;
currentVersion: string;
dependencies: Record<string, Record<string, string>>;
};
export abstract class ManifestActions {
protected initialManifestData: ManifestData | null = null;
constructor(protected projectGraphNode: ProjectGraphProjectNode) {}
/**
* Returns the primary manifest path for the project,
* such as a package.json/Cargo.toml/etc.
*/
abstract getPrimaryManifestPath(): string;
/**
* Implementation details of resolving a project's manifest file,
* such as a package.json/Cargo.toml/etc, from disk.
*/
abstract readManifestData(tree: Tree): Promise<ManifestData>;
/**
* Implementation details of resolving a project's current version from the manifest file.
* Used as part of the current version resolver, and should throw if the version cannot be read
* from the manifest file for whatever reason.
*/
abstract resolveCurrentVersion(tree: Tree): Promise<string>;
/**
* Implementation details of writing a newly derived version to a project's
* manifest file, such as a package.json/Cargo.toml/etc.
*/
abstract writeVersionToManifest(
tree: Tree,
newVersion: string
): Promise<void>;
/**
* Implementation details of updating a project's manifest file,
* such as a package.json/Cargo.toml/etc, with new dependency versions.
*/
abstract updateDependencies(
tree: Tree,
dependenciesToUpdate: Record<string, string>
): Promise<void>;
/**
* Implementation details of ensuring that the manifest file exists at the expected location,
* will be invoked immediately after resolving the manifest actions for a particular project.
*/
abstract ensureManifestExistsAtExpectedLocation(tree: Tree): Promise<void>;
/**
* Reads and caches the initial manifest data.
*/
async getInitialManifestData(tree: Tree): Promise<ManifestData> {
if (!this.initialManifestData) {
this.initialManifestData = await this.readManifestData(tree);
}
return this.initialManifestData;
}
async determineSemverVersion(
tree: Tree,
bumpType: SemverBumpType
): Promise<string> {
const manifestData = await this.getInitialManifestData(tree);
// TODO: Support preid
return deriveNewSemverVersion(manifestData.currentVersion, bumpType);
}
}
@@ -0,0 +1,766 @@
import { createTreeWithEmptyWorkspace } from '../../../generators/testing-utils/create-tree-with-empty-workspace';
import type { Tree } from '../../../generators/tree';
import {
createNxReleaseConfigAndPopulateWorkspace,
mockResolveManifestActionsForProjectImplementation,
} from './test-utils';
let mockDeriveSpecifierFromConventionalCommits = jest.fn();
let mockDeriveSpecifierFromVersionPlan = jest.fn();
let mockResolveManifestActionsForProject = jest.fn();
jest.doMock('./derive-specifier-from-conventional-commits', () => ({
deriveSpecifierFromConventionalCommits:
mockDeriveSpecifierFromConventionalCommits,
}));
jest.doMock('./flexible-version-management', () => ({
...jest.requireActual('./flexible-version-management'),
deriveSpecifierFromVersionPlan: mockDeriveSpecifierFromVersionPlan,
resolveManifestActionsForProject: mockResolveManifestActionsForProject,
}));
let mockResolveCurrentVersion = jest.fn();
jest.doMock('./resolve-current-version', () => ({
resolveCurrentVersion: mockResolveCurrentVersion,
}));
const { ReleaseGroupProcessor } = require('./release-group-processor') as {
ReleaseGroupProcessor: typeof import('./release-group-processor').ReleaseGroupProcessor;
};
// Using the daemon in unit tests would cause jest to never exit
process.env.NX_DAEMON = 'false';
describe('Multiple Release Groups', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
jest.resetAllMocks();
mockResolveManifestActionsForProject.mockImplementation(
mockResolveManifestActionsForProjectImplementation
);
});
describe('Two unrelated groups, both fixed relationship, just JS', () => {
it('should correctly version projects using mocked conventional commits', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "fixed" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.0.0 [js]
-> depends on pkg-a
group2 ({ "projectsRelationship": "fixed" }):
- pkg-c@2.0.0 [js]
- pkg-d@2.0.0 [js]
-> depends on pkg-c
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
// Should cause pkg-a to become 1.1.0 and pkg-b as well because they are in a fixed group. pkg-b dependency on pkg-a should also be updated to 1.1.0
if (projectName === 'pkg-a') return 'minor';
if (projectName === 'pkg-b') return 'minor';
// Should cause pkg-c to become 2.0.1 and pkg-d as well because they are in a fixed group. pkg-d dependency on pkg-c should also be updated to 2.0.1
if (projectName === 'pkg-c') return 'patch';
if (projectName === 'pkg-d') return 'patch';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.1.0",
"dependencies": {
"pkg-a": "1.1.0"
}
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "2.0.1"
}
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.0.1",
"dependencies": {
"pkg-c": "2.0.1"
}
}
"
`);
});
});
describe('Two unrelated groups, both independent relationship, just JS', () => {
it('should correctly version projects using mocked conventional commits', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "independent" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.1.0 [js]
-> depends on pkg-a
group2 ({ "projectsRelationship": "independent" }):
- pkg-c@2.0.0 [js]
- pkg-d@2.1.0 [js]
-> depends on pkg-c
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
if (projectName === 'pkg-a') return 'minor';
if (projectName === 'pkg-b') return 'patch';
if (projectName === 'pkg-c') return 'major';
if (projectName === 'pkg-d') return 'none';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.1.1",
"dependencies": {
"pkg-a": "1.1.0"
}
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "3.0.0"
}
"
`);
// Patch bump due to dependency update
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.1.1",
"dependencies": {
"pkg-c": "3.0.0"
}
}
"
`);
});
});
describe('Two unrelated groups, one fixed one independent, just JS', () => {
it('should correctly version projects using mocked conventional commits', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "fixed" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.0.0 [js]
-> depends on pkg-a
group2 ({ "projectsRelationship": "independent" }):
- pkg-c@2.0.0 [js]
- pkg-d@2.1.0 [js]
-> depends on pkg-c
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
if (projectName === 'pkg-a') return 'minor';
if (projectName === 'pkg-b') return 'minor';
if (projectName === 'pkg-c') return 'patch';
if (projectName === 'pkg-d') return 'minor';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.1.0",
"dependencies": {
"pkg-a": "1.1.0"
}
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "2.0.1"
}
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.2.0",
"dependencies": {
"pkg-c": "2.0.1"
}
}
"
`);
});
});
describe('Two related groups, both fixed relationship, just JS', () => {
it('should correctly version projects across group boundaries', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "fixed" }):
- pkg-a@1.0.0 [js]
-> depends on pkg-c
- pkg-b@1.0.0 [js]
group2 ({ "projectsRelationship": "fixed" }):
- pkg-c@2.0.0 [js]
- pkg-d@2.0.0 [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
// pkg-c has a bump, which should cause pkg-d to bump because they are in a fixed group
// and pkg-a depends on pkg-c so should also bump, and pkg-b is in a fixed group with pkg-a so should also bump
if (projectName === 'pkg-c') return 'patch';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.0.1",
"dependencies": {
"pkg-c": "2.0.1"
}
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.0.1"
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "2.0.1"
}
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.0.1"
}
"
`);
});
});
describe('Two related groups, both independent relationship, just JS', () => {
const graphDefinition = `
group1 ({ "projectsRelationship": "independent" }):
- pkg-a@1.0.0 [js]
-> depends on pkg-c
- pkg-b@1.1.0 [js]
group2 ({ "projectsRelationship": "independent" }):
- pkg-c@2.0.0 [js]
- pkg-d@2.1.0 [js]
`;
it('should correctly version projects across group boundaries', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
graphDefinition,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
// pkg-a should still be bumped to 1.0.1 purely because of its dependency on pkg-c from the other group
if (projectName === 'pkg-a') return 'none';
// pkg-b should not be bumped because it is in an independent group and has no specifier of its own
if (projectName === 'pkg-b') return 'none';
// pkg-c should be bumped to 3.0.0 from its own specifier
if (projectName === 'pkg-c') return 'major';
// pkg-d should not be bumped because it is in an independent group and has no specifier of its own
if (projectName === 'pkg-d') return 'none';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.0.1",
"dependencies": {
"pkg-c": "3.0.0"
}
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "3.0.0"
}
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.1.0"
}
"
`);
});
it('should pick an appropriate overall version across group boundaries when a project is influenced by both a direct specifier and a dependency bump', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
graphDefinition,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
// pkg-a should be bumped to 1.1.0 because its dependency on pkg-c causing a patch is lower than its own specifier of minor
if (projectName === 'pkg-a') return 'minor';
// pkg-b should not be bumped because it is in an independent group and has no specifier of its own
if (projectName === 'pkg-b') return 'none';
// pkg-c should be bumped to 3.0.0 from its own specifier
if (projectName === 'pkg-c') return 'major';
// pkg-d should not be bumped because it is in an independent group and has no specifier of its own
if (projectName === 'pkg-d') return 'none';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0",
"dependencies": {
"pkg-c": "3.0.0"
}
}
"
`);
expect(tree.read('pkg-b/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-b",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-c/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-c",
"version": "3.0.0"
}
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.1.0"
}
"
`);
});
});
describe('Mixed JS and Rust projects within groups', () => {
describe('Two unrelated groups, both fixed relationship, mixed JS and Rust', () => {
it('should correctly version projects using mocked conventional commits', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "fixed" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.0.0 [rust]
group2 ({ "projectsRelationship": "fixed" }):
- pkg-c@2.0.0 [rust]
- pkg-d@2.0.0 [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
if (projectName === 'pkg-a') return 'minor';
if (projectName === 'pkg-b') return 'minor';
if (projectName === 'pkg-c') return 'patch';
if (projectName === 'pkg-d') return 'patch';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-b/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-b'
version = '1.1.0'
"
`);
expect(tree.read('pkg-c/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-c'
version = '2.0.1'
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.0.1"
}
"
`);
});
});
describe('Two unrelated groups, both independent relationship, mixed JS and Rust', () => {
it('should correctly version projects using mocked conventional commits', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "independent" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.1.0 [rust]
group2 ({ "projectsRelationship": "independent" }):
- pkg-c@2.0.0 [rust]
- pkg-d@2.1.0 [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
if (projectName === 'pkg-a') return 'minor';
if (projectName === 'pkg-b') return 'patch';
if (projectName === 'pkg-c') return 'major';
if (projectName === 'pkg-d') return 'none';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.1.0"
}
"
`);
expect(tree.read('pkg-b/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-b'
version = '1.1.1'
"
`);
expect(tree.read('pkg-c/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-c'
version = '3.0.0'
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.1.0"
}
"
`);
});
});
describe('Two related groups, both fixed relationship, mixed JS and Rust', () => {
it('should correctly version projects across group boundaries', async () => {
const { nxReleaseConfig, projectGraph, releaseGroups } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
group1 ({ "projectsRelationship": "fixed" }):
- pkg-a@1.0.0 [js]
- pkg-b@1.0.0 [rust]
-> depends on pkg-c
group2 ({ "projectsRelationship": "fixed" }):
- pkg-c@2.0.0 [rust]
- pkg-d@2.0.0 [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
// pkg-a should be bumped to 1.0.1 because it is in a fixed group with pkg-b and has no specifier of its own
if (projectName === 'pkg-a') return 'none';
// pkg-b should be bumped to 1.0.1 because it depends on pkg-c which is being bumped
if (projectName === 'pkg-b') return 'none';
// pkg-c should be bumped to 2.0.1 because of its specifier
if (projectName === 'pkg-c') return 'patch';
// pkg-d should be bumped to 2.0.1 because it is in a fixed group with pkg-c and has no specifier of its own
if (projectName === 'pkg-d') return 'none';
return 'none';
}
);
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
releaseGroups
);
await processor.buildGroupGraph();
await processor.processGroups();
// Called for each project
expect(mockResolveManifestActionsForProject).toHaveBeenCalledTimes(4);
expect(tree.read('pkg-a/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-a",
"version": "1.0.1"
}
"
`);
expect(tree.read('pkg-b/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-b'
version = '1.0.1'
[dependencies]
pkg-c = '2.0.1'
"
`);
expect(tree.read('pkg-c/Cargo.toml', 'utf-8')).toMatchInlineSnapshot(`
"
[package]
name = 'pkg-c'
version = '2.0.1'
"
`);
expect(tree.read('pkg-d/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "pkg-d",
"version": "2.0.1"
}
"
`);
});
});
});
});
@@ -0,0 +1,57 @@
import * as chalk from 'chalk';
import { output } from '../../../utils/output';
const colors = [
{ instance: chalk.green, spinnerColor: 'green' },
{ instance: chalk.greenBright, spinnerColor: 'green' },
{ instance: chalk.red, spinnerColor: 'red' },
{ instance: chalk.redBright, spinnerColor: 'red' },
{ instance: chalk.cyan, spinnerColor: 'cyan' },
{ instance: chalk.cyanBright, spinnerColor: 'cyan' },
{ instance: chalk.yellow, spinnerColor: 'yellow' },
{ instance: chalk.yellowBright, spinnerColor: 'yellow' },
{ instance: chalk.magenta, spinnerColor: 'magenta' },
{ instance: chalk.magentaBright, spinnerColor: 'magenta' },
] as const;
function getColor(projectName: string) {
let code = 0;
for (let i = 0; i < projectName.length; ++i) {
code += projectName.charCodeAt(i);
}
const colorIndex = code % colors.length;
return colors[colorIndex];
}
export class ProjectLogger {
private logs: string[] = [];
private color: (typeof colors)[number];
constructor(private projectName: string) {
this.color = getColor(projectName);
}
buffer(msg: string) {
this.logs.push(msg);
}
flush() {
// Don't slow down or add noise to unit tests output unnecessarily
if (
process.env.JEST_WORKER_ID !== undefined ||
process.env.NODE_ENV === 'test'
) {
return;
}
output.logSingleLine(
`Running release version for project: ${this.color.instance.bold(
this.projectName
)}`
);
this.logs.forEach((msg) => {
console.log(this.color.instance.bold(this.projectName) + ' ' + msg);
});
}
}
@@ -0,0 +1,931 @@
import { prerelease } from 'semver';
import { ProjectGraph } from '../../../config/project-graph';
import { Tree } from '../../../generators/tree';
import { output } from '../../../utils/output';
import {
IMPLICIT_DEFAULT_RELEASE_GROUP,
type NxReleaseConfig,
} from '../config/config';
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
import { getLatestGitTagForPattern } from '../utils/git';
import { resolveSemverSpecifierFromPrompt } from '../utils/resolve-semver-specifier';
import { deriveNewSemverVersion } from '../version';
import { deriveSpecifierFromConventionalCommits } from './derive-specifier-from-conventional-commits';
import {
deriveSpecifierFromVersionPlan,
ManifestActions,
resolveManifestActionsForProject,
SemverBumpType,
} from './flexible-version-management';
import { ProjectLogger } from './project-logger';
import { resolveCurrentVersion } from './resolve-current-version';
interface GroupNode {
group: ReleaseGroupWithName;
dependencies: Set<string>;
dependents: Set<string>;
}
// TODO: updateDependents of never is maybe not being intuitively. Currently the dep is bumped in the relevant manifest, it's just the version of the manifest that isn't.
// Does "never" really suggest it should be doing nothing at all to dependents? Probably... Maybe we need a third mode...
export interface VersionData {
currentVersion: string;
newVersion: string;
dependentProjects: {
source: string;
target: string;
type: string;
dependencyCollection: string;
rawVersionSpec: string;
}[];
}
// export type VersionData = Record<
// string,
// {
// /**
// * newVersion will be null in the case that no changes are detected for the project,
// * e.g. when using conventional commits
// */
// newVersion: string | null;
// currentVersion: string;
// /**
// * The list of projects which depend upon the current project.
// * TODO: investigate generic type for this once more ecosystems are explored
// */
// dependentProjects: any[];
// }
// >;
// TODO: figure out what would actually be useful here and how it should overlap with project-logger
function verboseLog(...msgs: any[]) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.log(...msgs);
}
}
export const validReleaseVersionPrefixes = ['auto', '', '~', '^', '='] as const;
// TODO: Can we make the implementation more efficient if we sort the projects from the projectGraph topologically upfront?
export class ReleaseGroupProcessor {
private groupGraph: Map<string, GroupNode> = new Map();
private processedGroups: Set<string> = new Set();
private bumpedProjects: Set<string> = new Set();
private projectsToManifestActions: Map<string, ManifestActions> = new Map();
private updateDependents: 'auto' | 'never';
private versionData: Map<string, VersionData> = new Map();
private projectsToProcess: Set<string>;
private allProjectsToProcess: Set<string>;
private userGivenSpecifier: string | undefined;
private cachedCurrentVersions: Map<string, string> = new Map();
private cachedLatestMatchingGitTag: Map<
string,
Awaited<ReturnType<typeof getLatestGitTagForPattern>>
> = new Map();
private projectLoggers: Map<string, ProjectLogger> = new Map();
constructor(
private tree: Tree,
private projectGraph: ProjectGraph,
private nxReleaseConfig: NxReleaseConfig,
private releaseGroups: ReleaseGroupWithName[],
userGivenSpecifier?: SemverBumpType,
projectsToProcess?: string[]
) {
// Strip any leading "v" from the user given specifier, if it exists
this.userGivenSpecifier = userGivenSpecifier?.replace(/^v/, '');
this.updateDependents =
(this.nxReleaseConfig.version?.generatorOptions?.updateDependents as
| 'auto'
| 'never') || 'auto';
this.projectsToProcess = new Set(
projectsToProcess || Object.keys(this.projectGraph.nodes)
);
this.allProjectsToProcess = new Set(this.projectsToProcess);
if (this.updateDependents === 'auto') {
const projectsToProcess = Array.from(this.projectsToProcess);
let dependents = this.getAllDependents(projectsToProcess);
while (dependents.length > 0) {
dependents.forEach((dep) => this.allProjectsToProcess.add(dep));
dependents = this.getAllDependents(dependents);
}
}
}
private getAllDependents(projects: string[]): string[] {
return projects
.flatMap((project) => this.getDependentsForProject(project))
.filter((dep) => !this.allProjectsToProcess.has(dep));
}
private getDependentsForProject(project: string): string[] {
return Object.entries(this.projectGraph.dependencies)
.filter(([_, deps]) => deps.some((dep) => dep.target === project))
.map(([projectName]) => projectName);
}
async buildGroupGraph(): Promise<void> {
try {
// Initialize group nodes
for (const group of this.releaseGroups) {
this.groupGraph.set(group.name, {
group,
dependencies: new Set(),
dependents: new Set(),
});
}
// Build dependencies and dependents between groups
for (const [releaseGroupName, releaseGroupNode] of this.groupGraph) {
for (const projectName of releaseGroupNode.group.projects) {
// Resolve the appropriate ManifestActions instance for the project
if (!this.projectsToManifestActions.has(projectName)) {
// TODO: Surely this validation should move to the manifest actions???
if (
releaseGroupNode.group.version?.generatorOptions?.versionPrefix &&
!validReleaseVersionPrefixes.includes(
releaseGroupNode.group.version?.generatorOptions
?.versionPrefix as any
)
) {
throw new Error(
`Invalid value for version.generatorOptions.versionPrefix: "${
releaseGroupNode.group.version?.generatorOptions
?.versionPrefix
}"
Valid values are: ${validReleaseVersionPrefixes
.map((s) => `"${s}"`)
.join(', ')}`
);
}
const projectGraphNode = this.projectGraph.nodes[projectName];
this.projectsToManifestActions.set(
projectName,
await resolveManifestActionsForProject(
this.tree,
releaseGroupNode.group,
projectGraphNode
)
);
// Create a reusable project logger to use for the full lifecycle of the versioning process
const projectLogger = new ProjectLogger(projectName);
this.projectLoggers.set(
projectName,
new ProjectLogger(projectName)
);
let latestMatchingGitTag:
| Awaited<ReturnType<typeof getLatestGitTagForPattern>>
| undefined;
// Cache the last matching git tag for relevant projects
if (
releaseGroupNode.group.version.generatorOptions
.currentVersionResolver === 'git-tag'
) {
latestMatchingGitTag = await getLatestGitTagForPattern(
releaseGroupNode.group.releaseTagPattern,
{
projectName: projectGraphNode.name,
}
);
this.cachedLatestMatchingGitTag.set(
projectName,
latestMatchingGitTag
);
}
// Cache the current version for the project
const currentVersion = await resolveCurrentVersion(
this.tree,
projectGraphNode,
releaseGroupNode.group,
// TODO: tmp workaround for type mismatch
this.projectsToManifestActions.get(projectName) as any,
projectLogger,
latestMatchingGitTag
);
this.cachedCurrentVersions.set(projectName, currentVersion);
}
// Process project dependencies
const projectDeps = this.projectGraph.dependencies[projectName] || [];
for (const dep of projectDeps) {
const dependencyGroup = this.findGroupForProject(dep.target);
if (dependencyGroup && dependencyGroup !== releaseGroupName) {
releaseGroupNode.dependencies.add(dependencyGroup);
this.groupGraph
.get(dependencyGroup)!
.dependents.add(releaseGroupName);
}
}
}
}
// Filter projects in each group based on projectsToProcess
for (const [groupName, groupNode] of this.groupGraph) {
groupNode.group.projects = groupNode.group.projects.filter((project) =>
this.projectsToProcess.has(project)
);
}
} catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
output.error({
title: e.message,
});
// Dump the full stack trace in verbose mode
console.error(e);
} else {
output.error({
title: e.message,
});
}
process.exit(1);
}
}
findGroupForProject(projectName: string): string | null {
for (const [releaseGroupName, releaseGroupNode] of this.groupGraph) {
if (releaseGroupNode.group.projects.includes(projectName)) {
return releaseGroupName;
}
}
return null;
}
getNextGroup(): string | null {
for (const [groupName, groupNode] of this.groupGraph) {
if (
!this.processedGroups.has(groupName) &&
Array.from(groupNode.dependencies).every((dep) =>
this.processedGroups.has(dep)
)
) {
return groupName;
}
}
return null;
}
async processGroups(): Promise<string[]> {
const processOrder: string[] = [];
let nextGroup: string | null;
while ((nextGroup = this.getNextGroup()) !== null) {
await this.processGroup(nextGroup);
this.processedGroups.add(nextGroup);
processOrder.push(nextGroup);
}
return processOrder;
}
private async processGroup(releaseGroupName: string): Promise<void> {
const groupNode = this.groupGraph.get(releaseGroupName)!;
const bumped = await this.bumpVersions(groupNode.group);
// Flush the project loggers for the group
for (const project of groupNode.group.projects) {
const projectLogger = this.projectLoggers.get(project);
if (projectLogger) {
projectLogger.flush();
}
}
if (bumped) {
await this.propagateChangesToDependentGroups(releaseGroupName);
}
}
private async propagateChangesToDependentGroups(
changedReleaseGroupName: string
): Promise<void> {
const changedGroupNode = this.groupGraph.get(changedReleaseGroupName)!;
for (const depGroupName of changedGroupNode.dependents) {
if (!this.processedGroups.has(depGroupName)) {
await this.propagateChanges(depGroupName, changedReleaseGroupName);
}
}
}
private async bumpVersions(
releaseGroup: ReleaseGroupWithName
): Promise<boolean> {
if (releaseGroup.projectsRelationship === 'fixed') {
return this.bumpFixedVersionGroup(releaseGroup);
} else {
return this.bumpIndependentVersionGroup(releaseGroup);
}
}
private async bumpFixedVersionGroup(
releaseGroup: ReleaseGroupWithName
): Promise<boolean> {
if (releaseGroup.projects.length === 0) {
return false;
}
let bumped = false;
const firstProject = releaseGroup.projects[0];
const bumpType = await this.determineVersionBumpForProject(
releaseGroup,
firstProject
);
if (bumpType === 'none') {
// No direct bump for this group, but we may still need to bump if a dependency group has been bumped
let bumpedByDependency = false;
// Iterate through each project in the release group
for (const project of releaseGroup.projects) {
const dependencies = this.projectGraph.dependencies[project] || [];
for (const dep of dependencies) {
const depGroup = this.findGroupForProject(dep.target);
if (
depGroup &&
depGroup !== releaseGroup.name &&
this.processedGroups.has(depGroup)
) {
const depGroupBumpType = await this.getGroupBumpType(depGroup);
// If a dependency group has been bumped, determine if it should trigger a bump in this group
if (depGroupBumpType !== 'none') {
bumpedByDependency = true;
verboseLog(
`Bumping project ${project} due to dependency group ${depGroup} being bumped.`
);
const depBumpType = this.determineSideEffectBump(
releaseGroup,
depGroupBumpType
);
await this.bumpVersionForProject(project, depBumpType);
this.bumpedProjects.add(project);
// Update any dependencies in the manifest
await this.updateDependenciesForProject(project);
}
}
}
}
// If any project in the group was bumped due to dependency changes, we must bump all projects in the fixed group
if (bumpedByDependency) {
for (const project of releaseGroup.projects) {
if (!this.bumpedProjects.has(project)) {
verboseLog(
`Bumping project ${project} because it is part of the fixed group.`
);
await this.bumpVersionForProject(project, 'patch'); // Ensure the bump for remaining projects
this.bumpedProjects.add(project);
await this.updateDependenciesForProject(project);
}
}
}
return bumpedByDependency;
}
const newVersion = await this.calculateNewVersion(firstProject, bumpType);
// First, update versions for all projects in the fixed group
for (const project of releaseGroup.projects) {
const manifestActions = this.projectsToManifestActions.get(project);
if (!manifestActions) {
console.warn(`No manifest actions found for project ${project}`);
continue;
}
try {
const currentVersion = this.cachedCurrentVersions.get(project);
if (!currentVersion) {
throw new Error(
`Unexpected error: No cached current version found for project ${project}, please report this as a bug`
);
}
await this.setProjectVersion(project, newVersion);
this.bumpedProjects.add(project);
bumped = true;
// Populate version data for each project
this.versionData.set(project, {
currentVersion,
newVersion,
dependentProjects: await this.getDependentProjects(project),
});
} catch (error) {
console.error(`Error processing project ${project}:`, error);
}
}
// Then, update dependencies for all projects in the fixed group
if (bumped) {
for (const project of releaseGroup.projects) {
await this.updateDependenciesForProject(project);
}
}
return bumped;
}
private async bumpIndependentVersionGroup(
releaseGroup: ReleaseGroupWithName
): Promise<boolean> {
verboseLog('Starting bumpIndependentVersionGroup');
verboseLog('Projects in release group:', releaseGroup.projects);
verboseLog(
'All projects to process:',
Array.from(this.allProjectsToProcess)
);
verboseLog('updateDependents setting:', this.updateDependents);
let bumped = false;
const projectBumpTypes = new Map<string, SemverBumpType>();
const projectsToUpdate = new Set<string>();
// First pass: Determine bump types
for (const project of this.allProjectsToProcess) {
const bumpType = await this.determineVersionBumpForProject(
releaseGroup,
project
);
verboseLog(`First pass - Project: ${project}, Bump type: ${bumpType}`);
projectBumpTypes.set(project, bumpType);
if (bumpType !== 'none') {
projectsToUpdate.add(project);
}
}
verboseLog(
'After first pass - Projects to update:',
Array.from(projectsToUpdate)
);
// Second pass: Update versions
for (const project of releaseGroup.projects) {
if (projectsToUpdate.has(project)) {
const finalBumpType = projectBumpTypes.get(project)!;
verboseLog(
`Second pass - Project: ${project}, Final bump type: ${finalBumpType}`
);
if (finalBumpType !== 'none') {
await this.bumpVersionForProject(project, finalBumpType);
this.bumpedProjects.add(project);
bumped = true;
}
}
}
// Third pass: Update dependencies
for (const project of releaseGroup.projects) {
if (projectsToUpdate.has(project)) {
await this.updateDependenciesForProject(project);
}
}
verboseLog('Bumped projects:', Array.from(this.bumpedProjects));
return bumped;
}
private async determineVersionBumpForProject(
releaseGroup: ReleaseGroupWithName,
projectName: string
): Promise<SemverBumpType> {
// User given specifier has the highest precedence
if (this.userGivenSpecifier) {
return this.userGivenSpecifier as SemverBumpType;
}
// If the release group has a conventional commits config, use that
if (releaseGroup.version.conventionalCommits) {
const currentVersion = this.cachedCurrentVersions.get(projectName);
if (!currentVersion) {
throw new Error(
`Unexpected error: No cached current version found for project ${projectName}, please report this as a bug`
);
}
const projectLogger = this.projectLoggers.get(projectName);
if (!projectLogger) {
throw new Error(
`Unexpected error: No project logger found for project ${projectName}, please report this as a bug`
);
}
// TODO: Support passing args.preid through
const preid = undefined;
return deriveSpecifierFromConventionalCommits(
this.nxReleaseConfig,
this.projectGraph,
projectLogger,
releaseGroup,
this.projectGraph.nodes[projectName],
!!prerelease(currentVersion ?? ''),
this.cachedLatestMatchingGitTag.get(projectName),
preid
);
}
// If the release group uses version plan, resolve that way
if (releaseGroup.versionPlans) {
return deriveSpecifierFromVersionPlan(projectName);
}
// Only add the release group name to the log if it is one set by the user, otherwise it is useless noise
const maybeLogReleaseGroup = (log: string): string => {
if (releaseGroup.name === IMPLICIT_DEFAULT_RELEASE_GROUP) {
return log;
}
return `${log} within release group "${releaseGroup.name}"`;
};
if (releaseGroup.version.generatorOptions?.specifierSource === 'prompt') {
let specifier: SemverBumpType;
if (releaseGroup.projectsRelationship === 'independent') {
specifier = (await resolveSemverSpecifierFromPrompt(
`${maybeLogReleaseGroup(
`What kind of change is this for project "${projectName}"`
)}?`,
`${maybeLogReleaseGroup(
`What is the exact version for project "${projectName}"`
)}?`
// TODO: address type discrepancy
)) as SemverBumpType;
} else {
specifier = (await resolveSemverSpecifierFromPrompt(
`${maybeLogReleaseGroup(
`What kind of change is this for the ${releaseGroup.projects.length} matched projects(s)`
)}?`,
`${maybeLogReleaseGroup(
`What is the exact version for the ${releaseGroup.projects.length} matched project(s)`
)}?`
// TODO: address type discrepancy
)) as SemverBumpType;
}
return specifier;
}
throw new Error(`Unhandled version bump config`);
}
private async calculateNewVersion(
project: string,
bumpType: SemverBumpType
): Promise<string> {
const currentVersion = this.cachedCurrentVersions.get(project);
if (!currentVersion) {
throw new Error(
`Unexpected error: No cached current version found for project ${project}, please report this as a bug`
);
}
return deriveNewSemverVersion(currentVersion, bumpType);
}
private async setProjectVersion(
project: string,
newVersion: string
): Promise<void> {
const manifestActions = this.projectsToManifestActions.get(project);
if (!manifestActions) {
throw new Error(`No manifest actions found for project ${project}`);
}
await manifestActions.writeVersionToManifest(this.tree, newVersion);
}
// TODO: set valid values for versionPrefix somewhere
// export const validReleaseVersionPrefixes = ['auto', '', '~', '^', '='] as const;
// TODO: should prefix handling actually move to the manifest actions? Some prefixes are likely ecosystem specific
private async updateDependenciesForProject(
projectName: string
): Promise<void> {
verboseLog(`Updating dependencies for project: ${projectName}`);
if (!this.allProjectsToProcess.has(projectName)) {
verboseLog(`Skipping ${projectName} as it's not in allProjectsToProcess`);
return;
}
const manifestActions = this.projectsToManifestActions.get(projectName);
if (!manifestActions) {
console.error(`No manifest actions found for project ${projectName}`);
return;
}
const dependenciesToUpdate: Record<string, string> = {};
const dependencies = this.projectGraph.dependencies[projectName] || [];
const manifestData = await manifestActions.getInitialManifestData(
this.tree
);
// Get the versionPrefix from generatorOptions, defaulting to 'auto'
// TODO: Make this config come from the release group instead, and set it on the manifest actions when instantiating it
const versionPrefix = (this.nxReleaseConfig.version?.generatorOptions
?.versionPrefix ?? 'auto') as 'auto' | '~' | '^' | '=';
verboseLog(`Version prefix: ${versionPrefix}`);
for (const dep of dependencies) {
if (
this.allProjectsToProcess.has(dep.target) &&
this.bumpedProjects.has(dep.target)
) {
const targetVersionData = this.versionData.get(dep.target);
if (targetVersionData) {
const currentDependencyVersion =
manifestData.dependencies['dependencies']?.[dep.target] ||
manifestData.dependencies['devDependencies']?.[dep.target] ||
manifestData.dependencies['peerDependencies']?.[dep.target];
verboseLog(`Current dependency version: ${currentDependencyVersion}`);
let finalPrefix = '';
if (versionPrefix === 'auto') {
const prefixMatch = currentDependencyVersion?.match(/^([~^=])/);
finalPrefix = prefixMatch ? prefixMatch[1] : '';
} else if (['~', '^', '='].includes(versionPrefix)) {
finalPrefix = versionPrefix;
}
verboseLog(`Final prefix: ${finalPrefix}`);
// Remove any existing prefix from the new version before applying the finalPrefix
const cleanNewVersion = targetVersionData.newVersion.replace(
/^[~^=]/,
''
);
dependenciesToUpdate[dep.target] = `${finalPrefix}${cleanNewVersion}`;
verboseLog(
`Updating dependency ${dep.target} to version ${
dependenciesToUpdate[dep.target]
}`
);
}
}
}
if (Object.keys(dependenciesToUpdate).length > 0) {
verboseLog(
`Updating dependencies for ${projectName}:`,
dependenciesToUpdate
);
await manifestActions.updateDependencies(this.tree, dependenciesToUpdate);
} else {
verboseLog(`No dependencies to update for ${projectName}`);
}
}
private async bumpVersionForProject(
projectName: string,
bumpType: SemverBumpType
): Promise<void> {
verboseLog(
`Bumping version for project: ${projectName}, bump type: ${bumpType}`
);
if (bumpType === 'none') {
verboseLog(`Skipping bump for ${projectName} as bump type is 'none'`);
return;
}
const manifestActions = this.projectsToManifestActions.get(projectName);
if (!manifestActions) {
throw new Error(`No manifest actions found for project ${projectName}`);
}
// Use the cached current version
const currentVersion = this.cachedCurrentVersions.get(projectName);
if (!currentVersion) {
throw new Error(
`Unexpected error: No cached current version found for project ${projectName}, please report this as a bug`
);
}
// Log the current and new version
verboseLog(`Current version for ${projectName}: ${currentVersion}`);
const newVersion = deriveNewSemverVersion(currentVersion, bumpType);
verboseLog(`New version for ${projectName}: ${newVersion}`);
// Log manifest writing step
verboseLog(`Writing new version to manifest for project: ${projectName}`);
await manifestActions.writeVersionToManifest(this.tree, newVersion);
// Update version data
verboseLog(`Updating version data for project: ${projectName}`);
this.versionData.set(projectName, {
currentVersion,
newVersion,
dependentProjects: await this.getDependentProjects(projectName),
});
// Log project bump completion
this.bumpedProjects.add(projectName);
verboseLog(`Project ${projectName} successfully bumped to ${newVersion}`);
// Only update dependencies if updateDependents is 'auto'
if (this.updateDependents === 'auto') {
verboseLog(`Updating dependencies for dependents of ${projectName}`);
await this.updateDependenciesForDependents(projectName);
const dependents = this.getDependentsForProject(projectName);
verboseLog(`Dependents of ${projectName}: ${dependents}`);
for (const dependent of dependents) {
if (
this.allProjectsToProcess.has(dependent) &&
!this.bumpedProjects.has(dependent)
) {
verboseLog(`Bumping dependent project: ${dependent}`);
await this.bumpVersionForProject(dependent, 'patch');
}
}
} else {
verboseLog(
`Skipping dependent updates for ${projectName} as updateDependents is not 'auto'`
);
}
}
private async updateDependenciesForDependents(
updatedProject: string
): Promise<void> {
verboseLog(`Updating dependencies for dependents of ${updatedProject}`);
const dependents = this.getDependentsForProject(updatedProject);
verboseLog(`Dependents of ${updatedProject}:`, dependents);
for (const dependent of dependents) {
verboseLog(`Checking dependent: ${dependent}`);
if (this.allProjectsToProcess.has(dependent)) {
verboseLog(`Updating dependencies for ${dependent}`);
await this.updateDependenciesForProject(dependent);
} else {
verboseLog(`Skipping ${dependent} as it's not in allProjectsToProcess`);
}
}
}
public getVersionData(): Record<string, VersionData> {
return Object.fromEntries(this.versionData);
}
private async getDependentProjects(
project: string
): Promise<VersionData['dependentProjects']> {
const dependents: VersionData['dependentProjects'] = [];
const dependencies = this.projectGraph.dependencies || {};
for (const [source, deps] of Object.entries(dependencies)) {
if (!Array.isArray(deps)) continue;
for (const dep of deps) {
if (dep && dep.target === project) {
const manifestActions = this.projectsToManifestActions.get(source);
if (manifestActions) {
try {
const manifestData = await manifestActions.getInitialManifestData(
this.tree
);
if (manifestData && manifestData.dependencies) {
const dependencyCollection =
Object.keys(manifestData.dependencies).find(
(key) =>
manifestData.dependencies[key] &&
manifestData.dependencies[key][project]
) || 'dependencies';
const rawVersionSpec =
manifestData.dependencies[dependencyCollection]?.[project] ||
'';
dependents.push({
source,
target: project,
type: 'static',
dependencyCollection,
rawVersionSpec,
});
}
} catch (error) {
console.error(
`Error processing manifest for project ${source}:`,
error
);
}
}
}
}
}
return dependents;
}
async propagateChanges(
releaseGroupName: string,
changedDependencyGroup: string
): Promise<void> {
const releaseGroup = this.groupGraph.get(releaseGroupName)!.group;
let groupBumped = false;
let bumpType: SemverBumpType = 'none';
verboseLog(`Processing group: ${releaseGroupName}`);
verboseLog(
`Checking if group depends on changed group: ${changedDependencyGroup}`
);
if (releaseGroup.projectsRelationship === 'fixed') {
// For fixed groups, we only need to check one project
const project = releaseGroup.projects[0];
const dependencies = this.projectGraph.dependencies[project] || [];
const hasDependencyInChangedGroup = dependencies.some(
(dep) => this.findGroupForProject(dep.target) === changedDependencyGroup
);
verboseLog(`Fixed group dependencies: ${JSON.stringify(dependencies)}`);
if (hasDependencyInChangedGroup) {
const dependencyBumpType = await this.getGroupBumpType(
changedDependencyGroup
);
verboseLog(
`Dependency bump type for group ${changedDependencyGroup}: ${dependencyBumpType}`
);
bumpType = this.determineSideEffectBump(
releaseGroup,
dependencyBumpType
);
groupBumped = bumpType !== 'none';
}
} else {
// For independent groups, we need to check each project individually
for (const project of releaseGroup.projects) {
const dependencies = this.projectGraph.dependencies[project] || [];
const hasDependencyInChangedGroup = dependencies.some(
(dep) =>
this.findGroupForProject(dep.target) === changedDependencyGroup
);
verboseLog(
`Project ${project} dependencies: ${JSON.stringify(dependencies)}`
);
if (hasDependencyInChangedGroup) {
const dependencyBumpType = await this.getGroupBumpType(
changedDependencyGroup
);
verboseLog(
`Dependency bump type for project ${project}: ${dependencyBumpType}`
);
const projectBumpType = this.determineSideEffectBump(
releaseGroup,
dependencyBumpType
);
if (projectBumpType !== 'none') {
groupBumped = true;
if (!this.bumpedProjects.has(project)) {
await this.bumpVersionForProject(project, projectBumpType);
this.bumpedProjects.add(project);
verboseLog(`Bumped version for project ${project}`);
}
}
}
}
}
if (groupBumped) {
verboseLog(
`Group ${releaseGroupName} was bumped due to dependencies in group ${changedDependencyGroup}`
);
for (const project of releaseGroup.projects) {
if (!this.bumpedProjects.has(project)) {
await this.bumpVersionForProject(project, bumpType);
this.bumpedProjects.add(project);
}
}
}
}
private async getGroupBumpType(
releaseGroupName: string
): Promise<SemverBumpType> {
const releaseGroup = this.groupGraph.get(releaseGroupName)!.group;
return await this.determineVersionBumpForProject(
releaseGroup,
releaseGroup.projects[0]
);
}
private determineSideEffectBump(
releaseGroup: ReleaseGroupWithName,
dependencyBumpType: SemverBumpType
): SemverBumpType {
// TODO: figure out sideEffectBump
const sideEffectBump =
releaseGroup.version.generatorOptions?.sideEffectBump || 'patch';
if (sideEffectBump === 'same-as-dependency') {
return dependencyBumpType;
}
return sideEffectBump as SemverBumpType;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
import { createTreeWithEmptyWorkspace } from '../../../generators/testing-utils/create-tree-with-empty-workspace';
import type { Tree } from '../../../generators/tree';
import { ManifestActions } from './flexible-version-management';
import { ProjectLogger } from './project-logger';
import { resolveCurrentVersion } from './resolve-current-version';
describe('resolveCurrentVersion', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
describe('disk', () => {
class TestManifestActions extends ManifestActions {
getPrimaryManifestPath() {
return 'path/to/package.json';
}
async resolveCurrentVersion() {
return '1.2.3';
}
async ensureManifestExistsAtExpectedLocation() {
return;
}
async readManifestData() {
return {
name: 'test',
currentVersion: '1.2.3',
dependencies: {},
};
}
async writeVersionToManifest() {
return;
}
async updateDependencies() {
return;
}
}
class TestProjectLogger extends ProjectLogger {
constructor(projectName: string) {
super(projectName);
}
override buffer(message: string) {}
}
it('should resolve the current version from disk based on the provided manifestAction instance, when currentVersionResolver is set to disk on the releaseGroup and nothing is set on the project node', async () => {
const projectGraphNode: ProjectGraphProjectNode = {
name: 'test',
type: 'lib' as const,
data: {
root: tree.root,
},
// No release config, should use the releaseGroup config
};
const releaseGroup = {
name: 'release-group',
version: {
generatorOptions: {
currentVersionResolver: 'disk',
},
},
};
const currentVersion = await resolveCurrentVersion(
tree,
projectGraphNode,
// TODO: Fix types
releaseGroup as any,
// TODO: Fix types
new TestManifestActions(projectGraphNode) as any,
new TestProjectLogger(projectGraphNode.name)
);
expect(currentVersion).toBe('1.2.3');
});
it('should resolve the current version from disk based on the provided manifestAction instance, when currentVersionResolver is set to disk on the project node, regardless of what is set on the releaseGroup', async () => {
const projectGraphNode: ProjectGraphProjectNode = {
name: 'test',
type: 'lib' as const,
data: {
root: tree.root,
release: {
version: {
generatorOptions: {
currentVersionResolver: 'disk',
},
},
},
},
};
const releaseGroup = {
name: 'release-group',
version: {
generatorOptions: {
// Should be ignored in favor of the project node
currentVersionResolver: 'SOMETHING_ELSE',
},
},
};
const currentVersion = await resolveCurrentVersion(
tree,
projectGraphNode,
// TODO: Fix types
releaseGroup as any,
// TODO: Fix types
new TestManifestActions(projectGraphNode) as any,
new TestProjectLogger(projectGraphNode.name)
);
expect(currentVersion).toBe('1.2.3');
});
});
});
@@ -0,0 +1,118 @@
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
import type { Tree } from '../../../generators/tree';
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
import { getLatestGitTagForPattern } from '../utils/git';
import { ReleaseVersionGeneratorSchema } from '../version';
import { ManifestActions } from './flexible-version-management';
import { ProjectLogger } from './project-logger';
export async function resolveCurrentVersion(
tree: Tree,
projectGraphNode: ProjectGraphProjectNode,
releaseGroup: ReleaseGroupWithName,
manifestActions: ManifestActions,
logger: ProjectLogger,
latestMatchingGitTag?: Awaited<ReturnType<typeof getLatestGitTagForPattern>>
): Promise<string> {
/**
* The currentVersionResolver config can come from (in order of priority):
* 1. An optional project level release config
* 2. The project's releaseGroup (which will have been set appropriately by the nx release config handler based on the global config)
*/
let currentVersionResolver:
| ReleaseVersionGeneratorSchema['currentVersionResolver']
| undefined;
let currentVersionResolverMetadata:
| ReleaseVersionGeneratorSchema['currentVersionResolverMetadata']
| undefined;
if (
projectGraphNode.data.release?.version?.generatorOptions
?.currentVersionResolver
) {
currentVersionResolver = projectGraphNode.data.release.version
.generatorOptions
.currentVersionResolver as ReleaseVersionGeneratorSchema['currentVersionResolver'];
currentVersionResolverMetadata =
(projectGraphNode.data.release.version.generatorOptions
.currentVersionResolverMetadata as ReleaseVersionGeneratorSchema['currentVersionResolverMetadata']) ??
{};
} else {
currentVersionResolver = releaseGroup.version.generatorOptions
.currentVersionResolver as ReleaseVersionGeneratorSchema['currentVersionResolver'];
currentVersionResolverMetadata =
(releaseGroup.version.generatorOptions
.currentVersionResolverMetadata as ReleaseVersionGeneratorSchema['currentVersionResolverMetadata']) ??
{};
}
// TODO: Remove the temp fallback once we have moved currentVersionResolver to a direct property of the release group
if (!currentVersionResolver) {
currentVersionResolver = 'disk';
}
switch (currentVersionResolver) {
// TODO: Implement registry resolver
case 'registry': {
return resolveCurrentVersionFromDisk(tree, manifestActions, logger);
}
case 'disk': {
return resolveCurrentVersionFromDisk(tree, manifestActions, logger);
}
case 'git-tag': {
return resolveCurrentVersionFromGitTag(
tree,
projectGraphNode,
releaseGroup,
manifestActions,
logger,
latestMatchingGitTag
);
}
default:
throw new Error(
`Invalid value for "currentVersionResolver": ${currentVersionResolver}`
);
}
}
export async function resolveCurrentVersionFromDisk(
tree: Tree,
manifestActions: ManifestActions,
logger: ProjectLogger
): Promise<string> {
const currentVersion = await manifestActions.resolveCurrentVersion(tree);
const manifestPath = manifestActions.getPrimaryManifestPath();
logger.buffer(
`📄 Resolved the current version as ${currentVersion} from manifest: ${manifestPath}`
);
return currentVersion;
}
export async function resolveCurrentVersionFromGitTag(
tree: Tree,
projectGraphNode: ProjectGraphProjectNode,
releaseGroup: ReleaseGroupWithName,
manifestActions: ManifestActions,
logger: ProjectLogger,
latestMatchingGitTag?: Awaited<ReturnType<typeof getLatestGitTagForPattern>>
): Promise<string> {
if (!latestMatchingGitTag) {
if (
releaseGroup.version.generatorOptions.fallbackCurrentVersionResolver ===
'disk'
) {
return resolveCurrentVersionFromDisk(tree, manifestActions, logger);
} else {
throw new Error(
`No git tags matching pattern "${releaseGroup.releaseTagPattern}" for project "${projectGraphNode.name}" were found. You will need to create an initial matching tag to use as a base for determining the next version. Alternatively, you can use the --first-release option or set "release.version.generatorOptions.fallbackCurrentVersionResolver" to "disk" in order to fallback to the version on disk when no matching git tags are found.`
);
}
}
const currentVersion = latestMatchingGitTag.extractedVersion;
logger.buffer(
`📄 Resolved the current version as ${currentVersion} from git tag "${latestMatchingGitTag.tag}".`
);
return currentVersion;
}
@@ -0,0 +1,385 @@
import TOML from '@ltd/j-toml';
import { join } from 'node:path';
import type { NxJsonConfiguration } from '../../../config/nx-json';
import type { ProjectGraph } from '../../../config/project-graph';
import type { Tree } from '../../../generators/tree';
import { writeJson } from '../../../generators/utils/json';
import { createProjectFileMapUsingProjectGraph } from '../../../project-graph/file-map-utils';
import { createNxReleaseConfig, NxReleaseConfig } from '../config/config';
import { filterReleaseGroups } from '../config/filter-release-groups';
import { ManifestActions, ManifestData } from './flexible-version-management';
export async function createNxReleaseConfigAndPopulateWorkspace(
tree: Tree,
graphDefinition: string,
additionalNxReleaseConfig: Exclude<NxJsonConfiguration['release'], 'groups'>,
mockResolveCurrentVersion?: any
) {
const graph = parseGraphDefinition(graphDefinition);
const { groups, projectGraph } = setupGraph(tree, graph);
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
projectGraph,
await createProjectFileMapUsingProjectGraph(projectGraph),
{
...additionalNxReleaseConfig,
groups,
}
);
if (configError) {
throw configError;
}
let {
error: filterError,
releaseGroups,
// TODO: Leverage this?
// releaseGroupToFilteredProjects,
} = filterReleaseGroups(projectGraph, nxReleaseConfig!);
if (filterError) {
throw filterError;
}
// Mock the implementation of resolveCurrentVersion to reliably return the version of the project based on our graph definition
mockResolveCurrentVersion?.mockImplementation((_, { name }) => {
for (const [projectName, project] of Object.entries(graph.projects)) {
if (projectName === name) {
return (project as any).version;
}
}
throw new Error(`Unknown project name in test utils: ${name}`);
});
return {
nxReleaseConfig: nxReleaseConfig!,
releaseGroups,
projectGraph,
};
}
/**
* A non-production grade rust implementation to prove out loading multiple different manifestActions in various setups
*/
interface CargoToml {
workspace?: { members: string[] };
package: { name: string; version: string };
dependencies?: Record<
string,
string | { version: string; features?: string[]; optional?: boolean }
>;
'dev-dependencies'?: Record<
string,
string | { version: string; features: string[] }
>;
[key: string]: any;
}
export class ExampleRustManifestActions extends ManifestActions {
getPrimaryManifestPath(): string {
return join(this.projectGraphNode.data.root, 'Cargo.toml');
}
async ensureManifestExistsAtExpectedLocation(tree: Tree) {}
private parseCargoToml(cargoString: string): CargoToml {
return TOML.parse(cargoString, {
x: { comment: true },
}) as CargoToml;
}
static stringifyCargoToml(cargoToml: CargoToml): string {
const tomlString = TOML.stringify(cargoToml, {
newlineAround: 'section',
});
return Array.isArray(tomlString) ? tomlString.join('\n') : tomlString;
}
static modifyCargoTable(
toml: CargoToml,
section: string,
key: string,
value: string | object | Array<any> | (() => any)
) {
toml[section] ??= TOML.Section({});
toml[section][key] =
typeof value === 'object' && !Array.isArray(value)
? TOML.inline(value as any)
: typeof value === 'function'
? value()
: value;
}
async readManifestData(tree: Tree): Promise<ManifestData> {
const cargoTomlPath = join(this.projectGraphNode.data.root, 'Cargo.toml');
const cargoTomlString = tree.read(cargoTomlPath, 'utf-8')!.toString();
const cargoToml = this.parseCargoToml(cargoTomlString);
const currentVersion = cargoToml.package?.version || '0.0.0';
const name = cargoToml.package?.name || 'unknown';
const dependencies: Record<string, Record<string, string>> = {
dependencies: {},
};
if (cargoToml.dependencies) {
for (const [dep, version] of Object.entries(cargoToml.dependencies)) {
dependencies.dependencies[dep] =
typeof version === 'string' ? version : version.version;
}
}
return {
name,
currentVersion,
dependencies,
};
}
async resolveCurrentVersion(tree: Tree): Promise<string> {
return (await this.readManifestData(tree)).currentVersion;
}
async writeVersionToManifest(tree: Tree, newVersion: string) {
const cargoTomlPath = join(this.projectGraphNode.data.root, 'Cargo.toml');
const cargoTomlString = tree.read(cargoTomlPath, 'utf-8')!.toString();
const cargoToml = this.parseCargoToml(cargoTomlString);
ExampleRustManifestActions.modifyCargoTable(
cargoToml,
'package',
'version',
newVersion
);
const updatedCargoTomlString =
ExampleRustManifestActions.stringifyCargoToml(cargoToml);
tree.write(cargoTomlPath, updatedCargoTomlString);
}
async updateDependencies(
tree: Tree,
dependenciesToUpdate: Record<string, string>
) {
const cargoTomlPath = join(this.projectGraphNode.data.root, 'Cargo.toml');
const cargoTomlString = tree.read(cargoTomlPath, 'utf-8')!.toString();
const cargoToml = this.parseCargoToml(cargoTomlString);
for (const [dep, version] of Object.entries(dependenciesToUpdate)) {
ExampleRustManifestActions.modifyCargoTable(
cargoToml,
'dependencies',
dep,
version
);
}
const updatedCargoTomlString =
ExampleRustManifestActions.stringifyCargoToml(cargoToml);
tree.write(cargoTomlPath, updatedCargoTomlString);
}
}
function parseGraphDefinition(definition: string) {
const graph = { projects: {} as any };
const lines = definition.trim().split('\n');
let currentGroup = '';
let groupConfig = {};
let groupRelationship = '';
let lastProjectName = '';
lines.forEach((line) => {
line = line.trim();
if (!line) {
// Skip empty lines
return;
}
// Match group definitions with JSON config
const groupMatch = line.match(/^(\w+)\s*\(\s*(\{.*?\})\s*\):$/);
if (groupMatch) {
currentGroup = groupMatch[1];
groupConfig = JSON.parse(groupMatch[2]);
groupRelationship = groupConfig['projectsRelationship'] || 'independent';
return;
}
// Match project definitions with optional per-project JSON config
const projectMatch = line.match(
/^- ([\w-]+)@([\d\.]+) \[(\w+)\](?:\s*\(\s*(\{.*?\})\s*\))?$/
);
if (projectMatch) {
const [_, name, version, language, configJson] = projectMatch;
// Automatically add data for Rust projects
let projectData = {};
if (language === 'rust') {
projectData = {
release: { manifestActions: '__EXAMPLE_RUST_MANIFEST_ACTIONS__' },
};
}
// Merge explicit per-project config if present
if (configJson) {
const explicitConfig = JSON.parse(configJson);
projectData = { ...projectData, ...explicitConfig };
}
graph.projects[name] = {
version,
language,
group: currentGroup,
relationship: groupRelationship,
dependsOn: [],
data: projectData,
};
lastProjectName = name;
return;
}
// Match dependencies
const dependsMatch = line.match(
/^-> depends on ([~^=]?)([\w-]+)(?:\s*\{(\w+)\})?$/
);
if (dependsMatch) {
const [_, prefix, depProject, depCollection = 'dependencies'] =
dependsMatch;
// Add the dependency to the last added project
if (!graph.projects[lastProjectName].dependsOn) {
graph.projects[lastProjectName].dependsOn = [];
}
graph.projects[lastProjectName].dependsOn.push({
project: depProject,
collection: depCollection,
prefix: prefix || '', // Store the prefix (empty string if not specified)
});
return;
}
// Ignore unrecognized lines
});
return graph;
}
function setupGraph(tree: any, graph: any) {
const groups: NxReleaseConfig['groups'] = {};
const projectGraph: ProjectGraph = { nodes: {}, dependencies: {} };
for (const [projectName, projectData] of Object.entries(graph.projects)) {
const { version, language, group, relationship, dependsOn, data } =
projectData as any;
// Write project files based on language
if (language === 'js') {
const packageJson: any = { name: projectName, version };
if (dependsOn) {
dependsOn.forEach(
(dep: { project: string; collection: string; prefix: string }) => {
if (!packageJson[dep.collection]) {
packageJson[dep.collection] = {};
}
const depVersion = graph.projects[dep.project].version;
packageJson[dep.collection][
dep.project
] = `${dep.prefix}${depVersion}`;
}
);
}
writeJson(tree, `${projectName}/package.json`, packageJson);
} else if (language === 'rust') {
const cargoToml: CargoToml = {} as any;
ExampleRustManifestActions.modifyCargoTable(
cargoToml,
'package',
'name',
projectName
);
ExampleRustManifestActions.modifyCargoTable(
cargoToml,
'package',
'version',
version
);
if (dependsOn) {
dependsOn.forEach(
(dep: { project: string; collection: string; prefix: string }) => {
ExampleRustManifestActions.modifyCargoTable(
cargoToml,
dep.collection,
dep.project,
{
version: graph.projects[dep.project].version,
}
);
}
);
}
tree.write(
`${projectName}/Cargo.toml`,
ExampleRustManifestActions.stringifyCargoToml(cargoToml)
);
}
// Add to projectGraph nodes
projectGraph.nodes[projectName] = {
name: projectName,
type: 'lib',
data: {
root: projectName,
...data, // Merge any additional data from project config
},
};
// Initialize dependencies
projectGraph.dependencies[projectName] = [];
// Handle dependencies
if (dependsOn) {
dependsOn.forEach((dep: { project: string; collection: string }) => {
projectGraph.dependencies[projectName].push({
source: projectName,
target: dep.project,
type: 'static',
});
});
}
// Add to releaseGroups
if (!groups[group]) {
groups[group] = {
projectsRelationship: relationship,
projects: [],
// TODO: add missing properties here and remove as any
} as any;
}
groups[group].projects.push(projectName);
}
return { groups, projectGraph };
}
export async function mockResolveManifestActionsForProjectImplementation(
tree: Tree,
releaseGroup: any,
projectGraphNode: any
) {
const exampleRustManifestActions = '__EXAMPLE_RUST_MANIFEST_ACTIONS__';
if (
projectGraphNode.data.release?.manifestActions ===
exampleRustManifestActions ||
releaseGroup.manifestActions === exampleRustManifestActions
) {
const manifestActions = new ExampleRustManifestActions(projectGraphNode);
await manifestActions.ensureManifestExistsAtExpectedLocation(tree);
return manifestActions;
}
// @ts-ignore
const JsManifestActions = jest.requireActual(
// nx-ignore-next-line
'@nx/js/src/generators/release-version/manifest-actions'
).default;
const manifestActions = new JsManifestActions(projectGraphNode);
await manifestActions.ensureManifestExistsAtExpectedLocation(tree);
return manifestActions;
}
+334 -260
View File
@@ -54,6 +54,9 @@ import {
} from './utils/shared';
import { handleErrors } from '../../utils/handle-errors';
import { ReleaseGroupProcessor } from './version-utils/release-group-processor';
import { SemverBumpType } from './version-utils/flexible-version-management';
const LARGE_BUFFER = 1024 * 1000000;
// Reexport some utils for use in plugin release-version generator implementations
@@ -66,10 +69,16 @@ export type {
export const validReleaseVersionPrefixes = ['auto', '', '~', '^', '='] as const;
export interface ReleaseVersionGeneratorSchema {
// start: internal props
// The projects being versioned in the current execution
projects: ProjectGraphProjectNode[];
releaseGroup: ReleaseGroupWithName;
projectGraph: ProjectGraph;
conventionalCommitsConfig?: NxReleaseConfig['conventionalCommits'];
firstRelease?: boolean;
deleteVersionPlans?: boolean;
// end: internal props
specifier?: string;
specifierSource?: 'prompt' | 'conventional-commits' | 'version-plans';
preid?: string;
@@ -77,14 +86,11 @@ export interface ReleaseVersionGeneratorSchema {
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.
@@ -118,6 +124,42 @@ export interface NxReleaseVersionResult {
projectsVersionData: VersionData;
}
async function releaseVersionGenerator(
tree: Tree,
{
nxReleaseConfig,
projectGraph,
releaseGroups,
projects,
userGivenSpecifier,
}: // currentVersionResolver,
{
nxReleaseConfig: NxReleaseConfig;
projectGraph: ProjectGraph;
releaseGroups: ReleaseGroupWithName[];
projects: string[];
userGivenSpecifier?: SemverBumpType;
// currentVersionResolver: 'disk';
}
): Promise<ReleaseVersionGeneratorResult> {
const processor = new ReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig as any,
releaseGroups as any,
userGivenSpecifier,
projects
);
await processor.buildGroupGraph();
await processor.processGroups();
return {
callback: 'TODO__SHOULD_ACTUALLY_BE_A_CALLBACK_FUNCTION' as any,
data: processor.getVersionData(),
};
}
export const releaseVersionCLIHandler = (args: VersionOptions) =>
handleErrors(args.verbose, () => createAPI({})(args));
@@ -228,9 +270,9 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
const tree = new FsTree(workspaceRoot, args.verbose);
const versionData: VersionData = {};
const commitMessage: string | undefined =
args.gitCommitMessage || nxReleaseConfig.version.git.commitMessage;
// const versionData: VersionData = {};
// const commitMessage: string | undefined =
// args.gitCommitMessage || nxReleaseConfig.version.git.commitMessage;
const generatorCallbacks: (() => Promise<void>)[] = [];
/**
@@ -240,241 +282,272 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
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
);
const { callback, data } = await releaseVersionGenerator(tree, {
nxReleaseConfig,
projectGraph,
releaseGroups,
projects: args.projects,
userGivenSpecifier: args.specifier as SemverBumpType | undefined,
});
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));
});
}
}
console.log({ callback, data });
// 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);
const versionData = data;
printAndFlushChanges(tree, !!args.dryRun);
// 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));
// });
// }
for (const generatorCallback of generatorCallbacks) {
await generatorCallback();
}
// 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
// );
const changedFiles = [
...tree.listChanges().map((f) => f.path),
...additionalChangedFiles,
];
// 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));
// });
// }
// }
// 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,
};
}
// // 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);
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,
});
}
// printAndFlushChanges(tree, !!args.dryRun);
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,
});
}
}
// for (const generatorCallback of generatorCallbacks) {
// await generatorCallback();
// }
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,
});
}
// const changedFiles = [
// ...tree.listChanges().map((f) => f.path),
// ...additionalChangedFiles,
// ];
return {
// An overall workspace version cannot be relevant when filtering to independent projects
workspaceVersion: undefined,
projectsVersionData: versionData,
};
}
// // 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,
// });
// }
// }
// 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;
// for (const releaseGroup of releaseGroups) {
// const releaseGroupName = releaseGroup.name;
runPreVersionCommand(
releaseGroup.version.groupPreVersionCommand,
{
dryRun: args.dryRun,
verbose: args.verbose,
},
releaseGroup
);
// 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
);
// 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));
});
}
}
// 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);
// const gitTagValues: string[] =
// args.gitTag ?? nxReleaseConfig.version.git.tag
// ? createGitTagValues(
// releaseGroups,
// releaseGroupToFilteredProjects,
// versionData
// )
// : [];
// handleDuplicateGitTags(gitTagValues);
printAndFlushChanges(tree, !!args.dryRun);
@@ -508,44 +581,44 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
};
}
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.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,
});
}
}
// 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}"`);
@@ -596,8 +669,9 @@ async function runVersionOnProjects(
specifier: args.specifier ?? '',
preid: args.preid ?? '',
...generatorData.configGeneratorOptions,
// TODO: consider removing this in favour of the dynamic release API client
...(generatorOverrides ?? {}),
// The following are not overridable by user config
// The properties below this line are not overridable via generatorOptions, but are necessary for the generator to function appropriately
projects: projectNames.map((p) => projectGraph.nodes[p]),
projectGraph,
releaseGroup,
+14 -221
View File
@@ -235,9 +235,12 @@ importers:
'@jest/types':
specifier: 29.6.3
version: 29.6.3
'@ltd/j-toml':
specifier: ^1.38.0
version: 1.38.0
'@module-federation/enhanced':
specifier: ^0.8.8
version: 0.8.8(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)
version: 0.8.9(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)
'@module-federation/sdk':
specifier: ^0.8.8
version: 0.8.8
@@ -333,7 +336,7 @@ importers:
version: 20.4.0-beta.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(nx@20.4.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.7.3)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0))
'@nx/rspack':
specifier: 20.4.0-beta.2
version: 20.4.0-beta.2(ycojri7ferr326mi37jyqx4vdi)
version: 20.4.0-beta.2(2qpor2ynlccjwzkvsm6jq6b5ze)
'@nx/storybook':
specifier: 20.4.0-beta.2
version: 20.4.0-beta.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(@zkochan/js-yaml@0.0.7)(cypress@13.13.0)(eslint@8.57.0)(nx@20.4.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.7.3)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0))
@@ -4045,33 +4048,15 @@ packages:
'@mediapipe/tasks-vision@0.10.8':
resolution: {integrity: sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==}
'@module-federation/bridge-react-webpack-plugin@0.8.8':
resolution: {integrity: sha512-+yNakzLaZf+km3WAhOptdMZRa57lfvqPnZmPiHXW28hpB7CAMDt2aP6rwtrDmJ59WpIPgTbr/J3QeQ5kKdaadw==}
'@module-federation/bridge-react-webpack-plugin@0.8.9':
resolution: {integrity: sha512-gM5fnWvFfTEfh5UjSmNod1H/np/pUUjvJnnN31ucKcZ21mL6wJo0A+Vfj8a5gv34S+1n5lWR1b6eVv9CofwgDQ==}
'@module-federation/data-prefetch@0.8.8':
resolution: {integrity: sha512-HoqhdCviO8VmOOpI1/f0GqSLXTdABjejvgqOwbl2eR6PaOhragrr+hmLuKmJ+aaAc6nQml1ICxBo/WtJLx5hbw==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@module-federation/data-prefetch@0.8.9':
resolution: {integrity: sha512-doUzJ5uuLPrN5e/NMvKuyUE/lqiZFgGzFITwBF90/KKqOq7R2ZWh43R4RJgkUYfs/F119AYXZ9jxrSphIh+cjg==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@module-federation/dts-plugin@0.8.8':
resolution: {integrity: sha512-o141NhvPHJBDj09iLwiGnEaByav8OJv+GdxHGtKAmYLImZK7VlxdselsNtiyan1iJwJ04pmfXAvDVsRglC/hyQ==}
peerDependencies:
typescript: ^4.9.0 || ^5.0.0
vue-tsc: '>=1.0.24'
peerDependenciesMeta:
vue-tsc:
optional: true
'@module-federation/dts-plugin@0.8.9':
resolution: {integrity: sha512-Q40yqdQvTV8QRoegjUquTejhLl2gZoPv623hc8MWmGuP2vXLBIJJ9iV849LkFLeE8xaUgtwmn2uCQeBh2aJNjg==}
peerDependencies:
@@ -4081,20 +4066,6 @@ packages:
vue-tsc:
optional: true
'@module-federation/enhanced@0.8.8':
resolution: {integrity: sha512-WVKB4MYa9Pw7mpgSFs4ADeOifx6dpLxV/3IMW0UPN5JPOeET1Hx4f1suNBCh6+HNmsGxxHM/yovK6DxNTGPpAA==}
peerDependencies:
typescript: ^4.9.0 || ^5.0.0
vue-tsc: '>=1.0.24'
webpack: ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
vue-tsc:
optional: true
webpack:
optional: true
'@module-federation/enhanced@0.8.9':
resolution: {integrity: sha512-feZUhznCyFkyivWNSFPN+FHMZmyrxlKBhWW8ldWsp/LlKIzWI75vMLimpiydBWnWTZgnOhQDBZGZVxSlfnfKHQ==}
peerDependencies:
@@ -4109,31 +4080,17 @@ packages:
webpack:
optional: true
'@module-federation/error-codes@0.8.8':
resolution: {integrity: sha512-gQKzDrTvMUduK4NzfKhm/tdHaXPQJtmUxNffk4LsG3uAG/L0xLHLqPNEYyY2VYqvEDvwyQSNnOERgnwgcp5zVg==}
'@module-federation/error-codes@0.8.9':
resolution: {integrity: sha512-yUA3GZjOy8Ll6l193faXir2veexDaUiLdmptbzC9tIee/iSQiSwIlibdTafCfqaJ62cLZaytOUdmAFAKLv8QQw==}
'@module-federation/inject-external-runtime-core-plugin@0.8.8':
resolution: {integrity: sha512-nqmBjbH/1No5isiyBILELZin2gRL3L8zVN0AUu/h0my6V3/IWXu3dCiuqHtSN2vkXLVl4wK32NK7FCbzL98ftA==}
peerDependencies:
'@module-federation/runtime-tools': 0.8.8
'@module-federation/inject-external-runtime-core-plugin@0.8.9':
resolution: {integrity: sha512-F3s/3Iny20c0Y0CaXgOZdgl7PzgXj6ftCiT5FO6QWmyhzfl2R0o3KFuv54BmwtoimSFky1ShuC/jPluz4EGG1g==}
peerDependencies:
'@module-federation/runtime-tools': 0.8.9
'@module-federation/managers@0.8.8':
resolution: {integrity: sha512-dxoFVyB+lizdQp8vMIMXswuwHQxFTUhiahkaeHY4wjvhdLwDHFQ7HXYULtI5Nrly1HoFMBIUV4JDBNkHUMaSBQ==}
'@module-federation/managers@0.8.9':
resolution: {integrity: sha512-Fb28sa2iAqwSSSgiPBoL5Kp0RB9ZKJvdwLAEwZBV2c2LyZUqJCdTjmYuB5Q2sWB9wsudd/ijV6WWNK/nVoJxdA==}
'@module-federation/manifest@0.8.8':
resolution: {integrity: sha512-hef1p5TC2G1UUabvf7W8Uz8c44Sl6NmTY/fUwVjfdev1GC+Tu6ecBAx8l+dOPHfsQlxWFEwdBUJA7NcGOP8bmg==}
'@module-federation/manifest@0.8.9':
resolution: {integrity: sha512-48jIv50l0tTx+CVzs2JKMlTO0mcUvFRDxqO79nJaL1DLfpRQoCRQjCA8sXUp5PPIyJ8MHQqkHK/dnaal4NlR/A==}
@@ -4152,18 +4109,6 @@ packages:
react-dom:
optional: true
'@module-federation/rspack@0.8.8':
resolution: {integrity: sha512-l0iARZ8PgJhD/+6AjXky2gpryOiXaW9cKsiJgurqWQ8yseTANX2uW72TOooY0ezZK38X/ZiFYWTbxJNfDJ36UQ==}
peerDependencies:
'@rspack/core': '>=0.7'
typescript: ^4.9.0 || ^5.0.0
vue-tsc: '>=1.0.24'
peerDependenciesMeta:
typescript:
optional: true
vue-tsc:
optional: true
'@module-federation/rspack@0.8.9':
resolution: {integrity: sha512-wuHRx+aUYhsrAQzU5yKQksaOJ8pwhuQJDl8I86xmzkEsnxcWsZcn49xIR4t7v1HcYAqjBXLNXSoQwkIMH9zu2w==}
peerDependencies:
@@ -4176,27 +4121,18 @@ packages:
vue-tsc:
optional: true
'@module-federation/runtime-core@0.6.16':
resolution: {integrity: sha512-jx3O9hKyaW02S0ANxv14KS5JxOjlNpmrXNXrfvctpPlAIZL5WTiwECtOU3W6a4OXfNh54X4say6idaHWWOH1Ig==}
'@module-federation/runtime-core@0.6.17':
resolution: {integrity: sha512-PXFN/TT9f64Un6NQYqH1Z0QLhpytW15jkZvTEOV8W7Ed319BECFI0Rv4xAsAGa8zJGFoaM/c7QOQfdFXtKj5Og==}
'@module-federation/runtime-tools@0.5.1':
resolution: {integrity: sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==}
'@module-federation/runtime-tools@0.8.8':
resolution: {integrity: sha512-7wBi0zxUVc7h4v8UZ5EkI8F1gVSjXlfEZ4kS8/n/zIy5h+wGZzR3x8x7PCAWgH5b2EpF7sHfiMXPvLJyQAhegQ==}
'@module-federation/runtime-tools@0.8.9':
resolution: {integrity: sha512-xBUGx1oOZNuxXjPGdTMrLtAIDrbrN6jE2Mgb9w1qr2mQ4AW9b5TOlxbARBoX4q98xt9oFCGU6Q0eW5XJpsl8AQ==}
'@module-federation/runtime@0.5.1':
resolution: {integrity: sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==}
'@module-federation/runtime@0.8.8':
resolution: {integrity: sha512-ZKIhxpqUFtB5PTHWngsCEBOKtwRIJznJnpWBpP+Zvg6Xk+B8LC1rS5GU306TzIb5SWTzbMX8eWB0T8PtFvyjSQ==}
'@module-federation/runtime@0.8.9':
resolution: {integrity: sha512-i+a+/hoT/c+EE52mT+gJrbA6DhL86PY9cd/dIv/oKpLz9i+yYBlG+RA+puc7YsUEO4irbFLvnIMq6AGDUKVzYA==}
@@ -4209,9 +4145,6 @@ packages:
'@module-federation/sdk@0.8.9':
resolution: {integrity: sha512-QJ60itWC/SPjqduT7wDiF8UGwVU/yJ/Sz+QbnoxB9b7gNLzvI//swAXTo9eOtKsCy/V2BMwjt0F3eOcfnaqllA==}
'@module-federation/third-party-dts-extractor@0.8.8':
resolution: {integrity: sha512-6dPM7TXAjyHezq8w5PMFSfMNags23ux40qQr2WIze9dF/l0pEumjncmWax0f30FIRM+nIsY1dn2l7pJHqnu0zg==}
'@module-federation/third-party-dts-extractor@0.8.9':
resolution: {integrity: sha512-53v6B5zfhGlAPpH0SrlJDp9B6kcOcZaUPi6J4L/7ie5F0YVe0vq/mJGOOzAStN9ggJjSrjBZLFT3gFmBYM3Z9A==}
@@ -4233,9 +4166,6 @@ packages:
'@module-federation/webpack-bundler-runtime@0.5.1':
resolution: {integrity: sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==}
'@module-federation/webpack-bundler-runtime@0.8.8':
resolution: {integrity: sha512-tvUgzcaEuqNeGWRXdrm4/OsMfW5PX7ZJp+Z09AUwqMQ+usWTckByNEcVES4u7l2nVkkQbk6fVUo5Xhqm6fjKOA==}
'@module-federation/webpack-bundler-runtime@0.8.9':
resolution: {integrity: sha512-DYLvVi4b2MUYu/B4g5wIC5SHxiODboKHkYGHYapOhCcqOchca/N16gtiAI8eSNjJPc+fgUXUGIyGiB18IlFEeQ==}
@@ -21038,26 +20968,12 @@ snapshots:
'@mediapipe/tasks-vision@0.10.8': {}
'@module-federation/bridge-react-webpack-plugin@0.8.8':
dependencies:
'@module-federation/sdk': 0.8.8
'@types/semver': 7.5.8
semver: 7.6.3
'@module-federation/bridge-react-webpack-plugin@0.8.9':
dependencies:
'@module-federation/sdk': 0.8.9
'@types/semver': 7.5.8
semver: 7.6.3
'@module-federation/data-prefetch@0.8.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@module-federation/runtime': 0.8.8
'@module-federation/sdk': 0.8.8
fs-extra: 9.1.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@module-federation/data-prefetch@0.8.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@module-federation/runtime': 0.8.9
@@ -21066,31 +20982,6 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@module-federation/dts-plugin@0.8.8(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/error-codes': 0.8.8
'@module-federation/managers': 0.8.8
'@module-federation/sdk': 0.8.8
'@module-federation/third-party-dts-extractor': 0.8.8
adm-zip: 0.5.16
ansi-colors: 4.1.3
axios: 1.7.7
chalk: 3.0.0
fs-extra: 9.1.0
isomorphic-ws: 5.0.0(ws@8.18.0(bufferutil@4.0.7))
koa: 2.15.3
lodash.clonedeepwith: 4.5.0
log4js: 6.9.1
node-schedule: 2.1.1
rambda: 9.3.0
typescript: 5.7.3
ws: 8.18.0(bufferutil@4.0.7)
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
'@module-federation/dts-plugin@0.8.9(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/error-codes': 0.8.9
@@ -21116,32 +21007,6 @@ snapshots:
- supports-color
- utf-8-validate
'@module-federation/enhanced@0.8.8(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)':
dependencies:
'@module-federation/bridge-react-webpack-plugin': 0.8.8
'@module-federation/data-prefetch': 0.8.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@module-federation/dts-plugin': 0.8.8(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/error-codes': 0.8.8
'@module-federation/inject-external-runtime-core-plugin': 0.8.8(@module-federation/runtime-tools@0.8.8)
'@module-federation/managers': 0.8.8
'@module-federation/manifest': 0.8.8(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/rspack': 0.8.8(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/runtime-tools': 0.8.8
'@module-federation/sdk': 0.8.8
btoa: 1.2.1
upath: 2.0.1
optionalDependencies:
typescript: 5.7.3
webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)
transitivePeerDependencies:
- '@rspack/core'
- bufferutil
- debug
- react
- react-dom
- supports-color
- utf-8-validate
'@module-federation/enhanced@0.8.9(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)':
dependencies:
'@module-federation/bridge-react-webpack-plugin': 0.8.9
@@ -21168,45 +21033,18 @@ snapshots:
- supports-color
- utf-8-validate
'@module-federation/error-codes@0.8.8': {}
'@module-federation/error-codes@0.8.9': {}
'@module-federation/inject-external-runtime-core-plugin@0.8.8(@module-federation/runtime-tools@0.8.8)':
dependencies:
'@module-federation/runtime-tools': 0.8.8
'@module-federation/inject-external-runtime-core-plugin@0.8.9(@module-federation/runtime-tools@0.8.9)':
dependencies:
'@module-federation/runtime-tools': 0.8.9
'@module-federation/managers@0.8.8':
dependencies:
'@module-federation/sdk': 0.8.8
find-pkg: 2.0.0
fs-extra: 9.1.0
'@module-federation/managers@0.8.9':
dependencies:
'@module-federation/sdk': 0.8.9
find-pkg: 2.0.0
fs-extra: 9.1.0
'@module-federation/manifest@0.8.8(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/dts-plugin': 0.8.8(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/managers': 0.8.8
'@module-federation/sdk': 0.8.8
chalk: 3.0.0
find-pkg: 2.0.0
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- typescript
- utf-8-validate
- vue-tsc
'@module-federation/manifest@0.8.9(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/dts-plugin': 0.8.9(bufferutil@4.0.7)(typescript@5.7.3)
@@ -21245,24 +21083,6 @@ snapshots:
- utf-8-validate
- vue-tsc
'@module-federation/rspack@0.8.8(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/bridge-react-webpack-plugin': 0.8.8
'@module-federation/dts-plugin': 0.8.8(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/inject-external-runtime-core-plugin': 0.8.8(@module-federation/runtime-tools@0.8.8)
'@module-federation/managers': 0.8.8
'@module-federation/manifest': 0.8.8(bufferutil@4.0.7)(typescript@5.7.3)
'@module-federation/runtime-tools': 0.8.8
'@module-federation/sdk': 0.8.8
'@rspack/core': 1.1.6(@swc/helpers@0.5.11)
optionalDependencies:
typescript: 5.7.3
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
'@module-federation/rspack@0.8.9(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(typescript@5.7.3)':
dependencies:
'@module-federation/bridge-react-webpack-plugin': 0.8.9
@@ -21281,11 +21101,6 @@ snapshots:
- supports-color
- utf-8-validate
'@module-federation/runtime-core@0.6.16':
dependencies:
'@module-federation/error-codes': 0.8.8
'@module-federation/sdk': 0.8.8
'@module-federation/runtime-core@0.6.17':
dependencies:
'@module-federation/error-codes': 0.8.9
@@ -21296,11 +21111,6 @@ snapshots:
'@module-federation/runtime': 0.5.1
'@module-federation/webpack-bundler-runtime': 0.5.1
'@module-federation/runtime-tools@0.8.8':
dependencies:
'@module-federation/runtime': 0.8.8
'@module-federation/webpack-bundler-runtime': 0.8.8
'@module-federation/runtime-tools@0.8.9':
dependencies:
'@module-federation/runtime': 0.8.9
@@ -21310,12 +21120,6 @@ snapshots:
dependencies:
'@module-federation/sdk': 0.5.1
'@module-federation/runtime@0.8.8':
dependencies:
'@module-federation/error-codes': 0.8.8
'@module-federation/runtime-core': 0.6.16
'@module-federation/sdk': 0.8.8
'@module-federation/runtime@0.8.9':
dependencies:
'@module-federation/error-codes': 0.8.9
@@ -21332,12 +21136,6 @@ snapshots:
dependencies:
isomorphic-rslog: 0.0.7
'@module-federation/third-party-dts-extractor@0.8.8':
dependencies:
find-pkg: 2.0.0
fs-extra: 9.1.0
resolve: 1.22.8
'@module-federation/third-party-dts-extractor@0.8.9':
dependencies:
find-pkg: 2.0.0
@@ -21358,11 +21156,6 @@ snapshots:
'@module-federation/runtime': 0.5.1
'@module-federation/sdk': 0.5.1
'@module-federation/webpack-bundler-runtime@0.8.8':
dependencies:
'@module-federation/runtime': 0.8.8
'@module-federation/sdk': 0.8.8
'@module-federation/webpack-bundler-runtime@0.8.9':
dependencies:
'@module-federation/runtime': 0.8.9
@@ -22849,9 +22642,9 @@ snapshots:
- typescript
- verdaccio
'@nx/rspack@20.4.0-beta.2(ycojri7ferr326mi37jyqx4vdi)':
'@nx/rspack@20.4.0-beta.2(2qpor2ynlccjwzkvsm6jq6b5ze)':
dependencies:
'@module-federation/enhanced': 0.8.8(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)
'@module-federation/enhanced': 0.8.9(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)
'@module-federation/node': 2.6.22(@rspack/core@1.1.6(@swc/helpers@0.5.11))(bufferutil@4.0.7)(next@14.2.16(@babel/core@7.25.2)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.55.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)(webpack@5.88.0)
'@nx/devkit': 20.4.0-beta.2(nx@20.4.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))
'@nx/js': 20.4.0-beta.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(nx@20.4.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(typescript@5.7.3)(verdaccio@5.32.2(encoding@0.1.13)(typanion@3.14.0))
@@ -24256,7 +24049,7 @@ snapshots:
es-module-lexer: 1.5.4
fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.7.3)(webpack@5.88.0)
html-webpack-plugin: 5.5.0(webpack@5.88.0)
magic-string: 0.30.17
magic-string: 0.30.12
path-browserify: 1.0.1
process: 0.11.10
semver: 7.6.3
@@ -24391,7 +24184,7 @@ snapshots:
'@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.7.3)(webpack@5.88.0)':
dependencies:
debug: 4.3.7(supports-color@8.1.1)
debug: 4.4.0
endent: 2.1.0
find-cache-dir: 3.3.2
flat-cache: 3.2.0
@@ -31559,7 +31352,7 @@ snapshots:
content-disposition: 0.5.4
content-type: 1.0.5
cookies: 0.9.1
debug: 4.3.7(supports-color@8.1.1)
debug: 4.4.0
delegates: 1.0.0
depd: 2.0.0
destroy: 1.2.0
@@ -31861,7 +31654,7 @@ snapshots:
log4js@6.9.1:
dependencies:
date-format: 4.0.14
debug: 4.3.7(supports-color@8.1.1)
debug: 4.4.0
flatted: 3.3.1
rfdc: 1.4.1
streamroller: 3.1.5
@@ -36114,7 +35907,7 @@ snapshots:
streamroller@3.1.5:
dependencies:
date-format: 4.0.14
debug: 4.3.7(supports-color@8.1.1)
debug: 4.4.0
fs-extra: 8.1.0
transitivePeerDependencies:
- supports-color
@@ -36303,7 +36096,7 @@ snapshots:
stylus@0.64.0:
dependencies:
'@adobe/css-tools': 4.3.3
debug: 4.3.7(supports-color@8.1.1)
debug: 4.4.0
glob: 10.4.5
sax: 1.4.1
source-map: 0.7.4
@@ -37942,7 +37735,7 @@ snapshots:
browserslist: 4.24.2
chrome-trace-event: 1.0.4
enhanced-resolve: 5.17.1
es-module-lexer: 1.5.4
es-module-lexer: 1.6.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1