Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions packages/ci/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,6 @@ export default tseslint.config(
},
},
},
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{ ignoredDependencies: ['type-fest'] }, // only for internal typings
],
},
},
{
files: ['**/*.test.ts'],
rules: {
Expand Down
2 changes: 1 addition & 1 deletion packages/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"ansis": "^3.3.2",
"glob": "^11.0.1",
"simple-git": "^3.20.0",
"yaml": "^2.5.1",
"type-fest": "^4.26.1",
"zod": "^4.2.1"
},
"files": [
Expand Down
5 changes: 0 additions & 5 deletions packages/ci/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
export type { SourceFileIssue } from './lib/issues.js';
export type * from './lib/models.js';
export {
isMonorepoTool,
MONOREPO_TOOLS,
type MonorepoTool,
} from './lib/monorepo/index.js';
export { runInCI } from './lib/run.js';
export { configPatternsSchema } from './lib/schemas.js';
export {
Expand Down
2 changes: 1 addition & 1 deletion packages/ci/src/lib/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Format, PersistConfig, UploadConfig } from '@code-pushup/models';
import type { MonorepoTool } from '@code-pushup/utils';
import type { SourceFileIssue } from './issues.js';
import type { MonorepoTool } from './monorepo/index.js';

/**
* Customization options for {@link runInCI}
Expand Down
14 changes: 0 additions & 14 deletions packages/ci/src/lib/monorepo/detect-tool.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/ci/src/lib/monorepo/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MonorepoTool, MonorepoToolHandler } from '../tools.js';
import type { MonorepoTool } from '@code-pushup/utils';
import type { MonorepoToolHandler } from '../tools.js';
import { npmHandler } from './npm.js';
import { nxHandler } from './nx.js';
import { pnpmHandler } from './pnpm.js';
Expand Down
12 changes: 1 addition & 11 deletions packages/ci/src/lib/monorepo/handlers/npm.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import path from 'node:path';
import { fileExists } from '@code-pushup/utils';
import {
hasCodePushUpDependency,
hasScript,
hasWorkspacesEnabled,
listWorkspaces,
} from '../packages.js';
} from '@code-pushup/utils';
import type { MonorepoToolHandler } from '../tools.js';

export const npmHandler: MonorepoToolHandler = {
tool: 'npm',

async isConfigured(options) {
return (
(await fileExists(path.join(options.cwd, 'package-lock.json'))) &&
(await hasWorkspacesEnabled(options.cwd))
);
},

async listProjects(options) {
const { workspaces, rootPackageJson } = await listWorkspaces(options.cwd);
return workspaces
Expand Down
41 changes: 0 additions & 41 deletions packages/ci/src/lib/monorepo/handlers/npm.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,6 @@ describe('npmHandler', () => {
const pkgJsonContent = (content: PackageJson): string =>
JSON.stringify(content);

describe('isConfigured', () => {
it('should detect NPM workspaces when package-lock.json exists and "workspaces" set in package.json', async () => {
vol.fromJSON(
{
'package.json': pkgJsonContent({
private: true,
workspaces: ['packages/*'],
}),
'package-lock.json': '',
},
MEMFS_VOLUME,
);
await expect(npmHandler.isConfigured(options)).resolves.toBeTrue();
});

it('should NOT detect NPM workspaces when "workspaces" not set in package.json', async () => {
vol.fromJSON(
{
'package.json': pkgJsonContent({}),
'package-lock.json': '',
},
MEMFS_VOLUME,
);
await expect(npmHandler.isConfigured(options)).resolves.toBeFalse();
});

it("should NOT detect NPM workspaces when package-lock.json doesn't exist", async () => {
vol.fromJSON(
{
'package.json': pkgJsonContent({
private: true,
workspaces: ['packages/*'],
}),
'yarn.lock': '',
},
MEMFS_VOLUME,
);
await expect(npmHandler.isConfigured(options)).resolves.toBeFalse();
});
});

describe('listProjects', () => {
it('should list all NPM workspaces with code-pushup script', async () => {
vol.fromJSON(
Expand Down
27 changes: 11 additions & 16 deletions packages/ci/src/lib/monorepo/handlers/nx.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import path from 'node:path';
import {
executeProcess,
fileExists,
interpolate,
stringifyError,
toArray,
Expand All @@ -11,21 +9,18 @@ import type { MonorepoToolHandler } from '../tools.js';
export const nxHandler: MonorepoToolHandler = {
tool: 'nx',

async isConfigured(options) {
return (
(await fileExists(path.join(options.cwd, 'nx.json'))) &&
(
await executeProcess({
command: 'npx',
args: ['nx', 'report'],
cwd: options.cwd,
ignoreExitCode: true,
})
).code === 0
);
},

async listProjects({ cwd, task, nxProjectsFilter }) {
const { code, stderr } = await executeProcess({
command: 'npx',
args: ['nx', 'report'],
cwd,
ignoreExitCode: true,
});
if (code !== 0) {
const suffix = stderr ? ` - ${stderr}` : '';
throw new Error(`'nx report' failed with exit code ${code}${suffix}`);
}

const { stdout } = await executeProcess({
command: 'npx',
args: [
Expand Down
73 changes: 32 additions & 41 deletions packages/ci/src/lib/monorepo/handlers/nx.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { vol } from 'memfs';
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
import * as utils from '@code-pushup/utils';
import type {
Expand All @@ -16,42 +15,15 @@ describe('nxHandler', () => {
nxProjectsFilter: '--with-target={task}',
};

describe('isConfigured', () => {
it('should detect Nx when nx.json exists and `nx report` succeeds', async () => {
vol.fromJSON({ 'nx.json': '{}' }, MEMFS_VOLUME);
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
code: 0,
stdout: 'NX Report complete - copy this into the issue template',
} as utils.ProcessResult);

await expect(nxHandler.isConfigured(options)).resolves.toBeTrue();
});

it("should NOT detect Nx when nx.json doesn't exist", async () => {
vol.fromJSON({ 'turbo.json': '{}' }, MEMFS_VOLUME);
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
code: 0,
} as utils.ProcessResult);

await expect(nxHandler.isConfigured(options)).resolves.toBeFalse();
});

it('should NOT detect Nx when `nx report` fails with non-zero exit code', async () => {
vol.fromJSON({ 'nx.json': '' }, MEMFS_VOLUME);
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
code: 1,
stderr: 'Error: ValueExpected in nx.json',
} as utils.ProcessResult);

await expect(nxHandler.isConfigured(options)).resolves.toBeFalse();
});
});

describe('listProjects', () => {
const nxReportSuccess = { code: 0 } as utils.ProcessResult;

beforeEach(() => {
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
stdout: '["backend","frontend"]',
} as utils.ProcessResult);
vi.spyOn(utils, 'executeProcess')
.mockResolvedValueOnce(nxReportSuccess)
.mockResolvedValueOnce({
stdout: '["backend","frontend"]',
} as utils.ProcessResult);
});

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

it('should throw if `nx report` fails', async () => {
vi.spyOn(utils, 'executeProcess')
.mockReset()
.mockResolvedValueOnce({
code: 1,
stderr: 'Error: ValueExpected in nx.json',
} as utils.ProcessResult);

await expect(nxHandler.listProjects(options)).rejects.toThrow(
"'nx report' failed with exit code 1 - Error: ValueExpected in nx.json",
);
});

it('should throw if `nx show projects` outputs invalid JSON', async () => {
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
stdout: 'backend\nfrontend\n',
} as utils.ProcessResult);
vi.spyOn(utils, 'executeProcess')
.mockReset()
.mockResolvedValueOnce(nxReportSuccess)
.mockResolvedValueOnce({
stdout: 'backend\nfrontend\n',
} as utils.ProcessResult);

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

it("should throw if `nx show projects` JSON output isn't array of strings", async () => {
vi.spyOn(utils, 'executeProcess').mockResolvedValue({
stdout: '"backend"',
} as utils.ProcessResult);
vi.spyOn(utils, 'executeProcess')
.mockReset()
.mockResolvedValueOnce(nxReportSuccess)
.mockResolvedValueOnce({
stdout: '"backend"',
} as utils.ProcessResult);

await expect(nxHandler.listProjects(options)).rejects.toThrow(
'Invalid JSON output from \'nx show projects\', expected array of strings, received "backend"',
Expand Down
20 changes: 4 additions & 16 deletions packages/ci/src/lib/monorepo/handlers/pnpm.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import path from 'node:path';
import * as YAML from 'yaml';
import { fileExists, readTextFile } from '@code-pushup/utils';
import {
hasCodePushUpDependency,
hasScript,
listPackages,
readPnpmWorkspacePatterns,
readRootPackageJson,
} from '../packages.js';
} from '@code-pushup/utils';
import type { MonorepoToolHandler } from '../tools.js';

const WORKSPACE_FILE = 'pnpm-workspace.yaml';

export const pnpmHandler: MonorepoToolHandler = {
tool: 'pnpm',

async isConfigured(options) {
return (
(await fileExists(path.join(options.cwd, WORKSPACE_FILE))) &&
(await fileExists(path.join(options.cwd, 'package.json')))
);
},

async listProjects(options) {
const yaml = await readTextFile(path.join(options.cwd, WORKSPACE_FILE));
const workspace = YAML.parse(yaml) as { packages?: string[] };
const packages = await listPackages(options.cwd, workspace.packages);
const patterns = await readPnpmWorkspacePatterns(options.cwd);
const packages = await listPackages(options.cwd, patterns);
const rootPackageJson = await readRootPackageJson(options.cwd);
return packages
.filter(
Expand Down
37 changes: 0 additions & 37 deletions packages/ci/src/lib/monorepo/handlers/pnpm.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,6 @@ describe('pnpmHandler', () => {
const pkgJsonContent = (content: PackageJson): string =>
JSON.stringify(content);

describe('isConfigured', () => {
it('should detect PNPM workspace when pnpm-workspace.yaml and package.json files exist', async () => {
vol.fromJSON(
{
'package.json': pkgJsonContent({}),
'pnpm-workspace.yaml': 'packages:\n- apps/*\n- libs/*\n\n',
},
MEMFS_VOLUME,
);
await expect(pnpmHandler.isConfigured(options)).resolves.toBeTrue();
});

it("should NOT detect PNPM workspace when pnpm-workspace.yaml doesn't exist", async () => {
vol.fromJSON(
{
'package.json': pkgJsonContent({}),
'pnpm-lock.yaml': '',
},
MEMFS_VOLUME,
);
await expect(pnpmHandler.isConfigured(options)).resolves.toBeFalse();
});

it("should NOT detect PNPM workspace when root package.json doesn't exist", async () => {
vol.fromJSON(
{
'packages/cli/package.json': pkgJsonContent({}),
'packages/cli/pnpm-lock.yaml': '',
'packages/core/package.json': pkgJsonContent({}),
'packages/core/pnpm-lock.yaml': '',
},
MEMFS_VOLUME,
);
await expect(pnpmHandler.isConfigured(options)).resolves.toBeFalse();
});
});

describe('listProjects', () => {
it('should list all PNPM workspace packages with code-pushup script', async () => {
vol.fromJSON(
Expand Down
17 changes: 8 additions & 9 deletions packages/ci/src/lib/monorepo/handlers/turbo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path';
import { fileExists, readJsonFile } from '@code-pushup/utils';
import { MONOREPO_TOOL_DETECTORS, readJsonFile } from '@code-pushup/utils';
import type { MonorepoToolHandler } from '../tools.js';
import { npmHandler } from './npm.js';
import { pnpmHandler } from './pnpm.js';
Expand All @@ -14,18 +14,17 @@ type TurboConfig = {
export const turboHandler: MonorepoToolHandler = {
tool: 'turbo',

async isConfigured(options) {
async listProjects(options) {
const configPath = path.join(options.cwd, 'turbo.json');
return (
(await fileExists(configPath)) &&
options.task in (await readJsonFile<TurboConfig>(configPath)).tasks
);
},
if (
!(options.task in (await readJsonFile<TurboConfig>(configPath)).tasks)
) {
throw new Error(`Task "${options.task}" not found in turbo.json`);
}

async listProjects(options) {
// eslint-disable-next-line functional/no-loop-statements
for (const handler of WORKSPACE_HANDLERS) {
if (await handler.isConfigured(options)) {
if (await MONOREPO_TOOL_DETECTORS[handler.tool](options.cwd)) {
const projects = await handler.listProjects(options);
return projects
.filter(({ bin }) => bin.includes(`run ${options.task}`)) // must have package.json script
Expand Down
Loading