Skip to content
Merged
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
3 changes: 2 additions & 1 deletion packages/plugin-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"type": "module",
"dependencies": {
"@code-pushup/models": "0.59.0",
"@code-pushup/utils": "0.59.0"
"@code-pushup/utils": "0.59.0",
"zod": "^3.23.8"
},
"peerDependencies": {
"typescript": ">=4.0.0"
Expand Down
27 changes: 27 additions & 0 deletions packages/plugin-typescript/src/lib/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';
import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js';
import type { AuditSlug } from './types.js';

const auditSlugs = AUDITS.map(({ slug }) => slug) as [
AuditSlug,
...AuditSlug[],
];
export const typescriptPluginConfigSchema = z.object({
tsconfig: z
.string({
description: 'Path to a tsconfig file (default is tsconfig.json)',
})
.default(DEFAULT_TS_CONFIG),
onlyAudits: z
.array(z.enum(auditSlugs), {
description: 'Filters TypeScript compiler errors by diagnostic codes',
})
.optional(),
});

export type TypescriptPluginOptions = z.input<
typeof typescriptPluginConfigSchema
>;
export type TypescriptPluginConfig = z.infer<
typeof typescriptPluginConfigSchema
>;
69 changes: 69 additions & 0 deletions packages/plugin-typescript/src/lib/schema.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import {
type TypescriptPluginOptions,
typescriptPluginConfigSchema,
} from './schema.js';

describe('typescriptPluginConfigSchema', () => {
const tsconfig = 'tsconfig.json';

it('accepts a empty configuration', () => {
expect(() => typescriptPluginConfigSchema.parse({})).not.toThrow();
});

it('accepts a configuration with tsconfig set', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
tsconfig,
} satisfies TypescriptPluginOptions),
).not.toThrow();
});

it('accepts a configuration with tsconfig and empty onlyAudits', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
tsconfig,
onlyAudits: [],
} satisfies TypescriptPluginOptions),
).not.toThrow();
});

it('accepts a configuration with tsconfig and full onlyAudits', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
tsconfig,
onlyAudits: [
'syntax-errors',
'semantic-errors',
'configuration-errors',
],
} satisfies TypescriptPluginOptions),
).not.toThrow();
});

it('throws for invalid onlyAudits', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
onlyAudits: 123,
}),
).toThrow('invalid_type');
});

it('throws for invalid onlyAudits items', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
tsconfig,
onlyAudits: [123, true],
}),
).toThrow('invalid_type');
});

it('throws for unknown audit slug', () => {
expect(() =>
typescriptPluginConfigSchema.parse({
tsconfig,
onlyAudits: ['unknown-audit'],
}),
).toThrow(/unknown-audit/);
});
});
5 changes: 0 additions & 5 deletions packages/plugin-typescript/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import type { DiagnosticsOptions } from './runner/ts-runner.js';
import type { CodeRangeName } from './runner/types.js';

export type AuditSlug = CodeRangeName;

export type FilterOptions = { onlyAudits?: AuditSlug[] | undefined };
export type TypescriptPluginOptions = Partial<DiagnosticsOptions> &
FilterOptions;
56 changes: 56 additions & 0 deletions packages/plugin-typescript/src/lib/typescript-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createRequire } from 'node:module';
import type { PluginConfig } from '@code-pushup/models';
import { stringifyError } from '@code-pushup/utils';
import { DEFAULT_TS_CONFIG, TYPESCRIPT_PLUGIN_SLUG } from './constants.js';
import { createRunnerFunction } from './runner/runner.js';
import {
type TypescriptPluginConfig,
type TypescriptPluginOptions,
typescriptPluginConfigSchema,
} from './schema.js';
import { getAudits, getGroups, logSkippedAudits } from './utils.js';

const packageJson = createRequire(import.meta.url)(
'../../package.json',
) as typeof import('../../package.json');

export async function typescriptPlugin(
options?: TypescriptPluginOptions,
): Promise<PluginConfig> {
const { tsconfig = DEFAULT_TS_CONFIG, onlyAudits } = parseOptions(
options ?? {},
);

const filteredAudits = getAudits({ onlyAudits });
const filteredGroups = getGroups({ onlyAudits });

logSkippedAudits(filteredAudits);

return {
slug: TYPESCRIPT_PLUGIN_SLUG,
packageName: packageJson.name,
version: packageJson.version,
title: 'Typescript',
description: 'Official Code PushUp Typescript plugin.',
docsUrl: 'https://www.npmjs.com/package/@code-pushup/typescript-plugin/',
icon: 'typescript',
audits: filteredAudits,
groups: filteredGroups,
runner: createRunnerFunction({
tsconfig,
expectedAudits: filteredAudits,
}),
};
}

