Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-typescript): add plugin logic #936

Merged
merged 13 commits into from
Feb 17, 2025
Merged
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';

Check failure on line 1 in packages/plugin-typescript/src/lib/schema.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> Code coverage | Branch coverage

1st branch is not taken in any test case.
import { AUDITS, DEFAULT_TS_CONFIG } from './constants.js';
import type { AuditSlug } from './types.js';

const auditSlugs = AUDITS.map(({ slug }) => slug) as [

Check warning on line 5 in packages/plugin-typescript/src/lib/schema.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> JSDoc coverage | Variables coverage

Missing variables documentation for auditSlugs
AuditSlug,
...AuditSlug[],
];
export const typescriptPluginConfigSchema = z.object({

Check warning on line 9 in packages/plugin-typescript/src/lib/schema.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> JSDoc coverage | Variables coverage

Missing variables documentation for typescriptPluginConfigSchema
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';

Check failure on line 1 in packages/plugin-typescript/src/lib/typescript-plugin.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> Code coverage | Branch coverage

1st branch is not taken in any test case.
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)(

Check warning on line 13 in packages/plugin-typescript/src/lib/typescript-plugin.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> JSDoc coverage | Variables coverage

Missing variables documentation for packageJson
'../../package.json',
) as typeof import('../../package.json');

export async function typescriptPlugin(

Check warning on line 17 in packages/plugin-typescript/src/lib/typescript-plugin.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> JSDoc coverage | Functions coverage

Missing functions documentation for 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(

Check warning on line 46 in packages/plugin-typescript/src/lib/typescript-plugin.ts

View workflow job for this annotation

GitHub Actions / Code PushUp

<✓> JSDoc coverage | Functions coverage

Missing functions documentation for parseOptions
tsPluginOptions: TypescriptPluginOptions,
): TypescriptPluginConfig {
try {
return typescriptPluginConfigSchema.parse(tsPluginOptions);
} catch (error) {
throw new Error(
`Error parsing TypeScript Plugin options: ${stringifyError(error)}`,
);
}
}
40 changes: 40 additions & 0 deletions packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts
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
Loading