Skip to content

Commit fff65da

Browse files
feat: option to specify additional json files to update version number
1 parent c1a7f9e commit fff65da

File tree

8 files changed

+240
-3
lines changed

8 files changed

+240
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Important: merge commits messages are ignored by the tool when calculating next
9797
| **`--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)) |
9898
| **`--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)) |
9999
| **`--commitMessageFormat`** | `string` | `undefined` | format the auto-generated message commit ([details](https://github.com/jscutlery/semver#commit-message-customization)) |
100+
| **`--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 |
100101
| **`--preset`** | `string \| object` | `'angular'` | customize Conventional Changelog options ([details](https://github.com/jscutlery/semver#customizing-conventional-changelog-options)) |
101102

102103
#### Overwrite default configuration

packages/semver/src/executors/version/index.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ describe('@jscutlery/semver:version', () => {
3030
project.updatePackageJson as jest.MockedFunction<
3131
typeof project.updatePackageJson
3232
>;
33+
const mockUpdateCustomJsons =
34+
project.updateCustomJsons as jest.MockedFunction<
35+
typeof project.updateCustomJsons
36+
>;
3337
const mockUpdateChangelog = changelog.updateChangelog as jest.MockedFunction<
3438
typeof changelog.updateChangelog
3539
>;
@@ -107,6 +111,9 @@ describe('@jscutlery/semver:version', () => {
107111
mockUpdatePackageJson.mockImplementation(({ projectRoot }) =>
108112
of(project.getPackageJsonPath(projectRoot))
109113
);
114+
mockUpdateCustomJsons.mockImplementation(
115+
({ projectRoot, customJsonPaths }) => of([])
116+
);
110117
mockCalculateChangelogChanges.mockReturnValue((source) => {
111118
source.subscribe();
112119
return of('');
@@ -150,6 +157,10 @@ describe('@jscutlery/semver:version', () => {
150157
expect(mockUpdateChangelog).toHaveBeenCalledBefore(
151158
mockUpdatePackageJson as jest.Mock
152159
);
160+
expect(mockUpdatePackageJson).toHaveBeenCalledBefore(
161+
mockUpdateCustomJsons as jest.Mock
162+
);
163+
153164
expect(mockCommit).toHaveBeenCalledBefore(mockCreateTag as jest.Mock);
154165
expect(mockCreateTag).toHaveBeenCalledBefore(mockTryPush as jest.Mock);
155166
expect(mockTryPush).toHaveBeenCalledBefore(mockRunPostTargets as jest.Mock);

packages/semver/src/executors/version/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default async function version(
4848
allowEmptyRelease,
4949
skipCommitTypes,
5050
skipCommit,
51+
customJsonPaths,
5152
} = _normalizeOptions(options);
5253
const workspaceRoot = context.root;
5354
const projectName = context.projectName as string;
@@ -128,6 +129,7 @@ export default async function version(
128129
changelogHeader,
129130
workspaceRoot,
130131
projectName,
132+
customJsonPaths,
131133
skipProjectChangelog,
132134
commitMessage,
133135
dependencyUpdates,
@@ -232,6 +234,7 @@ function _normalizeOptions(options: VersionBuilderSchema) {
232234
versionTagPrefix: options.tagPrefix ?? options.versionTagPrefix,
233235
commitMessageFormat: options.commitMessageFormat as string,
234236
skipCommit: options.skipCommit as boolean,
237+
customJsonPaths: options.customJsonPaths as string[],
235238
preset:
236239
options.preset === 'conventional'
237240
? 'conventionalcommits'

packages/semver/src/executors/version/schema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface VersionBuilderSchema {
4343
allowEmptyRelease?: boolean;
4444
skipCommitTypes?: string[];
4545
commitMessageFormat?: string;
46+
customJsonPaths?: string[];
4647
preset: Preset;
4748
}
4849

packages/semver/src/executors/version/utils/logger.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Step =
88
| 'warning'
99
| 'calculate_version_success'
1010
| 'package_json_success'
11+
| 'custom_json_success'
1112
| 'changelog_success'
1213
| 'tag_success'
1314
| 'post_target_success'
@@ -22,6 +23,7 @@ const iconMap = new Map<Step, string>([
2223
['changelog_success', '📜'],
2324
['commit_success', '📦'],
2425
['package_json_success', '📝'],
26+
['custom_json_success', '📝'],
2527
['post_target_success', '🎉'],
2628
['tag_success', '🔖'],
2729
['push_success', '🚀'],

packages/semver/src/executors/version/utils/project.spec.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import * as fs from 'fs';
22
import { lastValueFrom } from 'rxjs';
33

4-
import { readPackageJson } from './project';
4+
import {
5+
readPackageJson,
6+
updateCustomJson,
7+
updateCustomJsons,
8+
} from './project';
9+
import { PathLike } from 'fs';
10+
import { FileHandle } from 'fs/promises';
11+
import { Stream } from 'stream';
512

613
const fsPromises = fs.promises;
714

@@ -15,3 +22,89 @@ describe('readPackageJson', () => {
1522
});
1623
});
1724
});
25+
26+
describe('Update custom version intojson', () => {
27+
it('should update version in JSON content - variant 1', async () => {
28+
jest.spyOn(fsPromises, 'access').mockResolvedValue(undefined);
29+
jest
30+
.spyOn(fsPromises, 'readFile')
31+
.mockResolvedValue(`{"info":{"version":"2.1.0"}}`);
32+
jest
33+
.spyOn(fsPromises, 'writeFile')
34+
.mockImplementation(
35+
async (
36+
file: PathLike | FileHandle,
37+
data:
38+
| string
39+
| NodeJS.ArrayBufferView
40+
| Iterable<string | NodeJS.ArrayBufferView>
41+
| AsyncIterable<string | NodeJS.ArrayBufferView>
42+
| Stream
43+
) => {
44+
expect(data).toBe(`{"info":{"version":"1.2.3"}}\n`);
45+
return;
46+
}
47+
);
48+
const s = updateCustomJson({
49+
newVersion: '1.2.3',
50+
projectName: 'test',
51+
dryRun: false,
52+
projectRoot: 'test',
53+
customJsonPath: 'src/version.json:info.version',
54+
});
55+
await lastValueFrom(s);
56+
});
57+
58+
it('should update version in multiple JSON contents', async () => {
59+
const result: string[] = [];
60+
jest.spyOn(fsPromises, 'access').mockResolvedValue(undefined);
61+
jest
62+
.spyOn(fsPromises, 'readFile')
63+
.mockImplementation(async (path: PathLike | FileHandle) => {
64+
if (path.toString().includes('file1.json')) {
65+
return '{"version":"0.0.0"}';
66+
}
67+
if (path.toString().includes('file2.json')) {
68+
return '{"info":{"version":"0.0.0"}}';
69+
}
70+
return '';
71+
});
72+
jest
73+
.spyOn(fsPromises, 'writeFile')
74+
.mockImplementation(
75+
async (
76+
file: PathLike | FileHandle,
77+
data:
78+
| string
79+
| NodeJS.ArrayBufferView
80+
| Iterable<string | NodeJS.ArrayBufferView>
81+
| AsyncIterable<string | NodeJS.ArrayBufferView>
82+
| Stream
83+
) => {
84+
if (file.toString().includes('file1.json')) {
85+
result.push(data as string);
86+
}
87+
if (file.toString().includes('file2.json')) {
88+
result.push(data as string);
89+
}
90+
}
91+
);
92+
93+
const s = updateCustomJsons({
94+
newVersion: '1.2.3',
95+
projectName: 'test',
96+
dryRun: false,
97+
projectRoot: 'test',
98+
customJsonPaths: [
99+
'src/file1.json:version',
100+
'src/file2.json:info.version',
101+
],
102+
});
103+
await lastValueFrom(s);
104+
105+
expect(result).toContainAllValues([
106+
'{"version":"1.2.3"}\n',
107+
'{"info":{"version":"1.2.3"}}\n',
108+
]);
109+
});
110+
});

packages/semver/src/executors/version/utils/project.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { resolve } from 'path';
2-
import { map, of, switchMap, type Observable } from 'rxjs';
2+
import { map, of, switchMap, type Observable, concat, toArray } from 'rxjs';
33
import { readFileIfExists, readJsonFile, writeFile } from './filesystem';
44
import { logStep } from './logger';
55
import * as detectIndent from 'detect-indent';
@@ -10,6 +10,10 @@ export function readPackageJson(projectRoot: string): Observable<{
1010
return readJsonFile(getPackageJsonPath(projectRoot));
1111
}
1212

13+
export function getCustomJsonPath(projectRoot: string, jsonPath: string) {
14+
return resolve(projectRoot, jsonPath);
15+
}
16+
1317
export function getPackageJsonPath(projectRoot: string) {
1418
return resolve(projectRoot, 'package.json');
1519
}
@@ -54,14 +58,117 @@ export function updatePackageJson({
5458
);
5559
}
5660

61+
/**
62+
* Safely update multiple custom *.json files.
63+
*/
64+
export function updateCustomJsons({
65+
newVersion,
66+
projectRoot,
67+
projectName,
68+
customJsonPaths,
69+
dryRun,
70+
}: {
71+
newVersion: string;
72+
projectRoot: string;
73+
projectName: string;
74+
customJsonPaths: string[];
75+
dryRun: boolean;
76+
}): Observable<(string | null)[]> {
77+
if (dryRun || !customJsonPaths) {
78+
return of([]);
79+
}
80+
81+
return concat(
82+
...customJsonPaths.map((customJsonPath) =>
83+
updateCustomJson({
84+
newVersion,
85+
projectRoot,
86+
projectName,
87+
customJsonPath,
88+
dryRun,
89+
})
90+
)
91+
).pipe(toArray());
92+
}
93+
94+
/**
95+
* Safely update custom *.json file.
96+
*/
97+
export function updateCustomJson({
98+
newVersion,
99+
projectRoot,
100+
projectName,
101+
customJsonPath,
102+
dryRun,
103+
}: {
104+
newVersion: string;
105+
projectRoot: string;
106+
projectName: string;
107+
customJsonPath: string;
108+
dryRun: boolean;
109+
}): Observable<string | null> {
110+
if (dryRun) {
111+
return of(null);
112+
}
113+
const [filePath, attrPath] = customJsonPath.split(':');
114+
const path = getCustomJsonPath(projectRoot, filePath);
115+
116+
return readFileIfExists(path).pipe(
117+
switchMap((customJson) => {
118+
if (!customJson.length) {
119+
return of(null);
120+
}
121+
122+
const newCustomJson = _updateCustomJsonVersion(
123+
customJson,
124+
attrPath,
125+
newVersion
126+
);
127+
128+
return writeFile(path, newCustomJson).pipe(
129+
logStep({
130+
step: 'custom_json_success',
131+
message: `Updated ${filePath} version.`,
132+
projectName,
133+
}),
134+
map(() => path)
135+
);
136+
})
137+
);
138+
}
139+
57140
function _updatePackageVersion(packageJson: string, version: string): string {
58141
const data = JSON.parse(packageJson);
59142
const { indent } = detectIndent(packageJson);
60143
return _stringifyJson({ ...data, version }, indent);
61144
}
62145

146+
function _updateCustomJsonVersion(
147+
contentJson: string,
148+
attr: string,
149+
version: string
150+
): string {
151+
const data = JSON.parse(contentJson);
152+
const { indent } = detectIndent(contentJson);
153+
const keys = attr.split('.');
154+
const patch = _createPatch(keys, version) as object;
155+
156+
return _stringifyJson({ ...data, ...patch }, indent);
157+
}
158+
63159
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64160
function _stringifyJson(data: any, indent: string | number): string {
65161
// We need to add a newline at the end so that Prettier will not complain about the new file.
66162
return JSON.stringify(data, null, indent).concat('\n');
67163
}
164+
165+
function _createPatch(attrPath: string[], version: string): string | object {
166+
const attr = attrPath.shift();
167+
if (attr) {
168+
return {
169+
[attr]: _createPatch(attrPath, version),
170+
};
171+
} else {
172+
return version;
173+
}
174+
}

packages/semver/src/executors/version/version.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { commit } from './utils/commit';
1010
import { addToStage, createTag, getLastCommitHash } from './utils/git';
1111
import { logStep } from './utils/logger';
12-
import { updatePackageJson } from './utils/project';
12+
import { updateCustomJsons, updatePackageJson } from './utils/project';
1313
import { getProjectRoots } from './utils/workspace';
1414

1515
export type Version =
@@ -35,6 +35,7 @@ export interface CommonVersionOptions {
3535
skipCommit: boolean;
3636
commitMessage: string;
3737
projectName: string;
38+
customJsonPaths: string[];
3839
skipProjectChangelog: boolean;
3940
dependencyUpdates: Version[];
4041
preset: Preset;
@@ -175,6 +176,24 @@ export function versionProject({
175176
)
176177
)
177178
),
179+
concatMap(() =>
180+
updateCustomJsons({
181+
newVersion,
182+
projectRoot,
183+
projectName,
184+
customJsonPaths: options.customJsonPaths,
185+
dryRun,
186+
}).pipe(
187+
concatMap((files) => {
188+
const paths: string[] = files.filter((v) => !!v) as string[];
189+
if (files.length !== 0) {
190+
return addToStage({ paths, dryRun });
191+
} else {
192+
return of(undefined);
193+
}
194+
})
195+
)
196+
),
178197
concatMap(() =>
179198
commit({
180199
skipCommit,

0 commit comments

Comments
 (0)