Skip to content

Commit f02b9de

Browse files
authored
feat(create-cli): add monorepo setup mode (#1265)
1 parent bbf49e8 commit f02b9de

36 files changed

+1191
-457
lines changed

packages/ci/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default tseslint.config(
1717
rules: {
1818
'@nx/dependency-checks': [
1919
'error',
20-
{ ignoredDependencies: ['type-fest'] }, // only for internal typings
20+
{ ignoredDependencies: ['type-fest'] }, // type-only imports not detected by rule
2121
],
2222
},
2323
},

packages/ci/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"ansis": "^3.3.2",
3333
"glob": "^11.0.1",
3434
"simple-git": "^3.20.0",
35-
"yaml": "^2.5.1",
35+
"type-fest": "^4.26.1",
3636
"zod": "^4.2.1"
3737
},
3838
"files": [

packages/ci/src/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
export type { SourceFileIssue } from './lib/issues.js';
22
export type * from './lib/models.js';
3-
export {
4-
isMonorepoTool,
5-
MONOREPO_TOOLS,
6-
type MonorepoTool,
7-
} from './lib/monorepo/index.js';
83
export { runInCI } from './lib/run.js';
94
export { configPatternsSchema } from './lib/schemas.js';
105
export {

packages/ci/src/lib/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Format, PersistConfig, UploadConfig } from '@code-pushup/models';
2+
import type { MonorepoTool } from '@code-pushup/utils';
23
import type { SourceFileIssue } from './issues.js';
3-
import type { MonorepoTool } from './monorepo/index.js';
44

55
/**
66
* Customization options for {@link runInCI}

packages/ci/src/lib/monorepo/detect-tool.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/ci/src/lib/monorepo/handlers/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { MonorepoTool, MonorepoToolHandler } from '../tools.js';
1+
import type { MonorepoTool } from '@code-pushup/utils';
2+
import type { MonorepoToolHandler } from '../tools.js';
23
import { npmHandler } from './npm.js';
34
import { nxHandler } from './nx.js';
45
import { pnpmHandler } from './pnpm.js';

packages/ci/src/lib/monorepo/handlers/npm.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
1-
import path from 'node:path';
2-
import { fileExists } from '@code-pushup/utils';
31
import {
42
hasCodePushUpDependency,
53
hasScript,
6-
hasWorkspacesEnabled,
74
listWorkspaces,
8-
} from '../packages.js';
5+
} from '@code-pushup/utils';
96
import type { MonorepoToolHandler } from '../tools.js';
107

118
export const npmHandler: MonorepoToolHandler = {
129
tool: 'npm',
1310

14-
async isConfigured(options) {
15-
return (
16-
(await fileExists(path.join(options.cwd, 'package-lock.json'))) &&
17-
(await hasWorkspacesEnabled(options.cwd))
18-
);
19-
},
20-
2111
async listProjects(options) {
2212
const { workspaces, rootPackageJson } = await listWorkspaces(options.cwd);
2313
return workspaces

packages/ci/src/lib/monorepo/handlers/npm.unit.test.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,6 @@ describe('npmHandler', () => {
1818
const pkgJsonContent = (content: PackageJson): string =>
1919
JSON.stringify(content);
2020

21-
describe('isConfigured', () => {
22-
it('should detect NPM workspaces when package-lock.json exists and "workspaces" set in package.json', async () => {
23-
vol.fromJSON(
24-
{
25-
'package.json': pkgJsonContent({
26-
private: true,
27-
workspaces: ['packages/*'],
28-
}),
29-
'package-lock.json': '',
30-
},
31-
MEMFS_VOLUME,
32-
);
33-
await expect(npmHandler.isConfigured(options)).resolves.toBeTrue();
34-
});
35-
36-
it('should NOT detect NPM workspaces when "workspaces" not set in package.json', async () => {
37-
vol.fromJSON(
38-
{
39-
'package.json': pkgJsonContent({}),
40-
'package-lock.json': '',
41-
},
42-
MEMFS_VOLUME,
43-
);
44-
await expect(npmHandler.isConfigured(options)).resolves.toBeFalse();
45-
});
46-
47-
it("should NOT detect NPM workspaces when package-lock.json doesn't exist", async () => {
48-
vol.fromJSON(
49-
{
50-
'package.json': pkgJsonContent({
51-
private: true,
52-
workspaces: ['packages/*'],
53-
}),
54-
'yarn.lock': '',
55-
},
56-
MEMFS_VOLUME,
57-
);
58-
await expect(npmHandler.isConfigured(options)).resolves.toBeFalse();
59-
});
60-
});
61-
6221
describe('listProjects', () => {
6322
it('should list all NPM workspaces with code-pushup script', async () => {
6423
vol.fromJSON(

packages/ci/src/lib/monorepo/handlers/nx.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import path from 'node:path';
21
import {
32
executeProcess,
4-
fileExists,
53
interpolate,
64
stringifyError,
75
toArray,
@@ -11,21 +9,18 @@ import type { MonorepoToolHandler } from '../tools.js';
119
export const nxHandler: MonorepoToolHandler = {
1210
tool: 'nx',
1311

14-
async isConfigured(options) {
15-
return (
16-
(await fileExists(path.join(options.cwd, 'nx.json'))) &&
17-
(
18-
await executeProcess({
19-
command: 'npx',
20-
args: ['nx', 'report'],
21-
cwd: options.cwd,
22-
ignoreExitCode: true,
23-
})
24-
).code === 0
25-
);
26-
},
27-
2812
async listProjects({ cwd, task, nxProjectsFilter }) {
13+
const { code, stderr } = await executeProcess({
14+
command: 'npx',
15+
args: ['nx', 'report'],
16+
cwd,
17+
ignoreExitCode: true,
18+
});
19+
if (code !== 0) {
20+
const suffix = stderr ? ` - ${stderr}` : '';
21+
throw new Error(`'nx report' failed with exit code ${code}${suffix}`);
22+
}
23+
2924
const { stdout } = await executeProcess({
3025
command: 'npx',
3126
args: [

packages/ci/src/lib/monorepo/handlers/nx.unit.test.ts

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { vol } from 'memfs';
21
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
32
import * as utils from '@code-pushup/utils';
43
import type {
@@ -16,42 +15,15 @@ describe('nxHandler', () => {
1615
nxProjectsFilter: '--with-target={task}',
1716
};
1817

19-
describe('isConfigured', () => {
20-
it('should detect Nx when nx.json exists and `nx report` succeeds', async () => {
21-
vol.fromJSON({ 'nx.json': '{}' }, MEMFS_VOLUME);
22-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
23-
code: 0,
24-
stdout: 'NX Report complete - copy this into the issue template',
25-
} as utils.ProcessResult);
26-
27-
await expect(nxHandler.isConfigured(options)).resolves.toBeTrue();
28-
});
29-
30-
it("should NOT detect Nx when nx.json doesn't exist", async () => {
31-
vol.fromJSON({ 'turbo.json': '{}' }, MEMFS_VOLUME);
32-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
33-
code: 0,
34-
} as utils.ProcessResult);
35-
36-
await expect(nxHandler.isConfigured(options)).resolves.toBeFalse();
37-
});
38-
39-
it('should NOT detect Nx when `nx report` fails with non-zero exit code', async () => {
40-
vol.fromJSON({ 'nx.json': '' }, MEMFS_VOLUME);
41-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
42-
code: 1,
43-
stderr: 'Error: ValueExpected in nx.json',
44-
} as utils.ProcessResult);
45-
46-
await expect(nxHandler.isConfigured(options)).resolves.toBeFalse();
47-
});
48-
});
49-
5018
describe('listProjects', () => {
19+
const nxReportSuccess = { code: 0 } as utils.ProcessResult;
20+
5121
beforeEach(() => {
52-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
53-
stdout: '["backend","frontend"]',
54-
} as utils.ProcessResult);
22+
vi.spyOn(utils, 'executeProcess')
23+
.mockResolvedValueOnce(nxReportSuccess)
24+
.mockResolvedValueOnce({
25+
stdout: '["backend","frontend"]',
26+
} as utils.ProcessResult);
5527
});
5628

5729
it('should list projects from `nx show projects`', async () => {
@@ -95,20 +67,39 @@ describe('nxHandler', () => {
9567
} satisfies utils.ProcessConfig);
9668
});
9769

70+
it('should throw if `nx report` fails', async () => {
71+
vi.spyOn(utils, 'executeProcess')
72+
.mockReset()
73+
.mockResolvedValueOnce({
74+
code: 1,
75+
stderr: 'Error: ValueExpected in nx.json',
76+
} as utils.ProcessResult);
77+
78+
await expect(nxHandler.listProjects(options)).rejects.toThrow(
79+
"'nx report' failed with exit code 1 - Error: ValueExpected in nx.json",
80+
);
81+
});
82+
9883
it('should throw if `nx show projects` outputs invalid JSON', async () => {
99-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
100-
stdout: 'backend\nfrontend\n',
101-
} as utils.ProcessResult);
84+
vi.spyOn(utils, 'executeProcess')
85+
.mockReset()
86+
.mockResolvedValueOnce(nxReportSuccess)
87+
.mockResolvedValueOnce({
88+
stdout: 'backend\nfrontend\n',
89+
} as utils.ProcessResult);
10290

10391
await expect(nxHandler.listProjects(options)).rejects.toThrow(
10492
"Invalid non-JSON output from 'nx show projects' - SyntaxError: Unexpected token",
10593
);
10694
});
10795

10896
it("should throw if `nx show projects` JSON output isn't array of strings", async () => {
109-
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
110-
stdout: '"backend"',
111-
} as utils.ProcessResult);
97+
vi.spyOn(utils, 'executeProcess')
98+
.mockReset()
99+
.mockResolvedValueOnce(nxReportSuccess)
100+
.mockResolvedValueOnce({
101+
stdout: '"backend"',
102+
} as utils.ProcessResult);
112103

113104
await expect(nxHandler.listProjects(options)).rejects.toThrow(
114105
'Invalid JSON output from \'nx show projects\', expected array of strings, received "backend"',

0 commit comments

Comments
 (0)