Skip to content

Commit 2fdf2c3

Browse files
committed
feat: ✅ add --sync-versions option
Closes #5
1 parent 648b711 commit 2fdf2c3

File tree

4 files changed

+213
-89
lines changed

4 files changed

+213
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as childProcess from '@lerna/child-process';
22
import { MockBuilderContext } from '@nrwl/workspace/testing';
3+
import * as fs from 'fs';
34
import * as standardVersion from 'standard-version';
4-
55
import { getMockContext } from '../../utils/testing';
66
import { runBuilder } from './builder';
77
import { VersionBuilderSchema } from './schema';
@@ -21,89 +21,159 @@ const options: VersionBuilderSchema = {
2121
describe('@jscutlery/semver:version', () => {
2222
let context: MockBuilderContext;
2323

24-
beforeEach(async () => {
25-
context = await getMockContext();
26-
context.logger.error = jest.fn();
27-
context.getProjectMetadata = jest
28-
.fn()
29-
.mockResolvedValue({ root: '/root/lib' });
24+
beforeEach(() => {
25+
jest
26+
.spyOn(fs, 'readFile')
27+
.mockImplementation((...args: Parameters<typeof fs.readFile>) => {
28+
const callback = args[args.length - 1] as Function;
29+
callback(
30+
null,
31+
JSON.stringify({
32+
version: 1,
33+
projects: {
34+
a: {
35+
root: 'packages/a',
36+
},
37+
b: {
38+
root: 'packages/b',
39+
},
40+
},
41+
})
42+
);
43+
});
3044
});
3145

32-
it('runs standard-version with project options', async () => {
33-
const output = await runBuilder(options, context).toPromise();
34-
35-
expect(output).toEqual(expect.objectContaining({ success: true }));
36-
expect(standardVersion).toBeCalledWith(
37-
expect.objectContaining({
38-
silent: false,
39-
preset: expect.stringContaining('conventional-changelog-angular'),
40-
dryRun: false,
41-
noVerify: false,
42-
firstRelease: false,
43-
path: '/root/lib',
44-
infile: '/root/lib/CHANGELOG.md',
45-
bumpFiles: ['/root/lib/package.json'],
46-
packageFiles: ['/root/lib/package.json'],
47-
})
48-
);
46+
describe('Independent version', () => {
47+
beforeEach(async () => {
48+
context = await getMockContext();
49+
context.logger.error = jest.fn();
50+
context.getProjectMetadata = jest
51+
.fn()
52+
.mockResolvedValue({ root: '/root/packages/lib' });
53+
});
54+
it('runs standard-version with project options', async () => {
55+
const output = await runBuilder(options, context).toPromise();
56+
57+
expect(output).toEqual(expect.objectContaining({ success: true }));
58+
expect(standardVersion).toBeCalledWith(
59+
expect.objectContaining({
60+
silent: false,
61+
preset: expect.stringContaining('conventional-changelog-angular'),
62+
dryRun: false,
63+
noVerify: false,
64+
firstRelease: false,
65+
path: '/root/packages/lib',
66+
infile: '/root/packages/lib/CHANGELOG.md',
67+
bumpFiles: ['/root/packages/lib/package.json'],
68+
packageFiles: ['/root/packages/lib/package.json'],
69+
})
70+
);
71+
});
72+
73+
it('should not push to Git by default', async () => {
74+
await runBuilder(options, context).toPromise();
75+
76+
expect(childProcess.exec).not.toHaveBeenCalled();
77+
});
78+
79+
it('should push to Git with right options', async () => {
80+
await runBuilder(
81+
{ ...options, push: true, remote: 'origin', baseBranch: 'main' },
82+
context
83+
).toPromise();
84+
85+
expect(childProcess.exec).toHaveBeenCalledWith(
86+
'git',
87+
expect.arrayContaining([
88+
'push',
89+
'--follow-tags',
90+
'--atomic',
91+
'origin',
92+
'main',
93+
])
94+
);
95+
});
96+
97+
it(`should push to Git and add '--no-verify' option when asked for`, async () => {
98+
await runBuilder(
99+
{
100+
...options,
101+
push: true,
102+
remote: 'origin',
103+
baseBranch: 'main',
104+
noVerify: true,
105+
},
106+
context
107+
).toPromise();
108+
109+
expect(childProcess.exec).toHaveBeenCalledWith(
110+
'git',
111+
expect.arrayContaining([
112+
'push',
113+
'--follow-tags',
114+
'--no-verify',
115+
'--atomic',
116+
'origin',
117+
'main',
118+
])
119+
);
120+
});
121+
122+
it('should fail if Git config is missing', async () => {
123+
const output = await runBuilder(
124+
{ ...options, push: true, remote: undefined, baseBranch: undefined },
125+
context
126+
).toPromise();
127+
128+
expect(context.logger.error).toBeCalled();
129+
expect(output).toEqual(expect.objectContaining({ success: false }));
130+
});
49131
});
50132

51-
it('should not push to Git by default', async () => {
52-
await runBuilder(options, context).toPromise();
53-
54-
expect(childProcess.exec).not.toHaveBeenCalled();
55-
});
56-
57-
it('should push to Git with right options', async () => {
58-
await runBuilder(
59-
{ ...options, push: true, remote: 'origin', baseBranch: 'main' },
60-
context
61-
).toPromise();
62-
63-
expect(childProcess.exec).toHaveBeenCalledWith(
64-
'git',
65-
expect.arrayContaining([
66-
'push',
67-
'--follow-tags',
68-
'--atomic',
69-
'origin',
70-
'main',
71-
])
72-
);
73-
});
74-
75-
it(`should push to Git and add '--no-verify' option when asked for`, async () => {
76-
await runBuilder(
77-
{
78-
...options,
79-
push: true,
80-
remote: 'origin',
81-
baseBranch: 'main',
82-
noVerify: true,
83-
},
84-
context
85-
).toPromise();
86-
87-
expect(childProcess.exec).toHaveBeenCalledWith(
88-
'git',
89-
expect.arrayContaining([
90-
'push',
91-
'--follow-tags',
92-
'--no-verify',
93-
'--atomic',
94-
'origin',
95-
'main',
96-
])
97-
);
98-
});
99-
100-
it('should fail if Git config is missing', async () => {
101-
const output = await runBuilder(
102-
{ ...options, push: true, remote: undefined, baseBranch: undefined },
103-
context
104-
).toPromise();
105-
106-
expect(context.logger.error).toBeCalled()
107-
expect(output).toEqual(expect.objectContaining({ success: false }));
133+
describe('Sync version', () => {
134+
beforeEach(async () => {
135+
context = await getMockContext();
136+
137+
/* With the sync version, the builder runs on the workspace. */
138+
context.getProjectMetadata = jest
139+
.fn()
140+
.mockResolvedValue({ root: '/root' });
141+
});
142+
143+
it('should sync projects versions', async () => {
144+
const output = await runBuilder(
145+
{
146+
...options,
147+
push: true,
148+
remote: undefined,
149+
baseBranch: undefined,
150+
/* Enable sync versions. */
151+
syncVersions: true,
152+
},
153+
context
154+
).toPromise();
155+
156+
expect(fs.readFile).toHaveBeenLastCalledWith(
157+
'/root/workspace.json',
158+
'utf-8',
159+
expect.any(Function)
160+
);
161+
expect(standardVersion).toBeCalledWith(
162+
expect.objectContaining({
163+
silent: false,
164+
preset: expect.stringContaining('conventional-changelog-angular'),
165+
dryRun: false,
166+
noVerify: false,
167+
firstRelease: false,
168+
path: '/root',
169+
infile: '/root/CHANGELOG.md',
170+
bumpFiles: [
171+
'/root/packages/a/package.json',
172+
'/root/packages/b/package.json',
173+
],
174+
packageFiles: ['/root/package.json'],
175+
})
176+
);
177+
});
108178
});
109179
});

packages/semver/src/builders/version/builder.ts

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
1+
import {
2+
BuilderContext,
3+
BuilderOutput,
4+
createBuilder,
5+
} from '@angular-devkit/architect';
26
import { noop } from '@angular-devkit/schematics';
37
import { exec } from '@lerna/child-process';
8+
import { readFile } from 'fs';
49
import { resolve } from 'path';
510
import { defer, from, Observable, of, throwError } from 'rxjs';
6-
import { catchError, mapTo, switchMap, switchMapTo } from 'rxjs/operators';
11+
import { catchError, map, mapTo, switchMap, switchMapTo } from 'rxjs/operators';
712
import * as standardVersion from 'standard-version';
8-
13+
import { promisify } from 'util';
914
import { VersionBuilderSchema } from './schema';
1015

1116
async function getProjectRoot(context: BuilderContext): Promise<string> {
@@ -94,22 +99,39 @@ export function runBuilder(
9499
options: VersionBuilderSchema,
95100
context: BuilderContext
96101
): Observable<BuilderOutput> {
97-
const { push, remote, dryRun, baseBranch, noVerify, firstRelease } = options;
102+
const {
103+
push,
104+
remote,
105+
dryRun,
106+
baseBranch,
107+
noVerify,
108+
firstRelease,
109+
syncVersions,
110+
} = options;
98111

99112
return from(getProjectRoot(context)).pipe(
100113
switchMap((projectRoot) =>
101-
standardVersion({
114+
getPackageFiles(projectRoot).pipe(
115+
map((packageFiles) => ({ projectRoot, packageFiles }))
116+
)
117+
),
118+
switchMap(({ projectRoot, packageFiles }) => {
119+
const bumpFiles = syncVersions
120+
? packageFiles
121+
: [resolve(projectRoot, 'package.json')];
122+
123+
return standardVersion({
102124
silent: false,
103125
path: projectRoot,
104126
dryRun,
105127
noVerify,
106128
firstRelease,
107129
infile: resolve(projectRoot, 'CHANGELOG.md'),
108130
packageFiles: [resolve(projectRoot, 'package.json')],
109-
bumpFiles: [resolve(projectRoot, 'package.json')],
131+
bumpFiles,
110132
preset: require.resolve('conventional-changelog-angular'),
111-
})
112-
),
133+
});
134+
}),
113135
push && dryRun === false
114136
? switchMapTo(
115137
tryPushingToGitRemote({
@@ -128,4 +150,30 @@ export function runBuilder(
128150
);
129151
}
130152

153+
export interface WorkspaceDefinition {
154+
projects: {
155+
[key: string]: {
156+
root: string;
157+
};
158+
};
159+
}
160+
161+
export function getPackageFiles(projectRoot: string): Observable<string[]> {
162+
return getWorkspaceDefinition(projectRoot).pipe(
163+
map((workspaceDefinition) =>
164+
Object.values(workspaceDefinition.projects).map((project) =>
165+
resolve(projectRoot, project.root, 'package.json')
166+
)
167+
)
168+
);
169+
}
170+
171+
export function getWorkspaceDefinition(
172+
projectRoot: string
173+
): Observable<WorkspaceDefinition> {
174+
return from(
175+
promisify(readFile)(resolve(projectRoot, 'workspace.json'), 'utf-8')
176+
).pipe(map((data) => JSON.parse(data)));
177+
}
178+
131179
export default createBuilder(runBuilder);

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

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export interface VersionBuilderSchema extends JsonObject {
77
push?: boolean;
88
remote?: string;
99
baseBranch?: string;
10+
syncVersions?: boolean;
1011
}

packages/semver/src/builders/version/schema.json

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
"description": "Pushes against git base branch.",
3535
"type": "string",
3636
"default": "main"
37+
},
38+
"syncVersions": {
39+
"description": "Sync all package versions.",
40+
"type": "boolean",
41+
"default": false
3742
}
3843
},
3944
"required": []

0 commit comments

Comments
 (0)