diff --git a/package.json b/package.json index 1499891..d01713a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "nx": "15.4.1", "parse-version-string": "^1.0.1", "prettier": "2.7.1", + "semver": "^7.5.4", "ts-jest": "28.0.8", "ts-node": "10.9.1", "tslib": "^2.0.0", diff --git a/packages/plugin-tools/generators.json b/packages/plugin-tools/generators.json index b197e69..90337b9 100644 --- a/packages/plugin-tools/generators.json +++ b/packages/plugin-tools/generators.json @@ -42,6 +42,11 @@ "factory": "./src/generators/config", "schema": "./src/generators/config/schema.json", "description": "Configure workspace scope." + }, + "bump-packages": { + "factory": "./src/generators/bump-packages/generator", + "schema": "./src/generators/bump-packages/schema.json", + "description": "bump-packages generator" } } } diff --git a/packages/plugin-tools/package.json b/packages/plugin-tools/package.json index 4c50d0d..8760877 100644 --- a/packages/plugin-tools/package.json +++ b/packages/plugin-tools/package.json @@ -46,6 +46,7 @@ "lint-staged": "^13.0.0", "nativescript-theme-core": "~1.0.4", "sass": "^1.35.0", + "semver": "^7.5.4", "parse-version-string": "^1.0.1", "prettier": "^2.7.0", "pretty-data": "^0.40.0", diff --git a/packages/plugin-tools/src/generators/bump-packages/generator.spec.ts b/packages/plugin-tools/src/generators/bump-packages/generator.spec.ts new file mode 100644 index 0000000..87fc503 --- /dev/null +++ b/packages/plugin-tools/src/generators/bump-packages/generator.spec.ts @@ -0,0 +1,71 @@ +import { Tree, addProjectConfiguration } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import generator from './generator'; + +describe('bump-packages generator', () => { + let appTree: Tree; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + appTree.write('apps/app/package.json', JSON.stringify({ version: '1.0.0' })); + appTree.write('packages/lib/package.json', JSON.stringify({ version: '1.0.0' })); + appTree.write('packages/lib2/package.json', JSON.stringify({ version: '2.0.0' })); + addProjectConfiguration(appTree, 'app', { + root: 'apps/app', + projectType: 'application', + tags: ['tag1', 'tag2'], + }); + addProjectConfiguration(appTree, 'lib', { + root: 'packages/lib', + projectType: 'library', + tags: ['tag2'], + }); + addProjectConfiguration(appTree, 'lib2', { + root: 'packages/lib2', + projectType: 'library', + tags: ['tag1'], + }); + }); + + it('should bump all by patch', async () => { + await generator(appTree, { + targetVersion: 'patch', + }); + expect(JSON.parse(appTree.read('apps/app/package.json').toString()).version).toBe('1.0.1'); + expect(JSON.parse(appTree.read('packages/lib/package.json').toString()).version).toBe('1.0.1'); + expect(JSON.parse(appTree.read('packages/lib2/package.json').toString()).version).toBe('2.0.1'); + }); + + it('should filter by type', async () => { + await generator(appTree, { + targetVersion: 'patch', + projectType: 'library', + }); + expect(JSON.parse(appTree.read('apps/app/package.json').toString()).version).toBe('1.0.0'); + expect(JSON.parse(appTree.read('packages/lib/package.json').toString()).version).toBe('1.0.1'); + expect(JSON.parse(appTree.read('packages/lib2/package.json').toString()).version).toBe('2.0.1'); + }); + it('should filter by tag', async () => { + await generator(appTree, { + targetVersion: 'patch', + tags: 'tag2', + }); + expect(JSON.parse(appTree.read('apps/app/package.json').toString()).version).toBe('1.0.1'); + expect(JSON.parse(appTree.read('packages/lib/package.json').toString()).version).toBe('1.0.1'); + expect(JSON.parse(appTree.read('packages/lib2/package.json').toString()).version).toBe('2.0.0'); + }); + + it('should fail to set non-semver', async () => { + const gen = await generator(appTree, { + targetVersion: 'not-semver', + }).catch(() => 'failed'); + expect(gen).toBe('failed'); + }); + it('should fail to set version lower than the existing version', async () => { + const gen = await generator(appTree, { + targetVersion: '1.0.1', + }).catch(() => 'failed'); + expect(gen).toBe('failed'); + }); +}); diff --git a/packages/plugin-tools/src/generators/bump-packages/generator.ts b/packages/plugin-tools/src/generators/bump-packages/generator.ts new file mode 100644 index 0000000..0c570ea --- /dev/null +++ b/packages/plugin-tools/src/generators/bump-packages/generator.ts @@ -0,0 +1,72 @@ +import { ProjectConfiguration, Tree, formatFiles, getProjects, logger } from '@nrwl/devkit'; +import * as semver from 'semver'; +import { BumpPackagesGeneratorSchema } from './schema'; + +interface NormalizedSchema extends BumpPackagesGeneratorSchema { + projects: ProjectConfiguration[]; + versionBump: semver.ReleaseType | false; + fixedVersion: string; +} + +function isVersionBump(version: string): version is semver.ReleaseType { + return ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].includes(version); +} + +function normalizeOptions(tree: Tree, options: BumpPackagesGeneratorSchema): NormalizedSchema { + const filters = { + projectType: options.projectType ?? '', + tags: options.tags?.split(',').map((s) => s.trim()), + }; + console.log(filters); + const projects = options.projectName + ? [getProjects(tree).get(options.projectName)] + : Array.from(getProjects(tree).values()).filter((v) => { + if (filters.projectType) { + if (v.projectType !== filters.projectType) { + return false; + } + } + if (filters.tags) { + if (!v.tags.some((t) => filters.tags.includes(t))) { + return false; + } + } + return true; + }); + + const versionBump = isVersionBump(options.targetVersion) ? options.targetVersion : false; + if (!versionBump) { + if (!semver.parse(options.targetVersion)) { + throw new Error(`Invalid version ${options.targetVersion}`); + } + } + + return { + ...options, + projects, + versionBump, + fixedVersion: versionBump ? '' : options.targetVersion, + }; +} + +export default async function (tree: Tree, options: BumpPackagesGeneratorSchema) { + const normalizedOptions = normalizeOptions(tree, options); + normalizedOptions.projects.forEach((project) => { + const packageJson = tree.read(`${project.root}/package.json`); + if (!packageJson) { + throw new Error(`Could not find package.json for project ${project.name}`); + } + const parsedPackageJson = JSON.parse(packageJson.toString()); + const oldVersion = parsedPackageJson.version; + const newVersion = normalizedOptions.versionBump ? semver.inc(parsedPackageJson.version, normalizedOptions.versionBump) : normalizedOptions.fixedVersion; + if (!newVersion) { + throw new Error(`Could not bump version for project ${project.name}`); + } + if (semver.gte(oldVersion, newVersion)) { + throw new Error(`New version (${newVersion}) is not greater than old version (${oldVersion}) for project ${project.name}, Skipping project`); + } + parsedPackageJson.version = newVersion; + tree.write(`${project.root}/package.json`, JSON.stringify(parsedPackageJson, null, 2)); + }); + await formatFiles(tree); +} diff --git a/packages/plugin-tools/src/generators/bump-packages/schema.d.ts b/packages/plugin-tools/src/generators/bump-packages/schema.d.ts new file mode 100644 index 0000000..3331053 --- /dev/null +++ b/packages/plugin-tools/src/generators/bump-packages/schema.d.ts @@ -0,0 +1,6 @@ +export interface BumpPackagesGeneratorSchema { + targetVersion: string; + projectName?: string; + tags?: string; + projectType?: string; +} diff --git a/packages/plugin-tools/src/generators/bump-packages/schema.json b/packages/plugin-tools/src/generators/bump-packages/schema.json new file mode 100644 index 0000000..b1957c2 --- /dev/null +++ b/packages/plugin-tools/src/generators/bump-packages/schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "BumpPackages", + "title": "", + "type": "object", + "properties": { + "targetVersion": { + "x-prompt": "What's the new version (patch, minor, major) or explicit version number?.", + "type": "string", + "description": "Desired version to bump (patch, minor, major), or explicit version number (must comply with semver)", + "default": "patch", + "$default": { + "$source": "argv", + "index": 0 + }, + "anyOf": [ + { + "enum": ["major", "premajor", "minor", "preminor", "patch", "prepatch", "prerelease"] + }, + { + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + ], + "x-priority": "important" + }, + "projectName": { + "description": "Project to bump. If unspecified all projects are bumped", + "type": "string", + "x-dropdown": "projects" + }, + "tags": { + "type": "string", + "description": "Filter projects by tags (comma separated)", + "alias": "t" + }, + "projectType": { + "type": "string", + "enum": ["application", "library"], + "default": "library" + } + }, + "required": ["targetVersion"] +} diff --git a/yarn.lock b/yarn.lock index 1d95a0f..eb7322f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5902,6 +5902,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"