Skip to content

Commit 44404c3

Browse files
authored
feat: release UI beta (#166)
1 parent 920bb92 commit 44404c3

30 files changed

+4454
-294
lines changed

jest.config.cjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ module.exports = {
5959
'/node_modules/',
6060
'/src/cli.ts',
6161
'/src/command-line-arguments.ts',
62+
'/src/ui.ts',
63+
'/src/ui/types.ts',
64+
'/src/dirname.ts',
6265
],
6366

6467
// Indicates which provider should be used to instrument code for coverage
@@ -217,7 +220,7 @@ module.exports = {
217220

218221
// A map from regular expressions to paths to transformers
219222
transform: {
220-
"\\.[jt]sx?$": "babel-jest"
223+
'\\.[jt]sx?$': 'babel-jest',
221224
},
222225

223226
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation

package.json

+17-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"dist/"
1515
],
1616
"scripts": {
17-
"build": "tsc --project tsconfig.build.json",
17+
"build": "tsc --project tsconfig.build.json && yarn build:ui",
18+
"build:ui": "vite build",
1819
"build:clean": "rimraf dist && yarn build",
1920
"lint": "yarn lint:eslint && yarn lint:misc --check",
2021
"lint:eslint": "eslint . --cache --ext js,ts",
@@ -30,6 +31,8 @@
3031
"@metamask/utils": "^9.0.0",
3132
"debug": "^4.3.4",
3233
"execa": "^8.0.1",
34+
"express": "^4.21.2",
35+
"open": "^10.1.0",
3336
"pony-cause": "^2.1.9",
3437
"semver": "^7.5.4",
3538
"validate-npm-package-name": "^5.0.0",
@@ -47,17 +50,22 @@
4750
"@metamask/eslint-config-jest": "^10.0.0",
4851
"@metamask/eslint-config-nodejs": "^10.0.0",
4952
"@metamask/eslint-config-typescript": "^10.0.0",
53+
"@tailwindcss/vite": "^4.0.9",
5054
"@types/debug": "^4.1.7",
55+
"@types/express": "^5.0.0",
5156
"@types/jest": "^29.5.10",
5257
"@types/jest-when": "^3.5.2",
5358
"@types/node": "^17.0.23",
5459
"@types/prettier": "^2.7.3",
60+
"@types/react": "^19.0.8",
61+
"@types/react-dom": "^19.0.3",
5562
"@types/rimraf": "^4.0.5",
5663
"@types/validate-npm-package-name": "^4.0.2",
5764
"@types/which": "^3.0.0",
5865
"@types/yargs": "^17.0.10",
5966
"@typescript-eslint/eslint-plugin": "^5.62.0",
6067
"@typescript-eslint/parser": "^5.62.0",
68+
"@vitejs/plugin-react": "^4.3.4",
6169
"babel-jest": "^29.7.0",
6270
"deepmerge": "^4.2.2",
6371
"eslint": "^8.27.0",
@@ -73,10 +81,15 @@
7381
"nanoid": "^3.3.4",
7482
"prettier": "^3.3.3",
7583
"prettier-plugin-packagejson": "^2.5.2",
84+
"react": "^19.0.0",
85+
"react-dom": "^19.0.0",
86+
"react-markdown": "^9.0.3",
7687
"rimraf": "^4.0.5",
7788
"stdio-mock": "^1.2.0",
89+
"tailwindcss": "^4.0.9",
7890
"tsx": "^4.6.1",
79-
"typescript": "~5.1.6"
91+
"typescript": "~5.1.6",
92+
"vite": "^6.2.0"
8093
},
8194
"peerDependencies": {
8295
"prettier": ">=3.0.0"
@@ -92,7 +105,8 @@
92105
"lavamoat": {
93106
"allowScripts": {
94107
"@lavamoat/preinstall-always-fail": false,
95-
"tsx>esbuild": false
108+
"tsx>esbuild": false,
109+
"vite>esbuild": false
96110
}
97111
}
98112
}

src/command-line-arguments.ts

+15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type CommandLineArguments = {
77
reset: boolean;
88
backport: boolean;
99
defaultBranch: string;
10+
interactive: boolean;
11+
port: number;
1012
};
1113

1214
/**
@@ -51,6 +53,19 @@ export async function readCommandLineArguments(
5153
default: 'main',
5254
type: 'string',
5355
})
56+
.option('interactive', {
57+
alias: 'i',
58+
describe:
59+
'Start an interactive web UI for selecting package versions to release',
60+
type: 'boolean',
61+
default: false,
62+
})
63+
.option('port', {
64+
describe:
65+
'Port to run the interactive web UI server (only used with --interactive)',
66+
type: 'number',
67+
default: 3000,
68+
})
5469
.help()
5570
.strict()
5671
.parse();

src/dirname.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { fileURLToPath } from 'url';
2+
import { dirname } from 'path';
3+
4+
const __filename = fileURLToPath(import.meta.url);
5+
const __dirname = dirname(__filename);
6+
7+
/**
8+
* Get the current directory path.
9+
*
10+
* @returns The current directory path.
11+
*/
12+
export function getCurrentDirectoryPath() {
13+
return __dirname;
14+
}

src/initial-parameters.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ describe('initial-parameters', () => {
3636
reset: true,
3737
backport: false,
3838
defaultBranch: 'main',
39+
interactive: false,
40+
port: 3000,
3941
});
4042
jest
4143
.spyOn(envModule, 'getEnvironmentVariables')
@@ -56,6 +58,8 @@ describe('initial-parameters', () => {
5658
reset: true,
5759
releaseType: 'ordinary',
5860
defaultBranch: 'main',
61+
interactive: false,
62+
port: 3000,
5963
});
6064
});
6165

