diff --git a/.eslintrc.json b/.eslintrc.json index 187cd9a..29e4a46 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,6 +18,13 @@ } ] } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "ignoreRestSiblings": true, + "varsIgnorePattern": "^_" + } ] } }, diff --git a/package.json b/package.json index dee32c4..826c120 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ "@commitlint/config-conventional": "^19.2.2", "@commitlint/cz-commitlint": "^19.4.0", "@commitlint/types": "^19.0.3", - "@nx/devkit": "19.5.7", - "@nx/eslint": "19.5.7", - "@nx/eslint-plugin": "19.5.7", - "@nx/js": "19.5.7", - "@nx/plugin": "19.5.7", - "@nx/vite": "19.5.7", - "@nx/web": "19.5.7", - "@nx/workspace": "19.5.7", + "@nx/devkit": "19.7.3", + "@nx/eslint": "19.7.3", + "@nx/eslint-plugin": "19.7.3", + "@nx/js": "19.7.3", + "@nx/plugin": "19.7.3", + "@nx/vite": "19.7.3", + "@nx/web": "19.7.3", + "@nx/workspace": "19.7.3", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -46,8 +46,8 @@ "jsonc-eslint-parser": "^2.4.0", "lint-staged": "^15.2.2", "memfs": "^4.11.1", - "nx": "19.5.7", - "nx-cloud": "19.0.0", + "nx": "19.7.3", + "nx-cloud": "19.1.0", "prettier": "^3.2.5", "semantic-release-npm": "^0.0.5", "semantic-release-plus": "^20.0.0", @@ -73,6 +73,7 @@ "@swc/helpers": "0.5.12", "archiver": "^7.0.0", "aws-lambda": "^1.0.7", + "axios": "^1.7.7", "chalk": "^4.1.1", "command-exists": "^1.2.9", "cross-spawn": "^7.0.3", @@ -82,7 +83,9 @@ "fs-extra": "^11.2.0", "glob": "^10.3.10", "lodash": "^4.17.21", + "ora": "5.3.0", "prompts": "^2.4.2", + "semver": "^7.5.3", "tslib": "^2.3.0", "uuid": "^9.0.1" }, diff --git a/packages/nx-python/README.md b/packages/nx-python/README.md index f42a6ab..ff9a67b 100644 --- a/packages/nx-python/README.md +++ b/packages/nx-python/README.md @@ -548,3 +548,13 @@ autoActivate = true The options and behavior are the same as the `nx:run-commands` executor. [See the Nx documentation for more information](https://nx.dev/packages/nx/executors/run-commands) + +#### Releases + +This plugin supports the [Nx releases](https://nx.dev/features/manage-releases) feature. + +If you are already using the `@nxlv/python` plugin and want to enable the releases feature, please run the following command: + +```bash +nx generate @nxlv/python:enable-releases +``` diff --git a/packages/nx-python/generators.json b/packages/nx-python/generators.json index 7deebc1..8d8c356 100644 --- a/packages/nx-python/generators.json +++ b/packages/nx-python/generators.json @@ -17,6 +17,18 @@ "factory": "./src/generators/poetry-project/generator", "schema": "./src/generators/poetry-project/schema.json", "description": "Python Poetry Project" + }, + "release-version": { + "factory": "./src/generators/release-version/release-version", + "schema": "./src/generators/release-version/schema.json", + "description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.", + "hidden": true + }, + "enable-releases": { + "factory": "./src/generators/enable-releases/generator", + "schema": "./src/generators/enable-releases/schema.json", + "description": "Enable Releases for Python projects", + "hidden": true } } } diff --git a/packages/nx-python/package.json b/packages/nx-python/package.json index b95ea77..18c09c5 100644 --- a/packages/nx-python/package.json +++ b/packages/nx-python/package.json @@ -22,7 +22,9 @@ "command-exists": "^1.2.9", "lodash": "^4.17.21", "@nx/devkit": "^19.0.0", - "nx": "^19.0.0" + "nx": "^19.0.0", + "ora": "5.3.0", + "semver": "^7.5.3" }, "nx-migrations": { "migrations": "./migrations.json" diff --git a/packages/nx-python/src/executors/utils/poetry.ts b/packages/nx-python/src/executors/utils/poetry.ts index bdc9ea9..ae91c2a 100644 --- a/packages/nx-python/src/executors/utils/poetry.ts +++ b/packages/nx-python/src/executors/utils/poetry.ts @@ -1,4 +1,4 @@ -import { ExecutorContext, ProjectConfiguration } from '@nx/devkit'; +import { ExecutorContext, ProjectConfiguration, Tree } from '@nx/devkit'; import chalk from 'chalk'; import spawn from 'cross-spawn'; import path from 'path'; @@ -84,6 +84,23 @@ export function parseToml(tomlFile: string) { return toml.parse(fs.readFileSync(tomlFile, 'utf-8')) as PyprojectToml; } +export function readPyprojectToml(tree: Tree, tomlFile: string) { + const content = tree.read(tomlFile, 'utf-8'); + if (!content) { + return null; + } + + return toml.parse(content) as PyprojectToml; +} + +export function writePyprojectToml( + tree: Tree, + tomlFile: string, + data: PyprojectToml, +) { + tree.write(tomlFile, toml.stringify(data)); +} + export function getLocalDependencyConfig( context: ExecutorContext, dependencyName: string, diff --git a/packages/nx-python/src/generators/enable-releases/generator.spec.ts b/packages/nx-python/src/generators/enable-releases/generator.spec.ts new file mode 100644 index 0000000..6deb78d --- /dev/null +++ b/packages/nx-python/src/generators/enable-releases/generator.spec.ts @@ -0,0 +1,56 @@ +import { vi, MockInstance } from 'vitest'; +import '../../utils/mocks/cross-spawn.mock'; +import * as poetryUtils from '../../executors/utils/poetry'; +import { readJson, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import generator from './generator'; +import projectGenerator from '../poetry-project/generator'; +import spawn from 'cross-spawn'; + +describe('nx-python enable-releases', () => { + let checkPoetryExecutableMock: MockInstance; + let appTree: Tree; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace({}); + checkPoetryExecutableMock = vi.spyOn(poetryUtils, 'checkPoetryExecutable'); + checkPoetryExecutableMock.mockResolvedValue(undefined); + vi.mocked(spawn.sync).mockReturnValue({ + status: 0, + output: [''], + pid: 0, + signal: null, + stderr: null, + stdout: null, + }); + }); + + it('should add release version generator', async () => { + await projectGenerator(appTree, { + name: 'proj1', + projectType: 'application', + pyprojectPythonDependency: '', + pyenvPythonVersion: '', + publishable: false, + buildLockedVersions: false, + buildBundleLocalDependencies: false, + linter: 'none', + unitTestRunner: 'none', + rootPyprojectDependencyGroup: 'main', + unitTestHtmlReport: false, + unitTestJUnitReport: false, + codeCoverage: false, + codeCoverageHtmlReport: false, + codeCoverageXmlReport: false, + projectNameAndRootFormat: 'derived', + }); + + await generator(appTree); + + expect(readJson(appTree, 'proj1/project.json').release).toEqual({ + version: { + generator: '@nxlv/python:release-version', + }, + }); + }); +}); diff --git a/packages/nx-python/src/generators/enable-releases/generator.ts b/packages/nx-python/src/generators/enable-releases/generator.ts new file mode 100644 index 0000000..637fff7 --- /dev/null +++ b/packages/nx-python/src/generators/enable-releases/generator.ts @@ -0,0 +1,20 @@ +import { getProjects, Tree, updateProjectConfiguration } from '@nx/devkit'; +import path from 'path'; + +async function generator(host: Tree) { + for (const project of getProjects(host)) { + const [projectName, projectConfig] = project; + const pyprojectTomlPath = path.join(projectConfig.root, 'pyproject.toml'); + if (host.exists(pyprojectTomlPath)) { + projectConfig.release = projectConfig.release || { + version: { + generator: '@nxlv/python:release-version', + }, + }; + + updateProjectConfiguration(host, projectName, projectConfig); + } + } +} + +export default generator; diff --git a/packages/nx-python/src/generators/enable-releases/schema.d.ts b/packages/nx-python/src/generators/enable-releases/schema.d.ts new file mode 100644 index 0000000..b7e34f3 --- /dev/null +++ b/packages/nx-python/src/generators/enable-releases/schema.d.ts @@ -0,0 +1 @@ +export type Schema = object; diff --git a/packages/nx-python/src/generators/enable-releases/schema.json b/packages/nx-python/src/generators/enable-releases/schema.json new file mode 100644 index 0000000..b1ce22c --- /dev/null +++ b/packages/nx-python/src/generators/enable-releases/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxEnableReleases", + "title": "Enable Releases for Python projects", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/packages/nx-python/src/generators/poetry-project/__snapshots__/generator.spec.ts.snap b/packages/nx-python/src/generators/poetry-project/__snapshots__/generator.spec.ts.snap index e5b5ef7..cf39e22 100644 --- a/packages/nx-python/src/generators/poetry-project/__snapshots__/generator.spec.ts.snap +++ b/packages/nx-python/src/generators/poetry-project/__snapshots__/generator.spec.ts.snap @@ -5,6 +5,11 @@ exports[`application generator > as-provided > should run successfully minimal c "$schema": "../../../node_modules/nx/schemas/project-schema.json", "name": "my-app-test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "src/app/test", "sourceRoot": "src/app/test/my_app_test", "tags": [], @@ -102,6 +107,11 @@ exports[`application generator > as-provided > should run successfully minimal c "$schema": "../node_modules/nx/schemas/project-schema.json", "name": "my-app-test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "my-app-test", "sourceRoot": "my-app-test/my_app_test", "tags": [], @@ -199,6 +209,11 @@ exports[`application generator > custom template dir > should run successfully w "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -278,6 +293,11 @@ exports[`application generator > individual package > should run successfully mi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -375,6 +395,11 @@ exports[`application generator > individual package > should run successfully mi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "library", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "libs/test", "sourceRoot": "libs/test/test", "tags": [], @@ -472,6 +497,11 @@ exports[`application generator > individual package > should run successfully mi "$schema": "../../../node_modules/nx/schemas/project-schema.json", "name": "subdir-test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/subdir/test", "sourceRoot": "apps/subdir/test/subdir_test", "tags": [], @@ -569,6 +599,11 @@ exports[`application generator > individual package > should run successfully mi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [ @@ -669,6 +704,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -794,6 +834,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -957,6 +1002,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1120,6 +1170,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1283,6 +1338,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1446,6 +1506,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1609,6 +1674,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1759,6 +1829,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -1967,6 +2042,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2197,6 +2277,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2406,6 +2491,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2544,6 +2634,11 @@ exports[`application generator > individual package > should run successfully wi "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2712,6 +2807,11 @@ exports[`application generator > shared virtual environment > should run success "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2829,6 +2929,11 @@ exports[`application generator > shared virtual environment > should run success "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -2946,6 +3051,11 @@ exports[`application generator > shared virtual environment > should run success "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], @@ -3064,6 +3174,11 @@ exports[`application generator > shared virtual environment > should run success "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "test", "projectType": "application", + "release": { + "version": { + "generator": "@nxlv/python:release-version", + }, + }, "root": "apps/test", "sourceRoot": "apps/test/test", "tags": [], diff --git a/packages/nx-python/src/generators/poetry-project/generator.ts b/packages/nx-python/src/generators/poetry-project/generator.ts index c8b3db9..3f79adc 100644 --- a/packages/nx-python/src/generators/poetry-project/generator.ts +++ b/packages/nx-python/src/generators/poetry-project/generator.ts @@ -456,13 +456,39 @@ export default async function ( }; } - addProjectConfiguration(tree, normalizedOptions.projectName, { + const projectConfiguration: ProjectConfiguration = { root: normalizedOptions.projectRoot, projectType: normalizedOptions.projectType, sourceRoot: `${normalizedOptions.projectRoot}/${normalizedOptions.moduleName}`, targets, tags: normalizedOptions.parsedTags, - }); + }; + + if (normalizedOptions.publishable) { + projectConfiguration.targets ??= {}; + projectConfiguration.targets['nx-release-publish'] = { + executor: 'nx:run-commands', + options: { + command: 'poetry publish', + cwd: normalizedOptions.projectRoot, + forwardAllArgs: false, + }, + dependsOn: ['build'], + }; + } + + projectConfiguration.release = { + version: { + generator: '@nxlv/python:release-version', + }, + }; + + addProjectConfiguration( + tree, + normalizedOptions.projectName, + projectConfiguration, + ); + addFiles(tree, normalizedOptions); updateDevDependenciesProject(tree, normalizedOptions); updateRootPyprojectToml(tree, normalizedOptions); diff --git a/packages/nx-python/src/generators/release-version/release-version.spec.ts b/packages/nx-python/src/generators/release-version/release-version.spec.ts new file mode 100644 index 0000000..978b613 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/release-version.spec.ts @@ -0,0 +1,2423 @@ +import { vi } from 'vitest'; + +const originalExit = process.exit; +let stubProcessExit = false; + +const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => { + if (stubProcessExit) { + return undefined as never; + } + return originalExit(code); +}); + +const enquirerMocks = vi.hoisted(() => { + const mocks = { + prompt: vi.fn(), + }; + + void mock('enquirer', mocks); + return mocks; +}); + +import { output, ProjectGraph, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { createWorkspaceWithPackageDependencies } from './test-utils/create-workspace-with-package-dependencies'; +import { releaseVersionGenerator } from './release-version'; +import { ReleaseGroupWithName } from 'nx/src/command-line/release/config/filter-release-groups'; +import { readPyprojectToml } from '../../executors/utils/poetry'; + +process.env.NX_DAEMON = 'false'; + +describe('release-version', () => { + let tree: Tree; + let projectGraph: ProjectGraph; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + + projectGraph = createWorkspaceWithPackageDependencies(tree, { + 'my-lib': { + projectRoot: 'libs/my-lib', + packageName: 'my-lib', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-lib/pyproject.toml', + localDependencies: [], + }, + 'project-with-dependency-on-my-pkg': { + projectRoot: 'libs/project-with-dependency-on-my-pkg', + packageName: 'project-with-dependency-on-my-pkg', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dependencies', + }, + ], + }, + 'project-with-devDependency-on-my-pkg': { + projectRoot: 'libs/project-with-devDependency-on-my-pkg', + packageName: 'project-with-devDependency-on-my-pkg', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dev', + }, + ], + }, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return a versionData object', async () => { + expect( + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "my-lib": { + "currentVersion": "0.0.1", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "0.0.1", + "source": "project-with-dependency-on-my-pkg", + "target": "my-lib", + "type": "static", + }, + { + "dependencyCollection": "devDependencies", + "rawVersionSpec": "0.0.1", + "source": "project-with-devDependency-on-my-pkg", + "target": "my-lib", + "type": "static", + }, + ], + "newVersion": "1.0.0", + }, + "project-with-dependency-on-my-pkg": { + "currentVersion": "0.0.1", + "dependentProjects": [], + "newVersion": "1.0.0", + }, + "project-with-devDependency-on-my-pkg": { + "currentVersion": "0.0.1", + "dependentProjects": [], + "newVersion": "1.0.0", + }, + }, + } + `); + }); + + describe('not all given projects have pyproject.toml files', () => { + beforeEach(() => { + tree.delete('libs/my-lib/pyproject.toml'); + }); + + it(`should exit with code one and print guidance when not all of the given projects are appropriate for Python versioning`, async () => { + stubProcessExit = true; + + const outputSpy = vi.spyOn(output, 'error').mockImplementationOnce(() => { + return undefined as never; + }); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + + expect(outputSpy).toHaveBeenCalledWith({ + title: `The project "my-lib" does not have a pyproject.toml available at libs/my-lib/pyproject.toml. + +To fix this you will either need to add a pyproject.toml file at that location, or configure "release" within your nx.json to exclude "my-lib" from the current release group, or amend the packageRoot configuration to point to where the pyproject.toml should be.`, + }); + + outputSpy.mockRestore(); + expect(processExitSpy).toHaveBeenCalledWith(1); + + stubProcessExit = false; + }); + }); + + describe('package with mixed "prod" and "dev" dependencies', () => { + beforeEach(() => { + projectGraph = createWorkspaceWithPackageDependencies(tree, { + 'my-app': { + projectRoot: 'libs/my-app', + packageName: 'my-app', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-app/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib-1', + dependencyCollection: 'dependencies', + }, + { + projectName: 'my-lib-2', + dependencyCollection: 'dev', + }, + ], + }, + 'my-lib-1': { + projectRoot: 'libs/my-lib-1', + packageName: 'my-lib-1', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-lib-1/pyproject.toml', + localDependencies: [], + }, + 'my-lib-2': { + projectRoot: 'libs/my-lib-2', + packageName: 'my-lib-2', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-lib-2/pyproject.toml', + localDependencies: [], + }, + }); + }); + + it('should update local dependencies only where it needs to', async () => { + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + + expect(readPyprojectToml(tree, 'libs/my-app/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib-1": { + "develop": true, + "path": "../my-lib-1", + }, + }, + "group": { + "dev": { + "dependencies": { + "my-lib-2": { + "develop": true, + "path": "../my-lib-2", + }, + }, + }, + }, + "name": "my-app", + "version": "1.0.0", + }, + }, + } + `); + }); + }); + + describe('fixed release group', () => { + it(`should work with semver keywords and exact semver versions`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('1.0.0'); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'minor', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('1.1.0'); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'patch', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('1.1.1'); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '1.2.3', // exact version + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('1.2.3'); + }); + + it(`should apply the updated version to the projects, including updating dependents`, async () => { + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "1.0.0", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "1.0.0", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "1.0.0", + }, + }, + } + `); + }); + }); + + describe('independent release group', () => { + describe('specifierSource: prompt', () => { + it(`should appropriately prompt for each project independently and apply the version updates across all pyproject.toml files`, async () => { + enquirerMocks.prompt + // First project will be minor + .mockResolvedValueOnce({ specifier: 'minor' }) + // Next project will be patch + .mockResolvedValueOnce({ specifier: 'patch' }) + // Final project will be custom explicit version + .mockResolvedValueOnce({ specifier: 'custom' }) + .mockResolvedValueOnce({ specifier: '1.2.3' }); + + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ).tool.poetry.version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ).tool.poetry.version, + ).toEqual('0.0.1'); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '', // no specifier override set, each individual project will be prompted + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "0.1.0", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.2", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "1.2.3", + }, + }, + } + `); + }); + + it(`should respect an explicit user CLI specifier for all, even when projects are independent, and apply the version updates across all pyproject.toml files`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ).tool.poetry.version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ).tool.poetry.version, + ).toEqual('0.0.1'); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '4.5.6', // user CLI specifier override set, no prompting should occur + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "4.5.6", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "4.5.6", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "4.5.6", + }, + }, + } + `); + }); + + describe('updateDependentsOptions', () => { + it(`should not update dependents when filtering to a subset of projects by default`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['my-lib']], // version only my-lib + projectGraph, + specifier: '9.9.9', // user CLI specifier override set, no prompting should occur + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + }); + + it(`should not update dependents when filtering to a subset of projects by default, if "updateDependents" is set to "never"`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['my-lib']], // version only my-lib + projectGraph, + specifier: '9.9.9', // user CLI specifier override set, no prompting should occur + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'never', + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + }); + + it(`should update dependents even when filtering to a subset of projects which do not include those dependents, if "updateDependents" is "auto"`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.1", + }, + }, + } + `); + + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['my-lib']], // version only my-lib + projectGraph, + specifier: '9.9.9', // user CLI specifier override set, no prompting should occur + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'auto', + }); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "0.0.2", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "0.0.2", + }, + }, + } + `); + }); + }); + }); + }); + + describe('leading v in version', () => { + it(`should strip a leading v from the provided specifier`, async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'v8.8.8', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "8.8.8", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "8.8.8", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "8.8.8", + }, + }, + } + `); + }); + }); + + describe('dependent version prefix', () => { + beforeEach(() => { + projectGraph = createWorkspaceWithPackageDependencies(tree, { + 'my-lib': { + projectRoot: 'libs/my-lib', + packageName: 'my-lib', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-lib/pyproject.toml', + localDependencies: [], + }, + 'project-with-dependency-on-my-pkg': { + projectRoot: 'libs/project-with-dependency-on-my-pkg', + packageName: 'project-with-dependency-on-my-pkg', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dependencies', + }, + ], + }, + 'project-with-devDependency-on-my-pkg': { + projectRoot: 'libs/project-with-devDependency-on-my-pkg', + packageName: 'project-with-devDependency-on-my-pkg', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dev', + }, + ], + }, + 'another-project-with-devDependency-on-my-pkg': { + projectRoot: 'libs/another-project-with-devDependency-on-my-pkg', + packageName: 'another-project-with-devDependency-on-my-pkg', + version: '0.0.1', + pyprojectTomlPath: + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dev', + }, + ], + }, + }); + }); + + it('should work with an empty prefix', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: '', + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "another-project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + }); + + it('should work with a ^ prefix', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: '^', + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "another-project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + }); + + it('should work with a ~ prefix', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: '~', + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "another-project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + }); + + it('should respect any existing prefix when set to "auto"', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: 'auto', + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "another-project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + }); + + it('should use the behavior of "auto" by default', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: undefined, + }); + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/another-project-with-devDependency-on-my-pkg/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + }, + }, + "name": "another-project-with-devDependency-on-my-pkg", + "version": "9.9.9", + }, + }, + } + `); + }); + + it(`should exit with code one and print guidance for invalid prefix values`, async () => { + stubProcessExit = true; + + const outputSpy = vi.spyOn(output, 'error').mockImplementationOnce(() => { + return undefined as never; + }); + + await releaseVersionGenerator(tree, { + projects: Object.values(projectGraph.nodes), // version all projects + projectGraph, + specifier: 'major', + currentVersionResolver: 'disk', + releaseGroup: createReleaseGroup('fixed'), + versionPrefix: '$' as never, + }); + + expect(outputSpy).toHaveBeenCalledWith({ + title: `Invalid value for version.generatorOptions.versionPrefix: "$" + +Valid values are: "auto", "", "~", "^", "="`, + }); + + outputSpy.mockRestore(); + expect(processExitSpy).toHaveBeenCalledWith(1); + + stubProcessExit = false; + }); + }); + + describe('transitive updateDependents', () => { + beforeEach(() => { + projectGraph = createWorkspaceWithPackageDependencies(tree, { + 'my-lib': { + projectRoot: 'libs/my-lib', + packageName: 'my-lib', + version: '0.0.1', + pyprojectTomlPath: 'libs/my-lib/pyproject.toml', + localDependencies: [], + }, + 'project-with-dependency-on-my-lib': { + projectRoot: 'libs/project-with-dependency-on-my-lib', + packageName: 'project-with-dependency-on-my-lib', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-dependency-on-my-lib/pyproject.toml', + localDependencies: [ + { + projectName: 'my-lib', + dependencyCollection: 'dependencies', + }, + ], + }, + 'project-with-transitive-dependency-on-my-lib': { + projectRoot: 'libs/project-with-transitive-dependency-on-my-lib', + packageName: 'project-with-transitive-dependency-on-my-lib', + version: '0.0.1', + pyprojectTomlPath: + 'libs/project-with-transitive-dependency-on-my-lib/pyproject.toml', + localDependencies: [ + { + // Depends on my-lib via the project-with-dependency-on-my-lib + projectName: 'project-with-dependency-on-my-lib', + dependencyCollection: 'dev', + }, + ], + }, + }); + }); + + it('should not update transitive dependents when updateDependents is set to "never" and the transitive dependents are not in the same batch', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-transitive-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "project-with-dependency-on-my-lib": { + "develop": true, + "path": "../project-with-dependency-on-my-lib", + }, + }, + }, + }, + "name": "project-with-transitive-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + + // It should not include transitive dependents in the versionData because we are filtering to only my-lib and updateDependents is set to "never" + expect( + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['my-lib']], // version only my-lib + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'never', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "my-lib": { + "currentVersion": "0.0.1", + "dependentProjects": [], + "newVersion": "9.9.9", + }, + }, + } + `); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + // The version of project-with-dependency-on-my-lib is untouched because it is not in the same batch as my-lib and updateDependents is set to "never" + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + + // The version of project-with-transitive-dependency-on-my-lib is untouched because it is not in the same batch as my-lib and updateDependents is set to "never" + expect( + readPyprojectToml( + tree, + 'libs/project-with-transitive-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "project-with-dependency-on-my-lib": { + "develop": true, + "path": "../project-with-dependency-on-my-lib", + }, + }, + }, + }, + "name": "project-with-transitive-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + }); + + it('should always update transitive dependents when updateDependents is set to "auto"', async () => { + expect( + readPyprojectToml(tree, 'libs/my-lib/pyproject.toml').tool.poetry + .version, + ).toEqual('0.0.1'); + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + expect( + readPyprojectToml( + tree, + 'libs/project-with-transitive-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "project-with-dependency-on-my-lib": { + "develop": true, + "path": "../project-with-dependency-on-my-lib", + }, + }, + }, + }, + "name": "project-with-transitive-dependency-on-my-lib", + "version": "0.0.1", + }, + }, + } + `); + + // It should include the appropriate versionData for transitive dependents + expect( + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['my-lib']], // version only my-lib + projectGraph, + specifier: '9.9.9', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'auto', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "my-lib": { + "currentVersion": "0.0.1", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "0.0.1", + "source": "project-with-dependency-on-my-lib", + "target": "my-lib", + "type": "static", + }, + ], + "newVersion": "9.9.9", + }, + "project-with-dependency-on-my-lib": { + "currentVersion": "0.0.1", + "dependentProjects": [ + { + "dependencyCollection": "devDependencies", + "groupKey": "dev", + "rawVersionSpec": "0.0.1", + "source": "project-with-transitive-dependency-on-my-lib", + "target": "project-with-dependency-on-my-lib", + "type": "static", + }, + ], + "newVersion": "0.0.2", + }, + "project-with-transitive-dependency-on-my-lib": { + "currentVersion": "0.0.1", + "dependentProjects": [], + "newVersion": "0.0.2", + }, + }, + } + `); + + expect(readPyprojectToml(tree, 'libs/my-lib/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "name": "my-lib", + "version": "9.9.9", + }, + }, + } + `); + + // The version of project-with-dependency-on-my-lib gets bumped by a patch number and the dependencies reference is updated to the new version of my-lib + expect( + readPyprojectToml( + tree, + 'libs/project-with-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "my-lib": { + "develop": true, + "path": "../my-lib", + }, + }, + "name": "project-with-dependency-on-my-lib", + "version": "0.0.2", + }, + }, + } + `); + + // The version of project-with-transitive-dependency-on-my-lib gets bumped by a patch number and the devDependencies reference is updated to the new version of project-with-dependency-on-my-lib because of the transitive dependency on my-lib + expect( + readPyprojectToml( + tree, + 'libs/project-with-transitive-dependency-on-my-lib/pyproject.toml', + ), + ).toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "group": { + "dev": { + "dependencies": { + "project-with-dependency-on-my-lib": { + "develop": true, + "path": "../project-with-dependency-on-my-lib", + }, + }, + }, + }, + "name": "project-with-transitive-dependency-on-my-lib", + "version": "0.0.2", + }, + }, + } + `); + }); + }); + + describe('circular dependencies', () => { + beforeEach(() => { + // package-a <-> package-b + projectGraph = createWorkspaceWithPackageDependencies(tree, { + 'package-a': { + projectRoot: 'packages/package-a', + packageName: 'package-a', + version: '1.0.0', + pyprojectTomlPath: 'packages/package-a/pyproject.toml', + localDependencies: [ + { + projectName: 'package-b', + dependencyCollection: 'dependencies', + }, + ], + }, + 'package-b': { + projectRoot: 'packages/package-b', + packageName: 'package-b', + version: '1.0.0', + pyprojectTomlPath: 'packages/package-b/pyproject.toml', + localDependencies: [ + { + projectName: 'package-a', + dependencyCollection: 'dependencies', + }, + ], + }, + }); + }); + + describe("updateDependents: 'never'", () => { + it('should allow versioning of circular dependencies when not all projects are included in the current batch', async () => { + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "1.0.0", + }, + }, + } + `); + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.0", + }, + }, + } + `); + + expect( + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['package-a']], // version only package-a + projectGraph, + specifier: '2.0.0', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'never', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "package-a": { + "currentVersion": "1.0.0", + "dependentProjects": [], + "newVersion": "2.0.0", + }, + }, + } + `); + + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "2.0.0", + }, + }, + } + `); + // package-b is unchanged + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.0", + }, + }, + } + `); + }); + + it('should allow versioning of circular dependencies when all projects are included in the current batch', async () => { + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "1.0.0", + }, + }, + } + `); + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.0", + }, + }, + } + `); + + expect( + await releaseVersionGenerator(tree, { + // version both packages + projects: [ + projectGraph.nodes['package-a'], + projectGraph.nodes['package-b'], + ], + + projectGraph, + specifier: '2.0.0', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'never', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "package-a": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", + "source": "package-b", + "target": "package-a", + "type": "static", + }, + ], + "newVersion": "2.0.0", + }, + "package-b": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", + "source": "package-a", + "target": "package-b", + "type": "static", + }, + ], + "newVersion": "2.0.0", + }, + }, + } + `); + + // Both the version of package-a, and the dependency on package-b are updated to 2.0.0 + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "2.0.0", + }, + }, + } + `); + // Both the version of package-b, and the dependency on package-a are updated to 2.0.0 + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "2.0.0", + }, + }, + } + `); + }); + }); + + describe("updateDependents: 'auto'", () => { + it('should allow versioning of circular dependencies when not all projects are included in the current batch', async () => { + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "1.0.0", + }, + }, + } + `); + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.0", + }, + }, + } + `); + + expect( + await releaseVersionGenerator(tree, { + projects: [projectGraph.nodes['package-a']], // version only package-a + projectGraph, + specifier: '2.0.0', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'auto', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "package-a": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", + "source": "package-b", + "target": "package-a", + "type": "static", + }, + ], + "newVersion": "2.0.0", + }, + "package-b": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "groupKey": undefined, + "rawVersionSpec": "1.0.0", + "source": "package-a", + "target": "package-b", + "type": "static", + }, + ], + "newVersion": "1.0.1", + }, + }, + } + `); + + // The version of package-a has been updated to 2.0.0, and the dependency on package-b has been updated to 1.0.1 + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "2.0.0", + }, + }, + } + `); + // The version of package-b has been patched to 1.0.1, and the dependency on package-a has been updated to 2.0.0 + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.1", + }, + }, + } + `); + }); + + it('should allow versioning of circular dependencies when all projects are included in the current batch', async () => { + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "1.0.0", + }, + }, + } + `); + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "1.0.0", + }, + }, + } + `); + + expect( + await releaseVersionGenerator(tree, { + // version both packages + projects: [ + projectGraph.nodes['package-a'], + projectGraph.nodes['package-b'], + ], + projectGraph, + specifier: '2.0.0', + currentVersionResolver: 'disk', + specifierSource: 'prompt', + releaseGroup: createReleaseGroup('independent'), + updateDependents: 'auto', + }), + ).toMatchInlineSnapshot(` + { + "callback": [Function], + "data": { + "package-a": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", + "source": "package-b", + "target": "package-a", + "type": "static", + }, + ], + "newVersion": "2.0.0", + }, + "package-b": { + "currentVersion": "1.0.0", + "dependentProjects": [ + { + "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", + "source": "package-a", + "target": "package-b", + "type": "static", + }, + ], + "newVersion": "2.0.0", + }, + }, + } + `); + + // Both the version of package-a, and the dependency on package-b are updated to 2.0.0 + expect(readPyprojectToml(tree, 'packages/package-a/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-b": { + "develop": true, + "path": "../package-b", + }, + }, + "name": "package-a", + "version": "2.0.0", + }, + }, + } + `); + // Both the version of package-b, and the dependency on package-a are updated to 2.0.0 + expect(readPyprojectToml(tree, 'packages/package-b/pyproject.toml')) + .toMatchInlineSnapshot(` + { + "tool": { + "poetry": { + "dependencies": { + "package-a": { + "develop": true, + "path": "../package-a", + }, + }, + "name": "package-b", + "version": "2.0.0", + }, + }, + } + `); + }); + }); + }); +}); + +function createReleaseGroup( + relationship: ReleaseGroupWithName['projectsRelationship'], + partialGroup: Partial = {}, +): ReleaseGroupWithName { + return { + name: 'myReleaseGroup', + releaseTagPattern: '{projectName}@v{version}', + ...partialGroup, + projectsRelationship: relationship, + } as ReleaseGroupWithName; +} + +async function mock(mockedUri, stub) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { Module } = (await import('module')) as any; + + Module._load_original = Module._load; + Module._load = (uri, parent) => { + if (uri === mockedUri) return stub; + return Module._load_original(uri, parent); + }; +} diff --git a/packages/nx-python/src/generators/release-version/release-version.ts b/packages/nx-python/src/generators/release-version/release-version.ts new file mode 100644 index 0000000..db2e643 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/release-version.ts @@ -0,0 +1,1045 @@ +import { + ProjectGraphProjectNode, + Tree, + formatFiles, + joinPathFragments, + output, +} from '@nx/devkit'; +import chalk from 'chalk'; +import { exec } from 'node:child_process'; +import { rm } from 'node:fs/promises'; +import { IMPLICIT_DEFAULT_RELEASE_GROUP } from 'nx/src/command-line/release/config/config'; +import { + GroupVersionPlan, + ProjectsVersionPlan, +} from 'nx/src/command-line/release/config/version-plans'; +import { + getFirstGitCommit, + getLatestGitTagForPattern, +} from 'nx/src/command-line/release/utils/git'; +import { + resolveSemverSpecifierFromConventionalCommits, + resolveSemverSpecifierFromPrompt, +} from 'nx/src/command-line/release/utils/resolve-semver-specifier'; +import { isValidSemverSpecifier } from 'nx/src/command-line/release/utils/semver'; +import { + ReleaseVersionGeneratorResult, + VersionData, + deriveNewSemverVersion, + validReleaseVersionPrefixes, +} from 'nx/src/command-line/release/version'; +import { interpolate } from 'nx/src/tasks-runner/utils'; +import ora from 'ora'; +import { ReleaseType, gt, inc, prerelease } from 'semver'; +import { ReleaseVersionGeneratorSchema } from './schema'; +import { + LocalPackageDependency, + resolveLocalPackageDependencies, +} from './utils/resolve-local-package-dependencies'; +import { sortProjectsTopologically } from './utils/sort-projects-topologically'; +import { + readPyprojectToml, + runPoetry, + writePyprojectToml, +} from '../../executors/utils/poetry'; +import { extractDependencyVersion } from './utils/package'; +import path, { dirname } from 'node:path'; + +export async function releaseVersionGenerator( + tree: Tree, + options: ReleaseVersionGeneratorSchema, +): Promise { + let logger: ProjectLogger | undefined; + + const poetryLocks: string[] = []; + + try { + const versionData: VersionData = {}; + + // If the user provided a specifier, validate that it is valid semver or a relative semver keyword + if (options.specifier) { + if (!isValidSemverSpecifier(options.specifier)) { + throw new Error( + `The given version specifier "${options.specifier}" is not valid. You provide an exact version or a valid semver keyword such as "major", "minor", "patch", etc.`, + ); + } + // The node semver library classes a leading `v` as valid, but we want to ensure it is not present in the final version + options.specifier = options.specifier.replace(/^v/, ''); + } + + if ( + options.versionPrefix && + validReleaseVersionPrefixes.indexOf(options.versionPrefix) === -1 + ) { + throw new Error( + `Invalid value for version.generatorOptions.versionPrefix: "${ + options.versionPrefix + }" + +Valid values are: ${validReleaseVersionPrefixes + .map((s) => `"${s}"`) + .join(', ')}`, + ); + } + + if (options.firstRelease) { + // always use disk as a fallback for the first release + options.fallbackCurrentVersionResolver = 'disk'; + } + + // Set default for updateDependents + const updateDependents = options.updateDependents ?? 'never'; + const updateDependentsBump = 'patch'; + + // Sort the projects topologically if update dependents is enabled + // TODO: maybe move this sorting to the command level? + const projects = + updateDependents === 'never' + ? options.projects + : sortProjectsTopologically(options.projectGraph, options.projects); + const projectToDependencyBumps = new Map>(); + + const resolvePackageRoot = createResolvePackageRoot(options.packageRoot); + + // Resolve any custom package roots for each project upfront as they will need to be reused during dependency resolution + const projectNameToPackageRootMap = new Map(); + for (const project of projects) { + projectNameToPackageRootMap.set( + project.name, + resolvePackageRoot(project), + ); + } + + let currentVersion: string | undefined = undefined; + let currentVersionResolvedFromFallback = false; + + // only used for options.currentVersionResolver === 'git-tag', but + // must be declared here in order to reuse it for additional projects + let latestMatchingGitTag: + | { tag: string; extractedVersion: string } + | null + | undefined = undefined; + + // if specifier is undefined, then we haven't resolved it yet + // if specifier is null, then it has been resolved and no changes are necessary + let specifier: string | null | undefined = options.specifier + ? options.specifier + : undefined; + + const deleteVersionPlanCallbacks: (( + dryRun?: boolean, + ) => Promise)[] = []; + + // If the user has set the logUnchangedProjects option to false, we will not print any logs for projects that have no changes. + const logUnchangedProjects = options.logUnchangedProjects ?? true; + + for (const project of projects) { + const projectName = project.name; + const packageRoot = projectNameToPackageRootMap.get(projectName); + if (!packageRoot) { + throw new Error( + `The project "${projectName}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`, + ); + } + + const pyprojectTomlPath = joinPathFragments( + packageRoot, + 'pyproject.toml', + ); + if (!tree.exists(pyprojectTomlPath)) { + throw new Error( + `The project "${projectName}" does not have a pyproject.toml available at ${pyprojectTomlPath}. + +To fix this you will either need to add a pyproject.toml file at that location, or configure "release" within your nx.json to exclude "${projectName}" from the current release group, or amend the packageRoot configuration to point to where the pyproject.toml should be.`, + ); + } + + const color = getColor(projectName); + logger = new ProjectLogger(projectName, color); + + const pyprojectToml = readPyprojectToml(tree, pyprojectTomlPath); + logger.buffer( + `🔍 Reading data for package "${pyprojectToml.tool.poetry.name}" from ${pyprojectTomlPath}`, + ); + + const { name: packageName, version: currentVersionFromDisk } = + pyprojectToml.tool.poetry; + + switch (options.currentVersionResolver) { + case 'registry': { + /** + * If the currentVersionResolver is set to registry, and the projects are not independent, we only want to make the request once for the whole batch of projects. + * For independent projects, we need to make a request for each project individually as they will most likely have different versions. + */ + if ( + !currentVersion || + options.releaseGroup.projectsRelationship === 'independent' + ) { + const spinner = ora( + `${Array.from(new Array(projectName.length + 3)).join( + ' ', + )}Resolving the current version from pip registry`, + ); + spinner.color = + color.spinnerColor as (typeof colors)[number]['spinnerColor']; + spinner.start(); + + try { + // Must be non-blocking async to allow spinner to render + currentVersion = await new Promise((resolve, reject) => { + exec( + `pip index versions ${packageName}`, + (error, stdout, stderr) => { + if (error) { + return reject(error); + } + if (stderr) { + return reject(stderr); + } + return resolve( + stdout + .trim() + .match(new RegExp(`${packageName} \\((.*)\\)`))[1], + ); + }, + ); + }); + + spinner.stop(); + + logger.buffer( + `📄 Resolved the current version as ${currentVersion} from pip registry`, + ); + } catch (e) { + spinner.stop(); + + if (options.fallbackCurrentVersionResolver === 'disk') { + logger.buffer( + `📄 Unable to resolve the current version from pip registry. Falling back to the version on disk of ${currentVersionFromDisk}`, + ); + currentVersion = currentVersionFromDisk; + currentVersionResolvedFromFallback = true; + } else { + throw new Error( + `Unable to resolve the current version from pip registry. Please ensure that the package exists in the registry in order to use the "registry" currentVersionResolver. 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 the registry lookup fails.`, + ); + } + } + } else { + if (currentVersionResolvedFromFallback) { + logger.buffer( + `📄 Using the current version ${currentVersion} already resolved from disk fallback.`, + ); + } else { + logger.buffer( + `📄 Using the current version ${currentVersion} already resolved from pip registry`, + ); + } + } + break; + } + case 'disk': + currentVersion = currentVersionFromDisk; + if (!currentVersion) { + throw new Error( + `Unable to determine the current version for project "${project.name}" from ${pyprojectTomlPath}`, + ); + } + logger.buffer( + `📄 Resolved the current version as ${currentVersion} from ${pyprojectTomlPath}`, + ); + break; + case 'git-tag': { + if ( + !currentVersion || + // We always need to independently resolve the current version from git tag per project if the projects are independent + options.releaseGroup.projectsRelationship === 'independent' + ) { + const releaseTagPattern = options.releaseGroup.releaseTagPattern; + latestMatchingGitTag = await getLatestGitTagForPattern( + releaseTagPattern, + { + projectName: project.name, + }, + ); + if (!latestMatchingGitTag) { + if (options.fallbackCurrentVersionResolver === 'disk') { + logger.buffer( + `📄 Unable to resolve the current version from git tag using pattern "${releaseTagPattern}". Falling back to the version on disk of ${currentVersionFromDisk}`, + ); + currentVersion = currentVersionFromDisk; + currentVersionResolvedFromFallback = true; + } else { + throw new Error( + `No git tags matching pattern "${releaseTagPattern}" for project "${project.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.`, + ); + } + } else { + currentVersion = latestMatchingGitTag.extractedVersion; + logger.buffer( + `📄 Resolved the current version as ${currentVersion} from git tag "${latestMatchingGitTag.tag}".`, + ); + } + } else { + if (currentVersionResolvedFromFallback) { + logger.buffer( + `📄 Using the current version ${currentVersion} already resolved from disk fallback.`, + ); + } else { + logger.buffer( + // In this code path we know that latestMatchingGitTag is defined, because we are not relying on the fallbackCurrentVersionResolver, so we can safely use the non-null assertion operator + `📄 Using the current version ${currentVersion} already resolved from git tag "${ + latestMatchingGitTag.tag + }".`, + ); + } + } + break; + } + default: + throw new Error( + `Invalid value for options.currentVersionResolver: ${options.currentVersionResolver}`, + ); + } + + if (options.specifier) { + logger.buffer( + `📄 Using the provided version specifier "${options.specifier}".`, + ); + // The user is forcibly overriding whatever specifierSource they had otherwise set by imperatively providing a specifier + options.specifierSource = 'prompt'; + } + + /** + * If we are versioning independently then we always need to determine the specifier for each project individually, except + * for the case where the user has provided an explicit specifier on the command. + * + * Otherwise, if versioning the projects together we only need to perform this logic if the specifier is still unset from + * previous iterations of the loop. + * + * NOTE: In the case that we have previously determined via conventional commits that no changes are necessary, the specifier + * will be explicitly set to `null`, so that is why we only check for `undefined` explicitly here. + */ + if ( + specifier === undefined || + (options.releaseGroup.projectsRelationship === 'independent' && + !options.specifier) + ) { + const specifierSource = options.specifierSource; + switch (specifierSource) { + case 'conventional-commits': { + if (options.currentVersionResolver !== 'git-tag') { + throw new Error( + `Invalid currentVersionResolver "${options.currentVersionResolver}" provided for release group "${options.releaseGroup.name}". Must be "git-tag" when "specifierSource" is "conventional-commits"`, + ); + } + + const affectedProjects = + options.releaseGroup.projectsRelationship === 'independent' + ? [projectName] + : 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 + : options.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.`, + ); + } + + specifier = await resolveSemverSpecifierFromConventionalCommits( + previousVersionRef, + options.projectGraph, + affectedProjects, + options.conventionalCommitsConfig, + ); + + if (!specifier) { + if ( + 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`, + ); + break; + } + logger.buffer( + `🚫 No changes were detected using git history and the conventional commits standard.`, + ); + break; + } + + // 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. + if (prerelease(currentVersion ?? '')) { + specifier = 'prerelease'; + logger.buffer( + `📄 Resolved the specifier as "${specifier}" since the current version is a prerelease.`, + ); + } else { + let extraText = ''; + if (options.preid && !specifier.startsWith('pre')) { + specifier = `pre${specifier}`; + extraText = `, combined with your given preid "${options.preid}"`; + } + logger.buffer( + `📄 Resolved the specifier as "${specifier}" using git history and the conventional commits standard${extraText}.`, + ); + } + break; + } + case 'prompt': { + // 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 ( + options.releaseGroup.name === IMPLICIT_DEFAULT_RELEASE_GROUP + ) { + return log; + } + return `${log} within release group "${options.releaseGroup.name}"`; + }; + if (options.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}"`, + )}?`, + ); + } else { + specifier = await resolveSemverSpecifierFromPrompt( + `${maybeLogReleaseGroup( + `What kind of change is this for the ${projects.length} matched projects(s)`, + )}?`, + `${maybeLogReleaseGroup( + `What is the exact version for the ${projects.length} matched project(s)`, + )}?`, + ); + } + break; + } + case 'version-plans': { + if (!options.releaseGroup.versionPlans) { + if ( + options.releaseGroup.name === IMPLICIT_DEFAULT_RELEASE_GROUP + ) { + throw new Error( + `Invalid specifierSource "version-plans" provided. To enable version plans, set the "release.versionPlans" configuration option to "true" in nx.json.`, + ); + } else { + throw new Error( + `Invalid specifierSource "version-plans" provided. To enable version plans for release group "${options.releaseGroup.name}", set the "versionPlans" configuration option to "true" within the release group configuration in nx.json.`, + ); + } + } + + if (options.releaseGroup.projectsRelationship === 'independent') { + specifier = ( + options.releaseGroup + .resolvedVersionPlans as ProjectsVersionPlan[] + ).reduce((spec: ReleaseType, plan: ProjectsVersionPlan) => { + if (!spec) { + return plan.projectVersionBumps[projectName]; + } + if (plan.projectVersionBumps[projectName]) { + const prevNewVersion = inc(currentVersion, spec); + const nextNewVersion = inc( + currentVersion, + plan.projectVersionBumps[projectName], + ); + return gt(nextNewVersion, prevNewVersion) + ? plan.projectVersionBumps[projectName] + : spec; + } + return spec; + }, null); + } else { + specifier = ( + options.releaseGroup.resolvedVersionPlans as GroupVersionPlan[] + ).reduce((spec: ReleaseType, plan: GroupVersionPlan) => { + if (!spec) { + return plan.groupVersionBump; + } + + const prevNewVersion = inc(currentVersion, spec); + const nextNewVersion = inc( + currentVersion, + plan.groupVersionBump, + ); + return gt(nextNewVersion, prevNewVersion) + ? plan.groupVersionBump + : spec; + }, null); + } + + if (!specifier) { + if ( + 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`, + ); + } else { + specifier = null; + logger.buffer( + `🚫 No changes were detected within version plans.`, + ); + } + } else { + logger.buffer( + `📄 Resolved the specifier as "${specifier}" using version plans.`, + ); + } + + if (options.deleteVersionPlans) { + (options.releaseGroup.resolvedVersionPlans || []).forEach((p) => { + deleteVersionPlanCallbacks.push(async (dryRun?: boolean) => { + if (!dryRun) { + await rm(p.absolutePath, { recursive: true, force: true }); + // the relative path is easier to digest, so use that for + // git operations and logging + return [p.relativePath]; + } else { + return []; + } + }); + }); + } + + break; + } + default: + throw new Error( + `Invalid specifierSource "${specifierSource}" provided. Must be one of "prompt", "conventional-commits" or "version-plans".`, + ); + } + } + + // Resolve any local package dependencies for this project (before applying the new version or updating the versionData) + const localPackageDependencies = resolveLocalPackageDependencies( + tree, + options.projectGraph, + projects, + projectNameToPackageRootMap, + resolvePackageRoot, + // includeAll when the release group is independent, as we may be filtering to a specific subset of projects, but we still want to update their dependents + options.releaseGroup.projectsRelationship === 'independent', + ); + + const allDependentProjects = Object.values(localPackageDependencies) + .flat() + .filter((localPackageDependency) => { + return localPackageDependency.target === project.name; + }); + + const includeTransitiveDependents = updateDependents === 'auto'; + const transitiveLocalPackageDependents: LocalPackageDependency[] = []; + if (includeTransitiveDependents) { + for (const directDependent of allDependentProjects) { + // Look through localPackageDependencies to find any which have a target on the current dependent + for (const localPackageDependency of Object.values( + localPackageDependencies, + ).flat()) { + if (localPackageDependency.target === directDependent.source) { + transitiveLocalPackageDependents.push(localPackageDependency); + } + } + } + } + + const dependentProjectsInCurrentBatch = []; + const dependentProjectsOutsideCurrentBatch = []; + // Track circular dependencies using value of project1:project2 + const circularDependencies = new Set(); + + for (const dependentProject of allDependentProjects) { + // Track circular dependencies (add both directions for easy look up) + if (dependentProject.target === projectName) { + circularDependencies.add( + `${dependentProject.source}:${dependentProject.target}`, + ); + circularDependencies.add( + `${dependentProject.target}:${dependentProject.source}`, + ); + } + + let isInCurrentBatch = options.projects.some( + (project) => project.name === dependentProject.source, + ); + + // For version-plans, we don't just need to consider the current batch of projects, but also the ones that are actually being updated as part of the plan file(s) + if (isInCurrentBatch && options.specifierSource === 'version-plans') { + isInCurrentBatch = ( + options.releaseGroup.resolvedVersionPlans || [] + ).some((plan) => { + if ('projectVersionBumps' in plan) { + return plan.projectVersionBumps[dependentProject.source]; + } + return true; + }); + } + + if (!isInCurrentBatch) { + dependentProjectsOutsideCurrentBatch.push(dependentProject); + } else { + dependentProjectsInCurrentBatch.push(dependentProject); + } + } + + // If not always updating dependents (when they don't already appear in the batch itself), print a warning to the user about what is being skipped and how to change it + if (updateDependents === 'never') { + if (dependentProjectsOutsideCurrentBatch.length > 0) { + let logMsg = `⚠️ Warning, the following packages depend on "${project.name}"`; + const reason = + options.specifierSource === 'version-plans' + ? 'because they are not referenced in any version plans' + : 'via --projects'; + if (options.releaseGroup.name === IMPLICIT_DEFAULT_RELEASE_GROUP) { + logMsg += ` but have been filtered out ${reason}, and therefore will not be updated:`; + } else { + logMsg += ` but are either not part of the current release group "${options.releaseGroup.name}", or have been filtered out ${reason}, and therefore will not be updated:`; + } + const indent = Array.from(new Array(projectName.length + 4)) + .map(() => ' ') + .join(''); + logMsg += `\n${dependentProjectsOutsideCurrentBatch + .map((dependentProject) => `${indent}- ${dependentProject.source}`) + .join('\n')}`; + logMsg += `\n${indent}=> You can adjust this behavior by setting \`version.generatorOptions.updateDependents\` to "auto"`; + logger.buffer(logMsg); + } + } + + if (!currentVersion) { + throw new Error( + `The current version for project "${project.name}" could not be resolved. Please report this on https://github.com/nrwl/nx`, + ); + } + + versionData[projectName] = { + currentVersion, + newVersion: null, // will stay as null in the final result in the case that no changes are detected + dependentProjects: + updateDependents === 'auto' + ? allDependentProjects.map(({ groupKey, ...rest }) => rest) + : dependentProjectsInCurrentBatch.map( + ({ groupKey, ...rest }) => rest, + ), + }; + + if (!specifier) { + logger.buffer( + `🚫 Skipping versioning "${pyprojectToml.tool.poetry.name}" as no changes were detected.`, + ); + // Print the buffered logs for this unchanged project, as long as the user has not explicitly disabled this behavior + if (logUnchangedProjects) { + logger.flush(); + } + continue; + } + + const newVersion = deriveNewSemverVersion( + currentVersion, + specifier, + options.preid, + ); + versionData[projectName].newVersion = newVersion; + + poetryLocks.push(dirname(pyprojectTomlPath)); + + writePyprojectToml(tree, pyprojectTomlPath, { + ...pyprojectToml, + tool: { + ...pyprojectToml.tool, + poetry: { + ...pyprojectToml.tool.poetry, + version: newVersion, + }, + }, + }); + + logger.buffer( + `✍️ New version ${newVersion} written to ${pyprojectTomlPath}`, + ); + + if (allDependentProjects.length > 0) { + const totalProjectsToUpdate = + updateDependents === 'auto' + ? allDependentProjects.length + + transitiveLocalPackageDependents.length - + // There are two entries per circular dep + circularDependencies.size / 2 + : dependentProjectsInCurrentBatch.length; + if (totalProjectsToUpdate > 0) { + logger.buffer( + `✍️ Applying new version ${newVersion} to ${totalProjectsToUpdate} ${ + totalProjectsToUpdate > 1 + ? 'packages which depend' + : 'package which depends' + } on ${project.name}`, + ); + } + } + + const updateDependentProjectAndAddToVersionData = ({ + dependentProject, + dependencyPackageName, + newDependencyVersion, + forceVersionBump, + }: { + dependentProject: LocalPackageDependency; + dependencyPackageName: string; + newDependencyVersion: string; + forceVersionBump: 'major' | 'minor' | 'patch' | false; + }) => { + const updatedPyprojectFilePath = joinPathFragments( + projectNameToPackageRootMap.get(dependentProject.source), + 'pyproject.toml', + ); + + poetryLocks.push(dirname(updatedPyprojectFilePath)); + + const updatedPyproject = readPyprojectToml( + tree, + updatedPyprojectFilePath, + ); + + if (!updatedPyproject?.tool?.poetry) { + return; + } + + // Auto (i.e.infer existing) by default + let versionPrefix = options.versionPrefix ?? 'auto'; + const currentDependencyVersion = + dependentProject.dependencyCollection === 'dependencies' + ? extractDependencyVersion( + tree, + projectNameToPackageRootMap.get(dependentProject.source), + updatedPyproject.tool.poetry.dependencies, + dependencyPackageName, + ) + : extractDependencyVersion( + tree, + projectNameToPackageRootMap.get(dependentProject.source), + updatedPyproject.tool.poetry.group[dependentProject.groupKey] + ?.dependencies, + dependencyPackageName, + ); + + const currentPackageVersion = + updatedPyproject.tool.poetry.version ?? null; + if (!currentPackageVersion) { + if (forceVersionBump) { + // Look up any dependent projects from the transitiveLocalPackageDependents list + const transitiveDependentProjects = + transitiveLocalPackageDependents.filter( + (localPackageDependency) => + localPackageDependency.target === dependentProject.source, + ); + versionData[dependentProject.source] = { + currentVersion: currentPackageVersion, + newVersion: currentDependencyVersion, + dependentProjects: transitiveDependentProjects, + }; + } + writePyprojectToml(tree, updatedPyprojectFilePath, updatedPyproject); + return; + } + + // For auto, we infer the prefix based on the current version of the dependent + if (versionPrefix === 'auto') { + versionPrefix = ''; // we don't want to end up printing auto + if (currentDependencyVersion) { + const prefixMatch = currentDependencyVersion.match(/^[~^]/); + if (prefixMatch) { + versionPrefix = + prefixMatch[0] as ReleaseVersionGeneratorSchema['versionPrefix']; + } else { + versionPrefix = ''; + } + } + } + + // Apply the new version of the dependency to the dependent (if not preserving locally linked package protocols) + // TODO + const shouldUpdateDependency = + !options.preserveLocalDependencyProtocols; + if (shouldUpdateDependency) { + const newDepVersion = `${versionPrefix}${newDependencyVersion}`; + if (dependentProject.dependencyCollection === 'dependencies') { + const mainGroup = updatedPyproject.tool.poetry.dependencies; + + if (typeof mainGroup[dependencyPackageName] === 'string') { + mainGroup[dependencyPackageName] = newDepVersion; + } else if (mainGroup[dependencyPackageName].version) { + mainGroup[dependencyPackageName].version = newDepVersion; + } + } else { + const group = + updatedPyproject.tool.poetry.group[dependentProject.groupKey] + ?.dependencies; + + if (typeof group[dependencyPackageName] === 'string') { + group[dependencyPackageName] = newDepVersion; + } else if (group[dependencyPackageName].version) { + group[dependencyPackageName].version = newDepVersion; + } + } + } + + // Bump the dependent's version if applicable and record it in the version data + if (forceVersionBump) { + const newPackageVersion = deriveNewSemverVersion( + currentPackageVersion, + forceVersionBump, + options.preid, + ); + updatedPyproject.tool.poetry.version = newPackageVersion; + + // Look up any dependent projects from the transitiveLocalPackageDependents list + const transitiveDependentProjects = + transitiveLocalPackageDependents.filter( + (localPackageDependency) => + localPackageDependency.target === dependentProject.source, + ); + + versionData[dependentProject.source] = { + currentVersion: currentPackageVersion, + newVersion: newPackageVersion, + dependentProjects: transitiveDependentProjects, + }; + } + + writePyprojectToml(tree, updatedPyprojectFilePath, updatedPyproject); + }; + + for (const dependentProject of dependentProjectsInCurrentBatch) { + if (projectToDependencyBumps.has(dependentProject.source)) { + const dependencyBumps = projectToDependencyBumps.get( + dependentProject.source, + ); + dependencyBumps.add(projectName); + } else { + projectToDependencyBumps.set( + dependentProject.source, + new Set([projectName]), + ); + } + updateDependentProjectAndAddToVersionData({ + dependentProject, + dependencyPackageName: packageName, + newDependencyVersion: newVersion, + // We don't force bump because we know they will come later in the topologically sorted projects loop and may have their own version update logic to take into account + forceVersionBump: false, + }); + } + + if (updateDependents === 'auto') { + for (const dependentProject of dependentProjectsOutsideCurrentBatch) { + if ( + options.specifierSource === 'version-plans' && + !projectToDependencyBumps.has(dependentProject.source) + ) { + projectToDependencyBumps.set( + dependentProject.source, + new Set([projectName]), + ); + } + + updateDependentProjectAndAddToVersionData({ + dependentProject, + dependencyPackageName: packageName, + newDependencyVersion: newVersion, + // For these additional dependents, we need to update their package.json version as well because we know they will not come later in the topologically sorted projects loop + // (Unless using version plans and the dependent is not filtered out by --projects) + forceVersionBump: + options.specifierSource === 'version-plans' && + projects.find((p) => p.name === dependentProject.source) + ? false + : updateDependentsBump, + }); + } + } + for (const transitiveDependentProject of transitiveLocalPackageDependents) { + // Check if the transitive dependent originates from a circular dependency + const isFromCircularDependency = circularDependencies.has( + `${transitiveDependentProject.source}:${transitiveDependentProject.target}`, + ); + const dependencyProjectName = transitiveDependentProject.target; + const dependencyPackageRoot = projectNameToPackageRootMap.get( + dependencyProjectName, + ); + if (!dependencyPackageRoot) { + throw new Error( + `The project "${dependencyProjectName}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`, + ); + } + const dependencyPyprojectTomlPath = joinPathFragments( + dependencyPackageRoot, + 'pyproject.toml', + ); + const dependencyPyprojectToml = readPyprojectToml( + tree, + dependencyPyprojectTomlPath, + ); + + updateDependentProjectAndAddToVersionData({ + dependentProject: transitiveDependentProject, + dependencyPackageName: dependencyPyprojectToml.tool.poetry.name, + newDependencyVersion: dependencyPyprojectToml.tool.poetry.version, + /** + * For these additional dependents, we need to update their package.json version as well because we know they will not come later in the topologically sorted projects loop. + * The one exception being if the dependent is part of a circular dependency, in which case we don't want to force a version bump as this would come in addition to the one + * already applied. + */ + forceVersionBump: isFromCircularDependency + ? false + : updateDependentsBump, + }); + } + + // Print the logs that have been buffered for this project + logger.flush(); + } + + /** + * Ensure that formatting is applied so that version bump diffs are as minimal as possible + * within the context of the user's workspace. + */ + await formatFiles(tree); + + // Return the version data so that it can be leveraged by the overall version command + return { + data: versionData, + callback: async (tree, opts) => { + const changedFiles: string[] = []; + const deletedFiles: string[] = []; + + for (const cb of deleteVersionPlanCallbacks) { + deletedFiles.push(...(await cb(opts.dryRun))); + } + + changedFiles.push( + ...poetryLocks.map((lockDir) => path.join(lockDir, 'poetry.lock')), + ); + if (!opts.dryRun) { + for (const lockDir of poetryLocks) { + runPoetry(['lock', '--no-update'], { + cwd: lockDir, + }); + } + } + + if (!opts.dryRun) { + if (tree.exists('pyproject.toml')) { + changedFiles.push('poetry.lock'); + runPoetry(['lock', '--no-update'], { cwd: tree.root }); + } + } + + return { changedFiles, deletedFiles }; + }, + }; + } catch (e) { + // Flush any pending logs before printing the error to make troubleshooting easier + logger?.flush(); + console.log(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); + } +} + +export default releaseVersionGenerator; + +function createResolvePackageRoot(customPackageRoot?: string) { + return (projectNode: ProjectGraphProjectNode): string => { + // Default to the project root if no custom packageRoot + if (!customPackageRoot) { + return projectNode.data.root; + } + if (projectNode.data.root === '.') { + // TODO This is a temporary workaround to fix NXC-574 until NXC-573 is resolved + return projectNode.data.root; + } + return interpolate(customPackageRoot, { + workspaceRoot: '', + projectRoot: projectNode.data.root, + projectName: projectNode.name, + }); + }; +} + +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]; +} + +class ProjectLogger { + private logs: string[] = []; + + constructor( + private projectName: string, + private color: (typeof colors)[number], + ) {} + + buffer(msg: string) { + this.logs.push(msg); + } + + flush() { + 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); + }); + } +} diff --git a/packages/nx-python/src/generators/release-version/schema.d.ts b/packages/nx-python/src/generators/release-version/schema.d.ts new file mode 100644 index 0000000..0bd430f --- /dev/null +++ b/packages/nx-python/src/generators/release-version/schema.d.ts @@ -0,0 +1 @@ +export { ReleaseVersionGeneratorSchema } from 'nx/src/command-line/release/version'; diff --git a/packages/nx-python/src/generators/release-version/schema.json b/packages/nx-python/src/generators/release-version/schema.json new file mode 100644 index 0000000..f51770a --- /dev/null +++ b/packages/nx-python/src/generators/release-version/schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxPythonReleaseVersionGenerator", + "cli": "nx", + "title": "Implementation details of `nx release version`", + "description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.", + "type": "object", + "properties": { + "projects": { + "type": "array", + "description": "The ProjectGraphProjectNodes being versioned in the current execution.", + "items": { + "type": "object" + } + }, + "projectGraph": { + "type": "object", + "description": "ProjectGraph instance" + }, + "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", + "description": "Which approach to use to determine the semver specifier used to bump the version of the project.", + "enum": ["prompt", "conventional-commits", "version-plans"] + }, + "preid": { + "type": "string", + "description": "The optional prerelease identifier to apply to the version, in the case that the specifier argument has been set to prerelease." + }, + "packageRoot": { + "type": "string", + "description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root" + }, + "currentVersionResolver": { + "type": "string", + "default": "disk", + "description": "Which approach to use to determine the current version of the project.", + "enum": ["registry", "disk", "git-tag"] + }, + "currentVersionResolverMetadata": { + "type": "object", + "description": "Additional metadata to pass to the current version resolver.", + "default": {} + }, + "skipLockFileUpdate": { + "type": "boolean", + "description": "Whether to skip updating the lock file after updating the version." + }, + "installArgs": { + "type": "string", + "description": "Additional arguments to pass to the package manager when updating the lock file with an install command." + }, + "installIgnoreScripts": { + "type": "boolean", + "description": "Whether to ignore install lifecycle scripts when updating the lock file with an install command." + } + }, + "required": ["projects", "projectGraph", "releaseGroup"] +} diff --git a/packages/nx-python/src/generators/release-version/test-utils/create-workspace-with-package-dependencies.ts b/packages/nx-python/src/generators/release-version/test-utils/create-workspace-with-package-dependencies.ts new file mode 100644 index 0000000..3dbd3d2 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/test-utils/create-workspace-with-package-dependencies.ts @@ -0,0 +1,90 @@ +import { ProjectGraph, Tree } from '@nx/devkit'; +import { PyprojectToml } from '../../../graph/dependency-graph'; +import path from 'path'; +import { writePyprojectToml } from '../../../executors/utils/poetry'; + +interface ProjectAndPackageData { + [projectName: string]: { + projectRoot: string; + packageName: string; + version: string; + pyprojectTomlPath: string; + localDependencies: { + projectName: string; + dependencyCollection: 'dependencies' | string; + }[]; + }; +} + +export function createWorkspaceWithPackageDependencies( + tree: Tree, + projectAndPackageData: ProjectAndPackageData, +): ProjectGraph { + const projectGraph: ProjectGraph = { + nodes: {}, + dependencies: {}, + }; + + for (const [projectName, data] of Object.entries(projectAndPackageData)) { + const pyprojectTomlContents = { + tool: { + poetry: { + name: data.packageName, + version: data.version, + }, + }, + } as PyprojectToml; + for (const dependency of data.localDependencies) { + const dependencyPackageName = + projectAndPackageData[dependency.projectName].packageName; + + if (dependency.dependencyCollection === 'dependencies') { + pyprojectTomlContents.tool.poetry.dependencies ??= {}; + pyprojectTomlContents.tool.poetry.dependencies[dependencyPackageName] = + { + develop: true, + path: path.relative( + data.projectRoot, + projectAndPackageData[dependency.projectName].projectRoot, + ), + }; + } else { + pyprojectTomlContents.tool.poetry.group ??= {}; + pyprojectTomlContents.tool.poetry.group[ + dependency.dependencyCollection + ] ??= { + dependencies: {}, + }; + + pyprojectTomlContents.tool.poetry.group[ + dependency.dependencyCollection + ].dependencies[dependencyPackageName] = { + develop: true, + path: path.relative( + data.projectRoot, + projectAndPackageData[dependency.projectName].projectRoot, + ), + }; + } + } + // add the project and its nx project level dependencies to the projectGraph + projectGraph.nodes[projectName] = { + name: projectName, + type: 'lib', + data: { + root: data.projectRoot, + }, + }; + projectGraph.dependencies[projectName] = data.localDependencies.map( + (dependency) => ({ + source: projectName, + target: dependency.projectName, + type: 'static', + }), + ); + // create the pyproject.toml in the tree + writePyprojectToml(tree, data.pyprojectTomlPath, pyprojectTomlContents); + } + + return projectGraph; +} diff --git a/packages/nx-python/src/generators/release-version/utils/package.ts b/packages/nx-python/src/generators/release-version/utils/package.ts new file mode 100644 index 0000000..d28b346 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/utils/package.ts @@ -0,0 +1,87 @@ +import { joinPathFragments, Tree } from '@nx/devkit'; +import { + PyprojectToml, + PyprojectTomlDependencies, +} from '../../../graph/dependency-graph'; +import { readPyprojectToml } from '../../../executors/utils/poetry'; + +export class Package { + name: string; + version: string; + location: string; + + constructor( + private tree: Tree, + private pyprojectToml: PyprojectToml, + workspaceRoot: string, + private workspaceRelativeLocation: string, + ) { + this.name = pyprojectToml.tool.poetry.name; + this.version = pyprojectToml.tool.poetry.version; + this.location = joinPathFragments(workspaceRoot, workspaceRelativeLocation); + } + + getLocalDependency(depName: string): { + collection: 'dependencies' | 'devDependencies' | 'optionalDependencies'; + groupKey?: string; + spec: string; + } | null { + if (this.pyprojectToml.tool?.poetry?.dependencies?.[depName]) { + return { + collection: 'dependencies', + spec: extractDependencyVersion( + this.tree, + this.workspaceRelativeLocation, + this.pyprojectToml.tool?.poetry?.dependencies, + depName, + ), + }; + } + + for (const groupKey of Object.keys( + this.pyprojectToml.tool?.poetry?.group, + )) { + if ( + this.pyprojectToml.tool?.poetry?.group[groupKey]?.dependencies?.[ + depName + ] + ) { + return { + collection: + groupKey === 'dev' ? 'devDependencies' : 'optionalDependencies', + groupKey, + spec: extractDependencyVersion( + this.tree, + this.workspaceRelativeLocation, + this.pyprojectToml.tool?.poetry?.group[groupKey]?.dependencies, + depName, + ), + }; + } + } + + return null; + } +} + +export function extractDependencyVersion( + tree: Tree, + projectLocation: string, + dependencyGroup: PyprojectTomlDependencies, + depName: string, +): string { + if (typeof dependencyGroup?.[depName] === 'string') { + return dependencyGroup?.[depName]; + } + + const dependentPyproject = readPyprojectToml( + tree, + joinPathFragments( + projectLocation, + dependencyGroup?.[depName].path, + 'pyproject.toml', + ), + ); + + return dependentPyproject.tool.poetry.version; +} diff --git a/packages/nx-python/src/generators/release-version/utils/resolve-local-package-dependencies.ts b/packages/nx-python/src/generators/release-version/utils/resolve-local-package-dependencies.ts new file mode 100644 index 0000000..7fbc017 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/utils/resolve-local-package-dependencies.ts @@ -0,0 +1,115 @@ +import { + ProjectGraph, + ProjectGraphDependency, + ProjectGraphProjectNode, + Tree, + joinPathFragments, + workspaceRoot, +} from '@nx/devkit'; +import { satisfies } from 'semver'; +import { Package } from './package'; +import { readPyprojectToml } from '../../../executors/utils/poetry'; + +export interface LocalPackageDependency extends ProjectGraphDependency { + /** + * The rawVersionSpec contains the value of the version spec as it was defined in the package.json + * of the dependent project. This can be useful in cases where the version spec is a range, path or + * workspace reference, and it needs to be be reverted to that original value as part of the release. + */ + rawVersionSpec: string; + groupKey?: string; + dependencyCollection: 'dependencies' | string; + // we don't currently manage peer dependencies +} + +export function resolveLocalPackageDependencies( + tree: Tree, + projectGraph: ProjectGraph, + filteredProjects: ProjectGraphProjectNode[], + projectNameToPackageRootMap: Map, + resolvePackageRoot: (projectNode: ProjectGraphProjectNode) => string, + includeAll = false, +): Record { + const localPackageDependencies: Record = {}; + const projectNodeToPackageMap = new Map(); + + const projects = includeAll + ? Object.values(projectGraph.nodes) + : filteredProjects; + + // Iterate through the projects being released and resolve any relevant package.json data + for (const projectNode of projects) { + // Resolve the package.json path for the project, taking into account any custom packageRoot settings + let packageRoot = projectNameToPackageRootMap.get(projectNode.name); + // packageRoot wasn't added to the map yet, try to resolve it dynamically + if (!packageRoot && includeAll) { + packageRoot = resolvePackageRoot(projectNode); + if (!packageRoot) { + continue; + } + // Append it to the map for later use within the release version generator + projectNameToPackageRootMap.set(projectNode.name, packageRoot); + } + const pyprojectTomlPath = joinPathFragments(packageRoot, 'pyproject.toml'); + if (!tree.exists(pyprojectTomlPath)) { + continue; + } + const pyprojectToml = readPyprojectToml(tree, pyprojectTomlPath); + const pkg = new Package(tree, pyprojectToml, workspaceRoot, packageRoot); + projectNodeToPackageMap.set(projectNode, pkg); + } + + // populate local npm package dependencies + for (const projectDeps of Object.values(projectGraph.dependencies)) { + const workspaceDeps = projectDeps.filter( + (dep) => + !isExternalNpmDependency(dep.target) && + !isExternalNpmDependency(dep.source), + ); + for (const dep of workspaceDeps) { + const source = projectGraph.nodes[dep.source]; + const target = projectGraph.nodes[dep.target]; + if ( + !source || + !projectNodeToPackageMap.has(source) || + !target || + !projectNodeToPackageMap.has(target) + ) { + // only relevant for dependencies between two workspace projects with Package objects + continue; + } + + const sourcePackage = projectNodeToPackageMap.get(source); + const targetPackage = projectNodeToPackageMap.get(target); + const sourcePoetryDependency = sourcePackage.getLocalDependency( + targetPackage.name, + ); + if (!sourcePoetryDependency) { + continue; + } + + const targetMatchesRequirement = + // For file: and workspace: protocols the targetVersionSpec could be a path, so we check if it matches the target's location + satisfies(targetPackage.version, targetPackage.version); + + if (targetMatchesRequirement) { + // track only local package dependencies that are satisfied by the target's version + localPackageDependencies[dep.source] = [ + ...(localPackageDependencies[dep.source] || []), + { + ...dep, + groupKey: sourcePoetryDependency.groupKey, + dependencyCollection: sourcePoetryDependency.collection, + rawVersionSpec: sourcePoetryDependency.spec, + }, + ]; + } + } + } + + return localPackageDependencies; +} + +function isExternalNpmDependency(dep: string): boolean { + return dep.startsWith('npm:'); +} diff --git a/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.spec.ts b/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.spec.ts new file mode 100644 index 0000000..da9b8d8 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.spec.ts @@ -0,0 +1,256 @@ +import { sortProjectsTopologically } from './sort-projects-topologically'; + +describe('sortProjectsTopologically', () => { + it('should return empty array if no projects are provided', () => { + const projectGraph = { + dependencies: {}, + nodes: {}, + }; + const projectNodes = []; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual([]); + }); + + it('should return a single project if only one project is provided', () => { + const projectGraph = { + dependencies: {}, + nodes: { + project1: { + name: 'project1', + data: { + root: '', + }, + type: 'app' as const, + }, + }, + }; + const projectNodes = [projectGraph.nodes.project1]; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual([projectGraph.nodes.project1]); + }); + + it('should return [2,1] if 1 depends on 2', () => { + const projectGraph = { + dependencies: { + project1: [ + { + source: 'project1', + target: 'project2', + type: 'static', + }, + ], + project2: [], + }, + nodes: { + project1: { + name: 'project1', + data: { + root: '', + }, + type: 'app' as const, + }, + project2: { + name: 'project2', + data: { + root: '', + }, + type: 'app' as const, + }, + }, + }; + const projectNodes = [ + projectGraph.nodes.project1, + projectGraph.nodes.project2, + ]; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual([ + projectGraph.nodes.project2, + projectGraph.nodes.project1, + ]); + }); + + it('should return the original list of nodes if a circular dependency is present', () => { + const projectGraph = { + dependencies: { + project1: [ + { + source: 'project1', + target: 'project2', + type: 'static', + }, + ], + project2: [ + { + source: 'project2', + target: 'project1', + type: 'static', + }, + ], + }, + nodes: { + project1: { + name: 'project1', + data: { + root: '', + }, + type: 'app' as const, + }, + project2: { + name: 'project2', + data: { + root: '', + }, + type: 'app' as const, + }, + }, + }; + const projectNodes = [ + projectGraph.nodes.project1, + projectGraph.nodes.project2, + ]; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual(projectNodes); + }); + + it('should return [3,2,1] if 1 depends on 2 and 2 depends on 3', () => { + const projectGraph = { + dependencies: { + project1: [ + { + source: 'project1', + target: 'project2', + type: 'static', + }, + ], + project2: [ + { + source: 'project2', + target: 'project3', + type: 'static', + }, + ], + }, + nodes: { + project1: { + name: 'project1', + data: { + root: '', + }, + type: 'app' as const, + }, + project2: { + name: 'project2', + data: { + root: '', + }, + type: 'app' as const, + }, + project3: { + name: 'project3', + data: { + root: '', + }, + type: 'app' as const, + }, + }, + }; + const projectNodes = [ + projectGraph.nodes.project1, + projectGraph.nodes.project2, + projectGraph.nodes.project3, + ]; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual([ + projectGraph.nodes.project3, + projectGraph.nodes.project2, + projectGraph.nodes.project1, + ]); + }); + + it('should return [1,2,3,4] if 1 has zero dependencies, 2 has one, 3 has two, and 4 has three', () => { + const projectGraph = { + dependencies: { + project1: [], + project2: [ + { + source: 'project2', + target: 'project1', + type: 'static', + }, + ], + project3: [ + { + source: 'project3', + target: 'project1', + type: 'static', + }, + { + source: 'project3', + target: 'project2', + type: 'static', + }, + ], + project4: [ + { + source: 'project4', + target: 'project3', + type: 'static', + }, + { + source: 'project4', + target: 'project2', + type: 'static', + }, + { + source: 'project4', + target: 'project1', + type: 'static', + }, + ], + }, + nodes: { + project1: { + name: 'project1', + data: { + root: '', + }, + type: 'app' as const, + }, + project2: { + name: 'project2', + data: { + root: '', + }, + type: 'app' as const, + }, + project3: { + name: 'project3', + data: { + root: '', + }, + type: 'app' as const, + }, + project4: { + name: 'project4', + data: { + root: '', + }, + type: 'app' as const, + }, + }, + }; + const projectNodes = [ + projectGraph.nodes.project1, + projectGraph.nodes.project2, + projectGraph.nodes.project3, + projectGraph.nodes.project4, + ]; + const result = sortProjectsTopologically(projectGraph, projectNodes); + expect(result).toEqual([ + projectGraph.nodes.project1, + projectGraph.nodes.project2, + projectGraph.nodes.project3, + projectGraph.nodes.project4, + ]); + }); +}); diff --git a/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.ts b/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.ts new file mode 100644 index 0000000..77f6d03 --- /dev/null +++ b/packages/nx-python/src/generators/release-version/utils/sort-projects-topologically.ts @@ -0,0 +1,66 @@ +import { + ProjectGraph, + ProjectGraphDependency, + ProjectGraphProjectNode, +} from '@nx/devkit'; + +export function sortProjectsTopologically( + projectGraph: ProjectGraph, + projectNodes: ProjectGraphProjectNode[], +): ProjectGraphProjectNode[] { + const edges = new Map( + projectNodes.map((node) => [node, 0]), + ); + + const filteredDependencies: ProjectGraphDependency[] = []; + for (const node of projectNodes) { + const deps = projectGraph.dependencies[node.name]; + if (deps) { + filteredDependencies.push( + ...deps.filter((dep) => + projectNodes.find((n) => n.name === dep.target), + ), + ); + } + } + + filteredDependencies.forEach((dep) => { + const sourceNode = projectGraph.nodes[dep.source]; + // dep.source depends on dep.target + edges.set(sourceNode, (edges.get(sourceNode) || 0) + 1); + }); + + // Initialize queue with projects that have no dependencies + const processQueue = [...edges] + .filter(([, count]) => count === 0) + .map(([node]) => node); + const sortedProjects = []; + + while (processQueue.length > 0) { + const node = processQueue.shift(); + sortedProjects.push(node); + + // Process each project that depends on the current node + filteredDependencies + .filter((dep) => dep.target === node.name) + .forEach((dep) => { + const dependentNode = projectGraph.nodes[dep.source]; + const count = edges.get(dependentNode) - 1; + edges.set(dependentNode, count); + if (count === 0) { + processQueue.push(dependentNode); + } + }); + } + + /** + * We cannot reliably sort the nodes, e.g. when a circular dependency is present. + * For releases, we allow the user to work with a graph that contains cycles, so + * we should not throw an error here and simply return the original list of projects. + */ + if (sortedProjects.length !== projectNodes.length) { + return projectNodes; + } + + return sortedProjects; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08ee4a2..81323e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: aws-lambda: specifier: ^1.0.7 version: 1.0.7 + axios: + specifier: ^1.7.7 + version: 1.7.7 chalk: specifier: ^4.1.1 version: 4.1.2 @@ -77,9 +80,15 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + ora: + specifier: 5.3.0 + version: 5.3.0 prompts: specifier: ^2.4.2 version: 2.4.2 + semver: + specifier: ^7.5.3 + version: 7.6.3 tslib: specifier: ^2.3.0 version: 2.6.3 @@ -101,29 +110,29 @@ devDependencies: specifier: ^19.0.3 version: 19.0.3 '@nx/devkit': - specifier: 19.5.7 - version: 19.5.7(nx@19.5.7) + specifier: 19.7.3 + version: 19.7.3(nx@19.7.3) '@nx/eslint': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3) '@nx/eslint-plugin': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.5.7)(typescript@5.5.4) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.7.3)(typescript@5.5.4) '@nx/js': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) '@nx/plugin': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) '@nx/vite': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) '@nx/web': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) '@nx/workspace': - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) '@semantic-release/changelog': specifier: ^6.0.3 version: 6.0.3(semantic-release@24.0.0) @@ -203,11 +212,11 @@ devDependencies: specifier: ^4.11.1 version: 4.11.1 nx: - specifier: 19.5.7 - version: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + specifier: 19.7.3 + version: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) nx-cloud: - specifier: 19.0.0 - version: 19.0.0 + specifier: 19.1.0 + version: 19.1.0 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -3469,18 +3478,18 @@ packages: fastq: 1.17.1 dev: true - /@nrwl/devkit@19.5.7(nx@19.5.7): - resolution: {integrity: sha512-sTEwqsAT6bMturU14o/0O6v509OkwGOglxpbiL/zIYO/fDkMoNgnhlHBIT87i4YVuofMz2Z+hTfjDskzDPRSYw==} + /@nrwl/devkit@19.7.3(nx@19.7.3): + resolution: {integrity: sha512-g9vANTuxgHan6uAkI6M6tkfLjHECLmbZ4A80UqpxJNQJsCJFEHkzIC9oxhf8bWV3PjgCH6Xm4VyQ2GHwb3sgWw==} dependencies: - '@nx/devkit': 19.5.7(nx@19.5.7) + '@nx/devkit': 19.7.3(nx@19.7.3) transitivePeerDependencies: - nx dev: true - /@nrwl/eslint-plugin-nx@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-yNi2U3Ro1RcNFb22urDs0raOfVg1ISKcrlx+tR/E3e9Mw/yTJVGg7wPDL4CJK0Xwt2AC1BPnRRechCJv6llqsw==} + /@nrwl/eslint-plugin-nx@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-lb3BUM+6AxRWWFl0+nbcpu2QXQ1S7PV+srQO9Xb70rbRkgFIdnevrM3PVL+Z3oQrl0oxqazBYSid6e6ISUpTQw==} dependencies: - '@nx/eslint-plugin': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.5.7)(typescript@5.5.4) + '@nx/eslint-plugin': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.7.3)(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3497,10 +3506,10 @@ packages: - verdaccio dev: true - /@nrwl/jest@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4): - resolution: {integrity: sha512-E0Ii+ybj3UewhElZ4IyElzeA0zY8NiF0kHI06tTYUi8sYsompxT6mkSLmNffn0ouovqnT0/IwYMZVn0BA5lURg==} + /@nrwl/jest@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4): + resolution: {integrity: sha512-knsOchwmN/0j9M7meMFMno1F//qK97YWNIEnN8qlkqQC7JX+VAH8/JSk/in87MCW//vi67twUB3XOiI9dKzOlA==} dependencies: - '@nx/jest': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) + '@nx/jest': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3517,10 +3526,10 @@ packages: - verdaccio dev: true - /@nrwl/js@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.4.5): - resolution: {integrity: sha512-Hb8ZBQYI7X5YsV573jCDm+3rn+htVqf0GEaDJGRmhzPe9PE/rlquti07gO5ao9+SeLcB34g6kAhR8PO+3sz0pw==} + /@nrwl/js@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.4.5): + resolution: {integrity: sha512-bbztlMkmARTRnTz79W5Mp4M1w4o1QdzWWnXEJLkGdeyOzUqSlHESC0vWDplcdFBjnWZ9A/P4L53GtKNn/VdHnQ==} dependencies: - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.4.5) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.4.5) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3534,10 +3543,10 @@ packages: - verdaccio dev: true - /@nrwl/js@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-Hb8ZBQYI7X5YsV573jCDm+3rn+htVqf0GEaDJGRmhzPe9PE/rlquti07gO5ao9+SeLcB34g6kAhR8PO+3sz0pw==} + /@nrwl/js@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-bbztlMkmARTRnTz79W5Mp4M1w4o1QdzWWnXEJLkGdeyOzUqSlHESC0vWDplcdFBjnWZ9A/P4L53GtKNn/VdHnQ==} dependencies: - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3551,18 +3560,18 @@ packages: - verdaccio dev: true - /@nrwl/nx-cloud@19.0.0: - resolution: {integrity: sha512-3WuXq3KKXwKnbjOkYK0OXosjD02LIjC3kEkyMIbaE36O9dMp3k/sa4ZtDVC3tAoIrj17VLVmjKfoDYbED1rapw==} + /@nrwl/nx-cloud@19.1.0: + resolution: {integrity: sha512-krngXVPfX0Zf6+zJDtcI59/Pt3JfcMPMZ9C/+/x6rvz4WGgyv1s0MI4crEUM0Lx5ZpS4QI0WNDCFVQSfGEBXUg==} dependencies: - nx-cloud: 19.0.0 + nx-cloud: 19.1.0 transitivePeerDependencies: - debug dev: true - /@nrwl/nx-plugin@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4): - resolution: {integrity: sha512-FRJU+yHXkE2V5TPlGWGfR+OBRDrD5OiN7fg9/WYraC6Nzmzkk4WpoIhtcEbGVbCqmAr4s7V9UCmR2ZO4YbGmGA==} + /@nrwl/nx-plugin@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4): + resolution: {integrity: sha512-i5tX4iuhCRJ28EUdTdXgO9z/XSLjTyTQGC2IIxA7028Co+anJMTMn3KHKqljC2oq/NJpaGpY6KQjVB9QZlfaoQ==} dependencies: - '@nx/plugin': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) + '@nx/plugin': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3581,11 +3590,11 @@ packages: - verdaccio dev: true - /@nrwl/tao@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7): - resolution: {integrity: sha512-c1rN6HY97+cEwoM5Q9412399Ac1rw7pI/3IS5iJSYkeI5TTGOobIpdCavJPZVcfqo4+wegXPA3F/OmulgbOUJA==} + /@nrwl/tao@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7): + resolution: {integrity: sha512-cIGhnSFPZdVTp4bI0fqwFoE9i7ToPg5jXz+hNMl/MTwcOQfKQ1JJY/ZPLM3aBUPORFIZ/GECQEycUb6+xCB56g==} hasBin: true dependencies: - nx: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + nx: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) tslib: 2.6.3 transitivePeerDependencies: - '@swc-node/register' @@ -3593,10 +3602,10 @@ packages: - debug dev: true - /@nrwl/vite@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5): - resolution: {integrity: sha512-8MEhLh9hl1wYYFBBqgas+MAhcxJgW2Ufn7rM5aAnpb6Js1ZTmFu0ztzB/n63eUInEMEpTbDHf6diAQq/yWOKGg==} + /@nrwl/vite@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5): + resolution: {integrity: sha512-mCE7VZiNQrFAVqEmrdUfAQqu7RJd6fkQmcU4252JgqXWBXFjxLgKQ1RjOaSTR2ljuR2Z0kIUOFeGa0YEzKUdLw==} dependencies: - '@nx/vite': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) + '@nx/vite': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3612,10 +3621,10 @@ packages: - vitest dev: true - /@nrwl/web@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-QKnxI91kpsv2In/WGcEHgm4VifY6Oflgtr4GSGgI0SogfAs0TYU7J4fDLKKuX1KQeO5y9PQ2V0/ci4NjiDnRXw==} + /@nrwl/web@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-CNFlbtpr3OAca/ArWhbzENqVwT5oAgyNsyMZWKzvq9bmO8xi6LhxDrtW5tuTPiyl9GNDlMY3YwqRemR8XrdejQ==} dependencies: - '@nx/web': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nx/web': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3629,35 +3638,35 @@ packages: - verdaccio dev: true - /@nrwl/workspace@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7): - resolution: {integrity: sha512-VzQmG+de1DvQnmWy2acMkxBrRPxFdvQ06Tja6tThn3UWMB9RwK2wKIEERttRhjBLGjGlr6ARi9Bd8zYTgpW0Lw==} + /@nrwl/workspace@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7): + resolution: {integrity: sha512-2ffUbLzBYGQte6zQ6dDLefgU9X812Uh7v61yTV7z4zfYbUtjwInkWqlkTvRuK08DRhD5vWo9xyUDp7acAdZaxw==} dependencies: - '@nx/workspace': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + '@nx/workspace': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) transitivePeerDependencies: - '@swc-node/register' - '@swc/core' - debug dev: true - /@nx/devkit@19.5.7(nx@19.5.7): - resolution: {integrity: sha512-mUtZQcdqbF0Q9HfyG14jmpPCtZ1GnVaLNIADZv5SLpFyfh4ZjaBw6wdjPj7Sp3imLoyqMrcd9nCRNO2hlem8bw==} + /@nx/devkit@19.7.3(nx@19.7.3): + resolution: {integrity: sha512-dIavuzfcMLCTa5uhd4R7HsxcFO0w9fHwG4wDg76wyBAbPGJlrR+9zg359hZ/SkXdguO6bMVmmQg/EXIvo6g69A==} peerDependencies: nx: '>= 17 <= 20' dependencies: - '@nrwl/devkit': 19.5.7(nx@19.5.7) + '@nrwl/devkit': 19.7.3(nx@19.7.3) ejs: 3.1.10 enquirer: 2.3.6 ignore: 5.3.1 minimatch: 9.0.3 - nx: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + nx: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) semver: 7.6.3 tmp: 0.2.3 tslib: 2.6.3 yargs-parser: 21.1.1 dev: true - /@nx/eslint-plugin@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-cldJ2THoCz3mbfqX/gHnm+XLrmDYa5WcXav8f/AGqeGYLwHJdBdgj151lcVOgIKChR5judn9bpLMGt7J2zq0Yg==} + /@nx/eslint-plugin@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-sZV0loOZ2Yi1/Kty/YwYtQjpVNr/HBcxiO0GELzlmcztcQhJaH86V6Q2Jr5VuH3SWbjuhjYprQs1MpkH6HKbqw==} peerDependencies: '@typescript-eslint/parser': ^6.13.2 || ^7.0.0 eslint-config-prettier: ^9.0.0 @@ -3665,9 +3674,9 @@ packages: eslint-config-prettier: optional: true dependencies: - '@nrwl/eslint-plugin-nx': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.5.7)(typescript@5.5.4) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nrwl/eslint-plugin-nx': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(@typescript-eslint/parser@7.18.0)(eslint-config-prettier@9.1.0)(eslint@8.57.0)(nx@19.7.3)(typescript@5.5.4) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.5.4) @@ -3691,8 +3700,8 @@ packages: - verdaccio dev: true - /@nx/eslint@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7): - resolution: {integrity: sha512-xqCiwWWuG1rRUE3rno7PUqAoZK3HHhxE5POKh4zf9BzOSaQwu8G3i6wRMoaVeEBqxfxIbgs2Uf6j9A5XyLW1Hw==} + /@nx/eslint@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3): + resolution: {integrity: sha512-EVkdZ/pRIyAETWVmkZkNes/VXAtD7epeRUTV+dRgkSJWHmeIpJZ/fv0o2vJygzLvyCiFcyWkdzXIfQpDyXfbDw==} peerDependencies: '@zkochan/js-yaml': 0.0.7 eslint: ^8.0.0 || ^9.0.0 @@ -3700,9 +3709,9 @@ packages: '@zkochan/js-yaml': optional: true dependencies: - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.4.5) - '@nx/linter': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.4.5) + '@nx/linter': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3) eslint: 8.57.0 semver: 7.6.3 tslib: 2.6.3 @@ -3719,14 +3728,14 @@ packages: - verdaccio dev: true - /@nx/jest@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4): - resolution: {integrity: sha512-3WUlLSlhzuunVlYSoJUMRiBsSgmFmPma5GmAT0LhDlU8Gdgrhy/ZPJf9E5gLBaK2r6w/rm0SQ210OQrt8zlntQ==} + /@nx/jest@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4): + resolution: {integrity: sha512-XAqKhF4cxzIH4/mPPV4oQftQ7whnvUF2pkJiLGZqyQM7TvWjjR8mWWuwnnyfBK/6S5heqDnI0n6tCiiJuoZQ4g==} dependencies: '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 - '@nrwl/jest': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nrwl/jest': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) '@phenomnomnominal/tsquery': 5.0.1(typescript@5.5.4) chalk: 4.1.2 identity-obj-proxy: 3.0.0 @@ -3735,6 +3744,7 @@ packages: jest-util: 29.7.0 minimatch: 9.0.3 resolve.exports: 1.1.0 + semver: 7.6.3 tslib: 2.6.3 yargs-parser: 21.1.1 transitivePeerDependencies: @@ -3753,8 +3763,8 @@ packages: - verdaccio dev: true - /@nx/js@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.4.5): - resolution: {integrity: sha512-DlZHz6nWIFyr+43T0g/FfISXETfKuLwg22clQGwTlsmal9ShMOt7uTNl18BzK1cnvxE+cwbFUQ8pCL1DcrYKsA==} + /@nx/js@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.4.5): + resolution: {integrity: sha512-M5yxRnwPuEFRqH+Gutou2EZyX1x5VZPCznpmktBvee/sjhtd/zwR0z/b48TOpLXShtcVmcOy4lUHu1B46CnPnA==} peerDependencies: verdaccio: ^5.0.4 peerDependenciesMeta: @@ -3768,9 +3778,9 @@ packages: '@babel/preset-env': 7.25.3(@babel/core@7.25.2) '@babel/preset-typescript': 7.24.7(@babel/core@7.25.2) '@babel/runtime': 7.25.0 - '@nrwl/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.4.5) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/workspace': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + '@nrwl/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.4.5) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/workspace': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) babel-plugin-const-enum: 1.2.0(@babel/core@7.25.2) babel-plugin-macros: 2.8.0 babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.25.2) @@ -3781,6 +3791,7 @@ packages: fs-extra: 11.2.0 ignore: 5.3.1 js-tokens: 4.0.0 + jsonc-parser: 3.2.0 minimatch: 9.0.3 npm-package-arg: 11.0.1 npm-run-path: 4.0.1 @@ -3802,8 +3813,8 @@ packages: - typescript dev: true - /@nx/js@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-DlZHz6nWIFyr+43T0g/FfISXETfKuLwg22clQGwTlsmal9ShMOt7uTNl18BzK1cnvxE+cwbFUQ8pCL1DcrYKsA==} + /@nx/js@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-M5yxRnwPuEFRqH+Gutou2EZyX1x5VZPCznpmktBvee/sjhtd/zwR0z/b48TOpLXShtcVmcOy4lUHu1B46CnPnA==} peerDependencies: verdaccio: ^5.0.4 peerDependenciesMeta: @@ -3817,9 +3828,9 @@ packages: '@babel/preset-env': 7.25.3(@babel/core@7.25.2) '@babel/preset-typescript': 7.24.7(@babel/core@7.25.2) '@babel/runtime': 7.25.0 - '@nrwl/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/workspace': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + '@nrwl/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/workspace': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) babel-plugin-const-enum: 1.2.0(@babel/core@7.25.2) babel-plugin-macros: 2.8.0 babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.25.2) @@ -3830,6 +3841,7 @@ packages: fs-extra: 11.2.0 ignore: 5.3.1 js-tokens: 4.0.0 + jsonc-parser: 3.2.0 minimatch: 9.0.3 npm-package-arg: 11.0.1 npm-run-path: 4.0.1 @@ -3851,10 +3863,10 @@ packages: - typescript dev: true - /@nx/linter@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7): - resolution: {integrity: sha512-4DXi17d11xEbrffNDOS+qoC9wIZJPxiyf88x6pRIhPyUb/NNMCT4hLnpEGnJvhqGb8LXF/c48UkJZqda/6p4qA==} + /@nx/linter@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3): + resolution: {integrity: sha512-+NL+Cp2ZfLZZY+IKI8RC9fgvExk6YEF5U0uJAT9YtUsP7Q8g6ZeFMkWgL61YWtmAh7SvsLBbKzcWfdSMTfaZBg==} dependencies: - '@nx/eslint': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7) + '@nx/eslint': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -3869,8 +3881,8 @@ packages: - verdaccio dev: true - /@nx/nx-darwin-arm64@19.5.7: - resolution: {integrity: sha512-5jFAZSfV8QVNoxOXayZw4/jNJbxMMctNOYZW8Qj4eU8Ti+OmhsLgouxz/9enCh5SDITriOMZ7IHZ9rhrlGQoig==} + /@nx/nx-darwin-arm64@19.7.3: + resolution: {integrity: sha512-0dDK0UkMR0vBv4AP/48Q9A+OC2dvpivdt8su/4W/CPADy69M9B5O3jPiK+jTRsLshQG/soC9JG0Rll1BNWymPg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -3878,8 +3890,8 @@ packages: dev: true optional: true - /@nx/nx-darwin-x64@19.5.7: - resolution: {integrity: sha512-Ss+rF2+MQxyKrNnSYAeEGhtdE9hUHiTqyjJo4n1lvIWJ++TairOCtk5QRHrYLgAxE1XTf0OabcsDzegxv7yk3Q==} + /@nx/nx-darwin-x64@19.7.3: + resolution: {integrity: sha512-hTdv5YY2GQTdT7GwVO7ST27ZzvCmAQvmkEapfnCdy74QsL4gapaXJFvtWLHVfG6qHNRHWXbpdegvR3VswRHZVQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -3887,8 +3899,8 @@ packages: dev: true optional: true - /@nx/nx-freebsd-x64@19.5.7: - resolution: {integrity: sha512-FMLXcUr3mw/v4LvmNqHMAXy2k+T/hp2YpdBUq9ExteMfRywFsnKNlm39n/quniFsgKthEMdvvzxSQppRKaVwIw==} + /@nx/nx-freebsd-x64@19.7.3: + resolution: {integrity: sha512-dwuB/3eoV2RbD0b0LHnagQOXa9PKAjLi7g5vNxzw6LuNT1tdaLaUZZGv2tfG0hHjsV0cOaAX41rEyOIwJyE7zg==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] @@ -3896,8 +3908,8 @@ packages: dev: true optional: true - /@nx/nx-linux-arm-gnueabihf@19.5.7: - resolution: {integrity: sha512-LhJ342HutpR258lBLVTkXd6x2Uj4ZPJ6xKdfEm+FYQvG1byPr2L0TlNXcfSBkYtd7wRn0qg9eQZoCV/5+w415Q==} + /@nx/nx-linux-arm-gnueabihf@19.7.3: + resolution: {integrity: sha512-X/eG3IqvIxlCfIOiCQKv7RKwra54I+SN9zj2TeSOtd/uK0paa3mYSlGUJqoP3wpzasW1+EPIGkTQqV283IA15w==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -3905,8 +3917,8 @@ packages: dev: true optional: true - /@nx/nx-linux-arm64-gnu@19.5.7: - resolution: {integrity: sha512-Q6gN+VNLisg7mYPTXC5JuGCP/s9tLjJFclKdH6FoP5K1Hgy88KK1uUoivDIfI8xaEgyLqphD1AAqokiFWZNWsg==} + /@nx/nx-linux-arm64-gnu@19.7.3: + resolution: {integrity: sha512-LNaX8DVcPlFVJhMf1AAAR6j1DZF9BlVhWlilRM44tIfnmvPfKIahKJIJbuikHE7q+lkvMrQUUDXKiQJlmm/qDw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3914,8 +3926,8 @@ packages: dev: true optional: true - /@nx/nx-linux-arm64-musl@19.5.7: - resolution: {integrity: sha512-BsYNcYujNKb+uE7PrJp4PrX8a3G9oy+THaUr0t5+L435HjuZDBiK+tK9JzYGvM0bR5FOYm5K99I1DVD/Hv0snw==} + /@nx/nx-linux-arm64-musl@19.7.3: + resolution: {integrity: sha512-TJ9PqSebhrn8NfrW+wqMXB9N65U0L0Kjt8FfahWffNKtSAEUvhurbNhqna2Rt5WJe2qaVf6zN2pOHKhF/5pL0w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3923,8 +3935,8 @@ packages: dev: true optional: true - /@nx/nx-linux-x64-gnu@19.5.7: - resolution: {integrity: sha512-ILaLU8b5lUokYVF3vxAVj62qFok1hexiNzBdLGJPI1OkPGELtLyb8RymI3939iJoNMk1DS3/6dqK7NHXvHX8Mw==} + /@nx/nx-linux-x64-gnu@19.7.3: + resolution: {integrity: sha512-YMb4WGGovwgxsP6VvAEnyWvLoUwsDrdE5CxFQ2yoThD2BixmSHUKLtx6dtPDHz25nOE3v1ZzM0xTwYXBhPaeRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3932,8 +3944,8 @@ packages: dev: true optional: true - /@nx/nx-linux-x64-musl@19.5.7: - resolution: {integrity: sha512-LfTnO4JZebLugioMk+GTptv3N38Wj2i2Pko0bdRZaKba+INGSlUgFqoRuO0KqZEmVIUGrxfkfqIN3HghVQ4D/Q==} + /@nx/nx-linux-x64-musl@19.7.3: + resolution: {integrity: sha512-zkjgDSvw2eDN+KuJBPPAPhU/lOdiMvJU0UMthJFw85dhQIYfAO8+UgiFg/qBsKo0kQ0MkhntnIPBPF8bH40qWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3941,8 +3953,8 @@ packages: dev: true optional: true - /@nx/nx-win32-arm64-msvc@19.5.7: - resolution: {integrity: sha512-cCTttdbf1AKuDU8j108SpIMWs53A/0mOVDPOPpa+oKkvBaI8ruZkxOceMjWZjWULd2gi1nS+5nJePpbrdQ8mkg==} + /@nx/nx-win32-arm64-msvc@19.7.3: + resolution: {integrity: sha512-qCTFG6VxNvEe5JfoAELGZsjWDL4G+2NVSoSS3tByJYwVX256qgALcVoUHMjpxBn9FeOvUW9w5PL4Am4PKDdXLw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -3950,8 +3962,8 @@ packages: dev: true optional: true - /@nx/nx-win32-x64-msvc@19.5.7: - resolution: {integrity: sha512-EqSnjpq1PNR/C8/YkL+Gn79dDfQ+HwJM8VJOt4qoCOQ9gQZqNJphjW2hg0H8WxLYezMScx3fbL99mvJO7ab2Cw==} + /@nx/nx-win32-x64-msvc@19.7.3: + resolution: {integrity: sha512-ULNf73gLgB5cU/O4dlQe6tetbRIROTmaUNYTUUCCAC0BqVwZwPDxn4u9C5LgiErVyfPwwAhlserCGei5taLASQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3959,14 +3971,14 @@ packages: dev: true optional: true - /@nx/plugin@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4): - resolution: {integrity: sha512-x6dFk++2HRwpJd/KeNUZ3OFlB9lBmS9UVAmd9i7ecTSPVE64LFk2crYgD3enuIPIX1pIKYaZhcZuRFyJgAGRQQ==} + /@nx/plugin@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4): + resolution: {integrity: sha512-unmKFdwbqqIkPffJjFNc9fZeYWh990XAuff0ufNsAjNrycfnHoVuN7d2VRgxjiUY8vBxRUOUST8fm/TbqQJg2w==} dependencies: - '@nrwl/nx-plugin': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/eslint': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.5.7) - '@nx/jest': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(ts-node@10.9.2)(typescript@5.5.4) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nrwl/nx-plugin': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/eslint': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(eslint@8.57.0)(nx@19.7.3) + '@nx/jest': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(ts-node@10.9.2)(typescript@5.5.4) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) fs-extra: 11.2.0 tslib: 2.6.3 transitivePeerDependencies: @@ -3987,18 +3999,19 @@ packages: - verdaccio dev: true - /@nx/vite@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5): - resolution: {integrity: sha512-a76Be6wu1/mkyj4dI0Gx115/F1TtSBD5a+0IUnvsRbGfHPBEr4mmBQR5zbGXlrqip/i7UnTERZHvpeVUPUJdKg==} + /@nx/vite@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5): + resolution: {integrity: sha512-9ZDtc5DTj/e7wkcOgp5IIDzTRsYDMwAAWBvstNC0+1p5gE+cp3AJAZH+z4zOVstnB9lRDshxCwQUKd2SHt+wDg==} peerDependencies: vite: ^5.0.0 - vitest: ^1.3.1 + vitest: ^1.3.1 || ^2.0.0 dependencies: - '@nrwl/vite': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nrwl/vite': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4)(vite@5.4.0)(vitest@2.0.5) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) '@phenomnomnominal/tsquery': 5.0.1(typescript@5.5.4) '@swc/helpers': 0.5.12 enquirer: 2.3.6 + minimatch: 9.0.3 tsconfig-paths: 4.2.0 vite: 5.4.0(@types/node@18.19.21) vitest: 2.0.5(@types/node@18.19.21)(@vitest/ui@2.0.5) @@ -4015,12 +4028,12 @@ packages: - verdaccio dev: true - /@nx/web@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4): - resolution: {integrity: sha512-ToZmgBuB1AJBFxY6+qILf+JyRgRjqbOIzwa0oyydwqFGPUr5UiHTDuDvmjHKOKsTZKcwfg/ladtvQkt943h/CQ==} + /@nx/web@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4): + resolution: {integrity: sha512-ODdwgNnE7/R3ytcgC8HagUellfrxLP1uo4y4mIdPH52fiPIWyU51VwKKq2ZPWchIX/cU+zs9SnZfyHWwgwKpKg==} dependencies: - '@nrwl/web': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) - '@nx/devkit': 19.5.7(nx@19.5.7) - '@nx/js': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.5.7)(typescript@5.5.4) + '@nrwl/web': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) + '@nx/devkit': 19.7.3(nx@19.7.3) + '@nx/js': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7)(@types/node@18.19.21)(nx@19.7.3)(typescript@5.5.4) chalk: 4.1.2 detect-port: 1.6.1 http-server: 14.1.1 @@ -4038,14 +4051,14 @@ packages: - verdaccio dev: true - /@nx/workspace@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7): - resolution: {integrity: sha512-HtyRP0358QxKCpRkEffG0SAvZ9aIWvazMX6vlyHoJt8fkUuxN/wkkR80TTmTurqt87OpJK67ylUx0eOzzzm8Lw==} + /@nx/workspace@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7): + resolution: {integrity: sha512-FUHeOLCXdHEB1b6FiNU9swCZIKXbsGWRDfgHpHGeiZHp7uhH41W/EKTVukRxnQ+HXhE7zfxhn8KkllfaXIifPg==} dependencies: - '@nrwl/workspace': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) - '@nx/devkit': 19.5.7(nx@19.5.7) + '@nrwl/workspace': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) + '@nx/devkit': 19.7.3(nx@19.7.3) chalk: 4.1.2 enquirer: 2.3.6 - nx: 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + nx: 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) tslib: 2.6.3 yargs-parser: 21.1.1 transitivePeerDependencies: @@ -6055,7 +6068,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} @@ -6119,15 +6131,14 @@ packages: resolution: {integrity: sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==} dev: true - /axios@1.7.3: - resolution: {integrity: sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==} + /axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} dependencies: follow-redirects: 1.15.6 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: true /b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} @@ -6339,7 +6350,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true /bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -6398,7 +6408,7 @@ packages: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} dependencies: base64-js: 1.5.1 - ieee754: 1.1.13 + ieee754: 1.2.1 isarray: 1.0.0 dev: false @@ -6407,7 +6417,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -6575,7 +6584,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} @@ -6605,7 +6613,6 @@ packages: /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - dev: true /cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} @@ -6660,7 +6667,6 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - dev: true /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} @@ -6714,7 +6720,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /command-exists@1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} @@ -7157,7 +7162,6 @@ packages: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: clone: 1.0.4 - dev: true /defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} @@ -7194,7 +7198,6 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: true /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -8010,7 +8013,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: true /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -8045,7 +8047,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -8705,6 +8706,11 @@ packages: requiresBuild: true dev: true + /ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /inquirer@8.2.5: resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} engines: {node: '>=12.0.0'} @@ -8842,7 +8848,6 @@ packages: /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - dev: true /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -8926,7 +8931,6 @@ packages: /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - dev: true /is-unicode-supported@2.0.0: resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} @@ -9593,8 +9597,8 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + /lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true @@ -9749,7 +9753,6 @@ packages: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true /log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} @@ -9945,7 +9948,6 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: true /mime-db@1.53.0: resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} @@ -9957,7 +9959,6 @@ packages: engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: true /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} @@ -9980,7 +9981,6 @@ packages: /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - dev: true /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} @@ -10501,26 +10501,26 @@ packages: dev: true optional: true - /nx-cloud@19.0.0: - resolution: {integrity: sha512-Aq1vQD8yBIdb5jLVpzsqmu8yDmMvRVdjaM30Pp1hghhlSvorGBlpTwY+TccZJv/hBtVO+SpXK8SnnegRZMrxdw==} + /nx-cloud@19.1.0: + resolution: {integrity: sha512-f24vd5/57/MFSXNMfkerdDiK0EvScGOKO71iOWgJNgI1xVweDRmOA/EfjnPMRd5m+pnoPs/4A7DzuwSW0jZVyw==} hasBin: true dependencies: - '@nrwl/nx-cloud': 19.0.0 - axios: 1.7.3 + '@nrwl/nx-cloud': 19.1.0 + axios: 1.7.7 chalk: 4.1.2 dotenv: 10.0.0 fs-extra: 11.2.0 + ini: 4.1.3 node-machine-id: 1.1.12 open: 8.4.2 - strip-json-comments: 3.1.1 tar: 6.2.1 yargs-parser: 21.1.1 transitivePeerDependencies: - debug dev: true - /nx@19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7): - resolution: {integrity: sha512-AUmGgE19NB4m/7oHYQVdzZHtclVevD8w0/nNzzjDJE823T8oeoNhmc9MfCLz+/2l2KOp+Wqm+8LiG9/xWpXk0g==} + /nx@19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7): + resolution: {integrity: sha512-8F4CzKavSuOFv+uKVwXHc00Px0q40CWAYCW6NC5IgU3AMaJVumyHzgB8Sn+yfkaVgfVnZVqznOsyrbZUWuj/VA==} hasBin: true requiresBuild: true peerDependencies: @@ -10533,13 +10533,13 @@ packages: optional: true dependencies: '@napi-rs/wasm-runtime': 0.2.4 - '@nrwl/tao': 19.5.7(@swc-node/register@1.9.2)(@swc/core@1.5.7) + '@nrwl/tao': 19.7.3(@swc-node/register@1.9.2)(@swc/core@1.5.7) '@swc-node/register': 1.9.2(@swc/core@1.5.7)(@swc/types@0.1.12)(typescript@5.5.4) '@swc/core': 1.5.7(@swc/helpers@0.5.12) '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.7 - axios: 1.7.3 + axios: 1.7.7 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -10554,7 +10554,7 @@ packages: ignore: 5.3.1 jest-diff: 29.7.0 jsonc-parser: 3.2.0 - lines-and-columns: 2.0.4 + lines-and-columns: 2.0.3 minimatch: 9.0.3 node-machine-id: 1.1.12 npm-run-path: 4.0.1 @@ -10570,16 +10570,16 @@ packages: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 19.5.7 - '@nx/nx-darwin-x64': 19.5.7 - '@nx/nx-freebsd-x64': 19.5.7 - '@nx/nx-linux-arm-gnueabihf': 19.5.7 - '@nx/nx-linux-arm64-gnu': 19.5.7 - '@nx/nx-linux-arm64-musl': 19.5.7 - '@nx/nx-linux-x64-gnu': 19.5.7 - '@nx/nx-linux-x64-musl': 19.5.7 - '@nx/nx-win32-arm64-msvc': 19.5.7 - '@nx/nx-win32-x64-msvc': 19.5.7 + '@nx/nx-darwin-arm64': 19.7.3 + '@nx/nx-darwin-x64': 19.7.3 + '@nx/nx-freebsd-x64': 19.7.3 + '@nx/nx-linux-arm-gnueabihf': 19.7.3 + '@nx/nx-linux-arm64-gnu': 19.7.3 + '@nx/nx-linux-arm64-musl': 19.7.3 + '@nx/nx-linux-x64-gnu': 19.7.3 + '@nx/nx-linux-x64-musl': 19.7.3 + '@nx/nx-win32-arm64-msvc': 19.7.3 + '@nx/nx-win32-x64-msvc': 19.7.3 transitivePeerDependencies: - debug dev: true @@ -10613,7 +10613,6 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 - dev: true /onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} @@ -10667,7 +10666,6 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true /ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} @@ -11109,7 +11107,6 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: true /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} @@ -11272,7 +11269,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readable-stream@4.5.2: resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} @@ -11460,7 +11456,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} @@ -11696,7 +11691,6 @@ packages: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - dev: true /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -11753,7 +11747,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -12963,7 +12956,6 @@ packages: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.4 - dev: true /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -13021,7 +13013,7 @@ packages: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} requiresBuild: true dependencies: - string-width: 1.0.2 + string-width: 4.2.3 dev: true optional: true