Skip to content

Commit

Permalink
feat: option to specify additional json files to update version number
Browse files Browse the repository at this point in the history
  • Loading branch information
mvrana-cen81948 committed Jan 31, 2023
1 parent c1a7f9e commit fff65da
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions packages/semver/src/executors/version/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
>;
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions packages/semver/src/executors/version/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -128,6 +129,7 @@ export default async function version(
changelogHeader,
workspaceRoot,
projectName,
customJsonPaths,
skipProjectChangelog,
commitMessage,
dependencyUpdates,
Expand Down Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/semver/src/executors/version/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface VersionBuilderSchema {
allowEmptyRelease?: boolean;
skipCommitTypes?: string[];
commitMessageFormat?: string;
customJsonPaths?: string[];
preset: Preset;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/semver/src/executors/version/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Step =
| 'warning'
| 'calculate_version_success'
| 'package_json_success'
| 'custom_json_success'
| 'changelog_success'
| 'tag_success'
| 'post_target_success'
Expand All @@ -22,6 +23,7 @@ const iconMap = new Map<Step, string>([
['changelog_success', '📜'],
['commit_success', '📦'],
['package_json_success', '📝'],
['custom_json_success', '📝'],
['post_target_success', '🎉'],
['tag_success', '🔖'],
['push_success', '🚀'],
Expand Down
95 changes: 94 additions & 1 deletion packages/semver/src/executors/version/utils/project.spec.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<string | NodeJS.ArrayBufferView>
| AsyncIterable<string | NodeJS.ArrayBufferView>
| 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<string | NodeJS.ArrayBufferView>
| AsyncIterable<string | NodeJS.ArrayBufferView>
| 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',
]);
});
});
109 changes: 108 additions & 1 deletion packages/semver/src/executors/version/utils/project.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
}
Expand Down Expand Up @@ -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<string | null> {
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;
}
}
21 changes: 20 additions & 1 deletion packages/semver/src/executors/version/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -35,6 +35,7 @@ export interface CommonVersionOptions {
skipCommit: boolean;
commitMessage: string;
projectName: string;
customJsonPaths: string[];
skipProjectChangelog: boolean;
dependencyUpdates: Version[];
preset: Preset;
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit fff65da

Please sign in to comment.