@@ -72,6 +76,8 @@ describe('initial-parameters', () => {
7276
reset: true,
7377
backport: false,
7478
defaultBranch: 'main',
79+
interactive: false,
80+
port: 3000,
7581
});
7682
jest
7783
.spyOn(envModule, 'getEnvironmentVariables')
@@ -102,6 +108,8 @@ describe('initial-parameters', () => {
102108
reset: true,
103109
backport: false,
104110
defaultBranch: 'main',
111+
interactive: false,
112+
port: 3000,
105113
});
106114
jest
107115
.spyOn(envModule, 'getEnvironmentVariables')
@@ -132,6 +140,8 @@ describe('initial-parameters', () => {
132140
reset: true,
133141
backport: false,
134142
defaultBranch: 'main',
143+
interactive: false,
144+
port: 3000,
135145
});
136146
jest
137147
.spyOn(envModule, 'getEnvironmentVariables')
@@ -162,6 +172,8 @@ describe('initial-parameters', () => {
162172
reset: true,
163173
backport: false,
164174
defaultBranch: 'main',
175+
interactive: false,
176+
port: 3000,
165177
});
166178
jest
167179
.spyOn(envModule, 'getEnvironmentVariables')
@@ -190,6 +202,8 @@ describe('initial-parameters', () => {
190202
reset: false,
191203
backport: false,
192204
defaultBranch: 'main',
205+
interactive: false,
206+
port: 3000,
193207
});
194208
jest
195209
.spyOn(envModule, 'getEnvironmentVariables')
@@ -218,6 +232,8 @@ describe('initial-parameters', () => {
218232
reset: false,
219233
backport: true,
220234
defaultBranch: 'main',
235+
interactive: false,
236+
port: 3000,
221237
});
222238
jest
223239
.spyOn(envModule, 'getEnvironmentVariables')
@@ -246,6 +262,8 @@ describe('initial-parameters', () => {
246262
reset: false,
247263
backport: false,
248264
defaultBranch: 'main',
265+
interactive: false,
266+
port: 3000,
249267
});
250268
jest
251269
.spyOn(envModule, 'getEnvironmentVariables')

src/initial-parameters.ts

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type InitialParameters = {
2121
reset: boolean;
2222
releaseType: ReleaseType;
2323
defaultBranch: string;
24+
interactive: boolean;
25+
port: number;
2426
};
2527