function parseOptions(
tsPluginOptions: TypescriptPluginOptions,
): TypescriptPluginConfig {
try {
return typescriptPluginConfigSchema.parse(tsPluginOptions);
} catch (error) {
throw new Error(
`Error parsing TypeScript Plugin options: ${stringifyError(error)}`,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from 'vitest';
import { pluginConfigSchema } from '@code-pushup/models';
import { AUDITS, GROUPS } from './constants.js';
import type { TypescriptPluginOptions } from './schema.js';
import { typescriptPlugin } from './typescript-plugin.js';

describe('typescriptPlugin-config-object', () => {
it('should create valid plugin config without options', async () => {
const pluginConfig = await typescriptPlugin();

expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();

const { audits, groups } = pluginConfig;
expect(audits).toHaveLength(AUDITS.length);
expect(groups).toBeDefined();
expect(groups!).toHaveLength(GROUPS.length);
});

it('should create valid plugin config', async () => {
const pluginConfig = await typescriptPlugin({
tsconfig: 'mocked-away/tsconfig.json',
onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'],
});

expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();

const { audits, groups } = pluginConfig;
expect(audits).toHaveLength(3);
expect(groups).toBeDefined();
expect(groups!).toHaveLength(2);
});

it('should throw for invalid valid params', async () => {
await expect(() =>
typescriptPlugin({
tsconfig: 42,
} as unknown as TypescriptPluginOptions),
).rejects.toThrow(/invalid_type/);
});
});
18 changes: 14 additions & 4 deletions packages/plugin-typescript/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type { CompilerOptions } from 'typescript';
import type { Audit, CategoryConfig, CategoryRef } from '@code-pushup/models';
import { kebabCaseToCamelCase } from '@code-pushup/utils';
import { kebabCaseToCamelCase, ui } from '@code-pushup/utils';
import { AUDITS, GROUPS, TYPESCRIPT_PLUGIN_SLUG } from './constants.js';
import type { FilterOptions, TypescriptPluginOptions } from './types.js';
import type {
TypescriptPluginConfig,
TypescriptPluginOptions,
} from './schema.js';

/**
* It filters the audits by the slugs
*
* @param slugs
*/
export function filterAuditsBySlug(slugs?: string[]) {
return ({ slug }: { slug: string }) => {
if (slugs && slugs.length > 0) {
Expand Down Expand Up @@ -58,7 +66,9 @@ export function getGroups(options?: TypescriptPluginOptions) {
})).filter(group => group.refs.length > 0);
}

export function getAudits(options?: FilterOptions) {
export function getAudits(
options?: Pick<TypescriptPluginConfig, 'onlyAudits'>,
) {
return AUDITS.filter(filterAuditsBySlug(options?.onlyAudits));
}

Expand Down Expand Up @@ -136,6 +146,6 @@ export function logSkippedAudits(audits: Audit[]) {
audit => !audits.some(filtered => filtered.slug === audit.slug),
).map(audit => kebabCaseToCamelCase(audit.slug));
if (skippedAudits.length > 0) {
console.warn(`Skipped audits: [${skippedAudits.join(', ')}]`);
ui().logger.info(`Skipped audits: [${skippedAudits.join(', ')}]`);
}
}
26 changes: 7 additions & 19 deletions packages/plugin-typescript/src/lib/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import { type Audit, categoryRefSchema } from '@code-pushup/models';
import { ui } from '@code-pushup/utils';
import { AUDITS } from './constants.js';
import {
filterAuditsByCompilerOptions,
Expand Down Expand Up @@ -99,31 +100,21 @@ describe('getCategoryRefsFromGroups', () => {

it('should return all groups as categoryRefs if compiler options are given', async () => {
const categoryRefs = await getCategoryRefsFromGroups({
tsConfigPath: 'tsconfig.json',
tsconfig: 'tsconfig.json',
});
expect(categoryRefs).toHaveLength(3);
});

it('should return a subset of all groups as categoryRefs if compiler options contain onlyAudits filter', async () => {
const categoryRefs = await getCategoryRefsFromGroups({
tsConfigPath: 'tsconfig.json',
tsconfig: 'tsconfig.json',
onlyAudits: ['semantic-errors'],
});
expect(categoryRefs).toHaveLength(1);
});
});

describe('logSkippedAudits', () => {
beforeEach(() => {
vi.mock('console', () => ({
warn: vi.fn(),
}));
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should not warn when all audits are included', () => {
logSkippedAudits(AUDITS);

Expand All @@ -133,18 +124,15 @@ describe('logSkippedAudits', () => {
it('should warn about skipped audits', () => {
logSkippedAudits(AUDITS.slice(0, -1));

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect(ui()).toHaveLogged(
'info',
expect.stringContaining(`Skipped audits: [`),
);
});

it('should camel case the slugs in the audit message', () => {
logSkippedAudits(AUDITS.slice(0, -1));

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining(`unknownCodes`),
);
expect(ui()).toHaveLogged('info', expect.stringContaining(`unknownCodes`));
});
});
1 change: 1 addition & 0 deletions packages/plugin-typescript/vite.config.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineConfig({
globalSetup: ['../../global-setup.ts'],
setupFiles: [
'../../testing/test-setup/src/lib/cliui.mock.ts',
'../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
'../../testing/test-setup/src/lib/fs.mock.ts',
'../../testing/test-setup/src/lib/console.mock.ts',
'../../testing/test-setup/src/lib/reset.mocks.ts',
Expand Down