From fff65dab5e3013e7b2798106d8697e99eb260d3e Mon Sep 17 00:00:00 2001 From: Miroslav Vrana Date: Wed, 25 Jan 2023 21:58:11 +0100 Subject: [PATCH] feat: option to specify additional json files to update version number --- README.md | 1 + .../src/executors/version/index.spec.ts | 11 ++ .../semver/src/executors/version/index.ts | 3 + .../semver/src/executors/version/schema.d.ts | 1 + .../src/executors/version/utils/logger.ts | 2 + .../executors/version/utils/project.spec.ts | 95 ++++++++++++++- .../src/executors/version/utils/project.ts | 109 +++++++++++++++++- .../semver/src/executors/version/version.ts | 21 +++- 8 files changed, 240 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f7d7517e4..c386a3ebe 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Important: merge commits messages are ignored by the tool when calculating next | **`--skipCommitTypes`** | `string[]` | `[]` | treat commits with specified types as non invoking version bump ([details](https://github.com/jscutlery/semver#skipping-release-for-specific-types-of-commits)) | | **`--skipCommit`** | `boolean` | `false` | skips generating a new commit, leaves all changes in index, tag would be put on last commit ([details](https://github.com/jscutlery/semver#skipping-commit)) | | **`--commitMessageFormat`** | `string` | `undefined` | format the auto-generated message commit ([details](https://github.com/jscutlery/semver#commit-message-customization)) | +| **`--customJsonPath`** | `string[]` | `undefined` | another json files to update version. Values should be like: 'src/version.json:build.version'. Part after colon says path to attribute | | **`--preset`** | `string \| object` | `'angular'` | customize Conventional Changelog options ([details](https://github.com/jscutlery/semver#customizing-conventional-changelog-options)) | #### Overwrite default configuration diff --git a/packages/semver/src/executors/version/index.spec.ts b/packages/semver/src/executors/version/index.spec.ts index d8d10b44a..8779c17e8 100644 --- a/packages/semver/src/executors/version/index.spec.ts +++ b/packages/semver/src/executors/version/index.spec.ts @@ -30,6 +30,10 @@ describe('@jscutlery/semver:version', () => { project.updatePackageJson as jest.MockedFunction< typeof project.updatePackageJson >; + const mockUpdateCustomJsons = + project.updateCustomJsons as jest.MockedFunction< + typeof project.updateCustomJsons + >; const mockUpdateChangelog = changelog.updateChangelog as jest.MockedFunction< typeof changelog.updateChangelog >; @@ -107,6 +111,9 @@ describe('@jscutlery/semver:version', () => { mockUpdatePackageJson.mockImplementation(({ projectRoot }) => of(project.getPackageJsonPath(projectRoot)) ); + mockUpdateCustomJsons.mockImplementation( + ({ projectRoot, customJsonPaths }) => of([]) + ); mockCalculateChangelogChanges.mockReturnValue((source) => { source.subscribe(); return of(''); @@ -150,6 +157,10 @@ describe('@jscutlery/semver:version', () => { expect(mockUpdateChangelog).toHaveBeenCalledBefore( mockUpdatePackageJson as jest.Mock ); + expect(mockUpdatePackageJson).toHaveBeenCalledBefore( + mockUpdateCustomJsons as jest.Mock + ); + expect(mockCommit).toHaveBeenCalledBefore(mockCreateTag as jest.Mock); expect(mockCreateTag).toHaveBeenCalledBefore(mockTryPush as jest.Mock); expect(mockTryPush).toHaveBeenCalledBefore(mockRunPostTargets as jest.Mock); diff --git a/packages/semver/src/executors/version/index.ts b/packages/semver/src/executors/version/index.ts index a82ac273a..ec32d84b7 100644 --- a/packages/semver/src/executors/version/index.ts +++ b/packages/semver/src/executors/version/index.ts @@ -48,6 +48,7 @@ export default async function version( allowEmptyRelease, skipCommitTypes, skipCommit, + customJsonPaths, } = _normalizeOptions(options); const workspaceRoot = context.root; const projectName = context.projectName as string; @@ -128,6 +129,7 @@ export default async function version( changelogHeader, workspaceRoot, projectName, + customJsonPaths, skipProjectChangelog, commitMessage, dependencyUpdates, @@ -232,6 +234,7 @@ function _normalizeOptions(options: VersionBuilderSchema) { versionTagPrefix: options.tagPrefix ?? options.versionTagPrefix, commitMessageFormat: options.commitMessageFormat as string, skipCommit: options.skipCommit as boolean, + customJsonPaths: options.customJsonPaths as string[], preset: options.preset === 'conventional' ? 'conventionalcommits' diff --git a/packages/semver/src/executors/version/schema.d.ts b/packages/semver/src/executors/version/schema.d.ts index 7078b6907..a8110fd62 100644 --- a/packages/semver/src/executors/version/schema.d.ts +++ b/packages/semver/src/executors/version/schema.d.ts @@ -43,6 +43,7 @@ export interface VersionBuilderSchema { allowEmptyRelease?: boolean; skipCommitTypes?: string[]; commitMessageFormat?: string; + customJsonPaths?: string[]; preset: Preset; } diff --git a/packages/semver/src/executors/version/utils/logger.ts b/packages/semver/src/executors/version/utils/logger.ts index 0d45c571c..ca2d8b8e8 100644 --- a/packages/semver/src/executors/version/utils/logger.ts +++ b/packages/semver/src/executors/version/utils/logger.ts @@ -8,6 +8,7 @@ type Step = | 'warning' | 'calculate_version_success' | 'package_json_success' + | 'custom_json_success' | 'changelog_success' | 'tag_success' | 'post_target_success' @@ -22,6 +23,7 @@ const iconMap = new Map([ ['changelog_success', '📜'], ['commit_success', '📦'], ['package_json_success', '📝'], + ['custom_json_success', '📝'], ['post_target_success', '🎉'], ['tag_success', '🔖'], ['push_success', '🚀'], diff --git a/packages/semver/src/executors/version/utils/project.spec.ts b/packages/semver/src/executors/version/utils/project.spec.ts index 34524b83e..22002d5ed 100644 --- a/packages/semver/src/executors/version/utils/project.spec.ts +++ b/packages/semver/src/executors/version/utils/project.spec.ts @@ -1,7 +1,14 @@ import * as fs from 'fs'; import { lastValueFrom } from 'rxjs'; -import { readPackageJson } from './project'; +import { + readPackageJson, + updateCustomJson, + updateCustomJsons, +} from './project'; +import { PathLike } from 'fs'; +import { FileHandle } from 'fs/promises'; +import { Stream } from 'stream'; const fsPromises = fs.promises; @@ -15,3 +22,89 @@ describe('readPackageJson', () => { }); }); }); + +describe('Update custom version intojson', () => { + it('should update version in JSON content - variant 1', async () => { + jest.spyOn(fsPromises, 'access').mockResolvedValue(undefined); + jest + .spyOn(fsPromises, 'readFile') + .mockResolvedValue(`{"info":{"version":"2.1.0"}}`); + jest + .spyOn(fsPromises, 'writeFile') + .mockImplementation( + async ( + file: PathLike | FileHandle, + data: + | string + | NodeJS.ArrayBufferView + | Iterable + | AsyncIterable + | Stream + ) => { + expect(data).toBe(`{"info":{"version":"1.2.3"}}\n`); + return; + } + ); + const s = updateCustomJson({ + newVersion: '1.2.3', + projectName: 'test', + dryRun: false, + projectRoot: 'test', + customJsonPath: 'src/version.json:info.version', + }); + await lastValueFrom(s); + }); + + it('should update version in multiple JSON contents', async () => { + const result: string[] = []; + jest.spyOn(fsPromises, 'access').mockResolvedValue(undefined); + jest + .spyOn(fsPromises, 'readFile') + .mockImplementation(async (path: PathLike | FileHandle) => { + if (path.toString().includes('file1.json')) { + return '{"version":"0.0.0"}'; + } + if (path.toString().includes('file2.json')) { + return '{"info":{"version":"0.0.0"}}'; + } + return ''; + }); + jest + .spyOn(fsPromises, 'writeFile') + .mockImplementation( + async ( + file: PathLike | FileHandle, + data: + | string + | NodeJS.ArrayBufferView + | Iterable + | AsyncIterable + | Stream + ) => { + if (file.toString().includes('file1.json')) { + result.push(data as string); + } + if (file.toString().includes('file2.json')) { + result.push(data as string); + } + } + ); + + const s = updateCustomJsons({ + newVersion: '1.2.3', + projectName: 'test', + dryRun: false, + projectRoot: 'test', + customJsonPaths: [ + 'src/file1.json:version', + 'src/file2.json:info.version', + ], + }); + await lastValueFrom(s); + + expect(result).toContainAllValues([ + '{"version":"1.2.3"}\n', + '{"info":{"version":"1.2.3"}}\n', + ]); + }); +}); diff --git a/packages/semver/src/executors/version/utils/project.ts b/packages/semver/src/executors/version/utils/project.ts index 12681ea4c..d75eb4305 100644 --- a/packages/semver/src/executors/version/utils/project.ts +++ b/packages/semver/src/executors/version/utils/project.ts @@ -1,5 +1,5 @@ import { resolve } from 'path'; -import { map, of, switchMap, type Observable } from 'rxjs'; +import { map, of, switchMap, type Observable, concat, toArray } from 'rxjs'; import { readFileIfExists, readJsonFile, writeFile } from './filesystem'; import { logStep } from './logger'; import * as detectIndent from 'detect-indent'; @@ -10,6 +10,10 @@ export function readPackageJson(projectRoot: string): Observable<{ return readJsonFile(getPackageJsonPath(projectRoot)); } +export function getCustomJsonPath(projectRoot: string, jsonPath: string) { + return resolve(projectRoot, jsonPath); +} + export function getPackageJsonPath(projectRoot: string) { return resolve(projectRoot, 'package.json'); } @@ -54,14 +58,117 @@ export function updatePackageJson({ ); } +/** + * Safely update multiple custom *.json files. + */ +export function updateCustomJsons({ + newVersion, + projectRoot, + projectName, + customJsonPaths, + dryRun, +}: { + newVersion: string; + projectRoot: string; + projectName: string; + customJsonPaths: string[]; + dryRun: boolean; +}): Observable<(string | null)[]> { + if (dryRun || !customJsonPaths) { + return of([]); + } + + return concat( + ...customJsonPaths.map((customJsonPath) => + updateCustomJson({ + newVersion, + projectRoot, + projectName, + customJsonPath, + dryRun, + }) + ) + ).pipe(toArray()); +} + +/** + * Safely update custom *.json file. + */ +export function updateCustomJson({ + newVersion, + projectRoot, + projectName, + customJsonPath, + dryRun, +}: { + newVersion: string; + projectRoot: string; + projectName: string; + customJsonPath: string; + dryRun: boolean; +}): Observable { + if (dryRun) { + return of(null); + } + const [filePath, attrPath] = customJsonPath.split(':'); + const path = getCustomJsonPath(projectRoot, filePath); + + return readFileIfExists(path).pipe( + switchMap((customJson) => { + if (!customJson.length) { + return of(null); + } + + const newCustomJson = _updateCustomJsonVersion( + customJson, + attrPath, + newVersion + ); + + return writeFile(path, newCustomJson).pipe( + logStep({ + step: 'custom_json_success', + message: `Updated ${filePath} version.`, + projectName, + }), + map(() => path) + ); + }) + ); +} + function _updatePackageVersion(packageJson: string, version: string): string { const data = JSON.parse(packageJson); const { indent } = detectIndent(packageJson); return _stringifyJson({ ...data, version }, indent); } +function _updateCustomJsonVersion( + contentJson: string, + attr: string, + version: string +): string { + const data = JSON.parse(contentJson); + const { indent } = detectIndent(contentJson); + const keys = attr.split('.'); + const patch = _createPatch(keys, version) as object; + + return _stringifyJson({ ...data, ...patch }, indent); +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any function _stringifyJson(data: any, indent: string | number): string { // We need to add a newline at the end so that Prettier will not complain about the new file. return JSON.stringify(data, null, indent).concat('\n'); } + +function _createPatch(attrPath: string[], version: string): string | object { + const attr = attrPath.shift(); + if (attr) { + return { + [attr]: _createPatch(attrPath, version), + }; + } else { + return version; + } +} diff --git a/packages/semver/src/executors/version/version.ts b/packages/semver/src/executors/version/version.ts index f21e45e17..5ab6d336a 100644 --- a/packages/semver/src/executors/version/version.ts +++ b/packages/semver/src/executors/version/version.ts @@ -9,7 +9,7 @@ import { import { commit } from './utils/commit'; import { addToStage, createTag, getLastCommitHash } from './utils/git'; import { logStep } from './utils/logger'; -import { updatePackageJson } from './utils/project'; +import { updateCustomJsons, updatePackageJson } from './utils/project'; import { getProjectRoots } from './utils/workspace'; export type Version = @@ -35,6 +35,7 @@ export interface CommonVersionOptions { skipCommit: boolean; commitMessage: string; projectName: string; + customJsonPaths: string[]; skipProjectChangelog: boolean; dependencyUpdates: Version[]; preset: Preset; @@ -175,6 +176,24 @@ export function versionProject({ ) ) ), + concatMap(() => + updateCustomJsons({ + newVersion, + projectRoot, + projectName, + customJsonPaths: options.customJsonPaths, + dryRun, + }).pipe( + concatMap((files) => { + const paths: string[] = files.filter((v) => !!v) as string[]; + if (files.length !== 0) { + return addToStage({ paths, dryRun }); + } else { + return of(undefined); + } + }) + ) + ), concatMap(() => commit({ skipCommit,