2628
/**
@@ -61,5 +63,7 @@ export async function determineInitialParameters({
6163
reset: args.reset,
6264
defaultBranch: args.defaultBranch,
6365
releaseType: args.backport ? 'backport' : 'ordinary',
66+
interactive: args.interactive,
67+
port: args.port,
6468
};
6569
}

src/main.test.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@ import { buildMockProject } from '../tests/unit/helpers.js';
33
import { main } from './main.js';
44
import * as initialParametersModule from './initial-parameters.js';
55
import * as monorepoWorkflowOperations from './monorepo-workflow-operations.js';
6+
import * as ui from './ui.js';
67

78
jest.mock('./initial-parameters');
89
jest.mock('./monorepo-workflow-operations');
10+
jest.mock('./ui');
11+
jest.mock('./dirname', () => ({
12+
getCurrentDirectoryPath: jest.fn().mockReturnValue('/path/to/somewhere'),
13+
}));
14+
jest.mock('open', () => ({
15+
apps: {
16+
browser: jest.fn(),
17+
},
18+
}));
919

1020
describe('main', () => {
11-
it('executes the monorepo workflow if the project is a monorepo', async () => {
21+
it('executes the CLI monorepo workflow if the project is a monorepo and interactive is false', async () => {
1222
const project = buildMockProject({ isMonorepo: true });
1323
const stdout = fs.createWriteStream('/dev/null');
1424
const stderr = fs.createWriteStream('/dev/null');
@@ -20,6 +30,8 @@ describe('main', () => {
2030
reset: true,
2131
defaultBranch: 'main',
2232
releaseType: 'backport',
33+
interactive: false,
34+
port: 3000,
2335
});
2436
const followMonorepoWorkflowSpy = jest
2537
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')
@@ -43,6 +55,40 @@ describe('main', () => {
4355
});
4456
});
4557

58+
it('executes the interactive UI monorepo workflow if the project is a monorepo and interactive is true', async () => {
59+
const project = buildMockProject({ isMonorepo: true });
60+
const stdout = fs.createWriteStream('/dev/null');
61+
const stderr = fs.createWriteStream('/dev/null');
62+
jest
63+
.spyOn(initialParametersModule, 'determineInitialParameters')
64+
.mockResolvedValue({
65+
project,
66+
tempDirectoryPath: '/path/to/temp/directory',
67+
reset: true,
68+
defaultBranch: 'main',
69+
releaseType: 'backport',
70+
interactive: true,
71+
port: 3000,
72+
});
73+
const startUISpy = jest.spyOn(ui, 'startUI').mockResolvedValue();
74+
75+
await main({
76+
argv: [],
77+
cwd: '/path/to/somewhere',
78+
stdout,
79+
stderr,
80+
});
81+
82+
expect(startUISpy).toHaveBeenCalledWith({
83+
project,
84+
releaseType: 'backport',
85+
defaultBranch: 'main',
86+
port: 3000,
87+
stdout,
88+
stderr,
89+
});
90+
});
91+
4692
it('executes the polyrepo workflow if the project is within a polyrepo', async () => {
4793
const project = buildMockProject({ isMonorepo: false });
4894
const stdout = fs.createWriteStream('/dev/null');
@@ -55,6 +101,8 @@ describe('main', () => {
55101
reset: false,
56102
defaultBranch: 'main',
57103
releaseType: 'backport',
104+
interactive: false,
105+
port: 3000,
58106
});
59107
const followMonorepoWorkflowSpy = jest
60108
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')

src/main.ts

+32-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { WriteStream } from 'fs';
22
import { determineInitialParameters } from './initial-parameters.js';
33
import { followMonorepoWorkflow } from './monorepo-workflow-operations.js';
4+
import { startUI } from './ui.js';
45

56
/**
67
* The main function for this tool. Designed to not access `process.argv`,
@@ -25,22 +26,42 @@ export async function main({
2526
stdout: Pick<WriteStream, 'write'>;
2627
stderr: Pick<WriteStream, 'write'>;
2728
}) {
28-
const { project, tempDirectoryPath, reset, releaseType, defaultBranch } =
29-
await determineInitialParameters({ argv, cwd, stderr });
29+
const {
30+
project,
31+
tempDirectoryPath,
32+
reset,
33+
releaseType,
34+
defaultBranch,
35+
interactive,
36+
port,
37+
} = await determineInitialParameters({ argv, cwd, stderr });
3038

3139
if (project.isMonorepo) {
3240
stdout.write(
3341
'Project appears to have workspaces. Following monorepo workflow.\n',
3442
);
35-
await followMonorepoWorkflow({
36-
project,
37-
tempDirectoryPath,
38-
firstRemovingExistingReleaseSpecification: reset,
39-
releaseType,
40-
defaultBranch,
41-
stdout,
42-
stderr,
43-
});
43+
44+
if (interactive) {
45+
stdout.write(`Starting UI on port ${port}...\n`);
46+
await startUI({
47+
project,
48+
releaseType,
49+
defaultBranch,
50+
port,
51+
stdout,
52+
stderr,
53+
});
54+
} else {
55+
await followMonorepoWorkflow({
56+
project,
57+
tempDirectoryPath,
58+
firstRemovingExistingReleaseSpecification: reset,
59+
releaseType,
60+
defaultBranch,
61+
stdout,
62+
stderr,
63+
});
64+
}
4465
} else {
4566
stdout.write(
4667
'Project does not appear to have any workspaces. Following polyrepo workflow.\n',

0 commit comments

Comments
 (0)