Skip to content

Commit

Permalink
refactor(examples-plugins): refactor knip reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed May 4, 2024
1 parent 5b58b9b commit 8c8da11
Show file tree
Hide file tree
Showing 15 changed files with 739 additions and 1,407 deletions.
740 changes: 32 additions & 708 deletions examples/plugins/mocks/knip-raw.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`knipReporter > should produce valid audit outputsass 1`] = `
[
{
"details": {
"issues": [
{
"message": "Unused file code-pushup.json",
"severity": "info",
"source": {
"file": "../../../code-pushup.json",
},
},
],
},
"score": 0,
"slug": "unused-files",
"value": 1,
},
{
"details": {
"issues": [
{
"message": "Unused dependency cli-table3",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/package.json",
},
},
],
},
"score": 0,
"slug": "unused-dependencies",
"value": 1,
},
{
"details": {
"issues": [
{
"message": "Unused devDependency @trivago/prettier-plugin-sort-imports",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/package.json",
},
},
],
},
"score": 0,
"slug": "unused-devdependencies",
"value": 1,
},
{
"details": {
"issues": [
{
"message": "Referenced optional peerDependency ts-node",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/package.json",
},
},
],
},
"score": 0,
"slug": "referenced-optional-peerdependencies",
"value": 1,
},
{
"details": {
"issues": [
{
"message": "Unlisted dependency jsonc-eslint-parser",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/packages/plugin-lighthouse/.eslintrc.json",
},
},
{
"message": "Unlisted dependency jsonc-eslint-parser",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/.eslintrc.json",
},
},
],
},
"score": 0,
"slug": "unlisted-dependencies",
"value": 2,
},
{
"score": 1,
"slug": "unlisted-binaries",
"value": 0,
},
{
"score": 1,
"slug": "unresolved-imports",
"value": 0,
},
{
"details": {
"issues": [
{
"message": "Unused export duplicateErrorMsg",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/packages/models/src/lib/category-config.ts",
"position": {
"startColumn": 17,
"startLine": 54,
},
},
},
],
},
"score": 0,
"slug": "unused-exports",
"value": 1,
},
{
"details": {
"issues": [
{
"message": "Unused exported type GroupMeta",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/packages/models/src/lib/group.ts",
"position": {
"startColumn": 13,
"startLine": 26,
},
},
},
],
},
"score": 0,
"slug": "unused-exported-types",
"value": 1,
},
{
"score": 1,
"slug": "unused-exported-enum-members",
"value": 0,
},
{
"details": {
"issues": [
{
"message": "Duplicate export initGenerator|default",
"severity": "error",
"source": {
"file": "/Users/username/Projects/code-pushup/packages/nx-plugin/src/generators/init/generator.ts",
},
},
],
},
"score": 0,
"slug": "duplicate-exports",
"value": 1,
},
]
`;
62 changes: 62 additions & 0 deletions examples/plugins/src/knip/src/reporter/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IssueType } from 'knip/dist/types/issues';

export const ISSUE_TYPES = [
'files',
'dependencies',
'devDependencies',
'optionalPeerDependencies',
'unlisted',
'binaries',
'unresolved',
'exports',
'nsExports',
'types',
'nsTypes',
'enumMembers',
'classMembers',
'duplicates',
] as const;

export const ISSUE_TYPE_TITLE: Record<IssueType | '_files', string> = {
files: 'Unused files',
_files: 'Unused files',
dependencies: 'Unused dependencies',
devDependencies: 'Unused devDependencies',
optionalPeerDependencies: 'Referenced optional peerDependencies',
unlisted: 'Unlisted dependencies',
binaries: 'Unlisted binaries',
unresolved: 'Unresolved imports',
exports: 'Unused exports',
nsExports: 'Exports in used namespace',
types: 'Unused exported types',
nsTypes: 'Exported types in used namespace',
enumMembers: 'Unused exported enum members',
classMembers: 'Unused exported class members',
duplicates: 'Duplicate exports',
} as const;

export const ISSUE_TYPE_MESSAGE: Record<
IssueType | '_files',
(arg: string) => string
> = {
files: (file: string) => `Unused file ${file}`,
// eslint-disable-next-line @typescript-eslint/naming-convention
_files: (file: string) => `Unused file ${file}`,
dependencies: (dep: string) => `Unused dependency ${dep}`,
devDependencies: (dep: string) => `Unused devDependency ${dep}`,
optionalPeerDependencies: (dep: string) =>
`Referenced optional peerDependency ${dep}`,
unlisted: (dep: string) => `Unlisted dependency ${dep}`,
binaries: (binary: string) => `Unlisted binary ${binary}`,
unresolved: (importName: string) => `Unresolved import ${importName}`,
exports: (exportName: string) => `Unused export ${exportName}`,
nsExports: (namespace: string) => `Exports in used namespace ${namespace}`,
types: (type: string) => `Unused exported type ${type}`,
nsTypes: (namespace: string) =>
`Exported types in used namespace ${namespace}`,
enumMembers: (enumMember: string) =>
`Unused exported enum member ${enumMember}`,
classMembers: (classMember: string) =>
`Unused exported class member ${classMember}`,
duplicates: (duplicate: string) => `Duplicate export ${duplicate}`,
} as const;
49 changes: 3 additions & 46 deletions examples/plugins/src/knip/src/reporter/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,5 @@
import type { ReporterOptions } from 'knip';
import { writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { ensureDirectoryExists } from '@code-pushup/utils';
import { knipToCpReport } from './utils';

/**
* @example
*
* npx knip --reporter ./code-pushup.reporter.ts --reporter-options '{"outputFile":"tmp"}'
*
*/
export type CustomReporterOptions = {
outputFile?: string;
rawOutputFile?: string;
};

function parseCustomReporterOptions(
optionsString?: string,
): Record<string, unknown> {
return typeof optionsString === 'string' && optionsString !== ''
? (JSON.parse(optionsString) as Record<string, unknown>)
: {};
}

export const knipReporter = async ({
report,
issues,
options,
}: ReporterOptions) => {
const reporterOptions = parseCustomReporterOptions(
options,
) as CustomReporterOptions;
const { outputFile = `knip-report.json`, rawOutputFile } = reporterOptions;
if (rawOutputFile) {
await ensureDirectoryExists(dirname(rawOutputFile));
await writeFile(
rawOutputFile,
JSON.stringify({ report, issues, options: reporterOptions }, null, 2),
);
}
const result = knipToCpReport({ issues });

await ensureDirectoryExists(dirname(outputFile));
await writeFile(outputFile, JSON.stringify(result, null, 2));
};
import { knipReporter } from './reporter';

export default knipReporter;
export { knipReporter } from './reporter';
export { CustomReporterOptions } from './model';
34 changes: 34 additions & 0 deletions examples/plugins/src/knip/src/reporter/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import chalk from 'chalk';
import { z } from 'zod';
import { filePathSchema } from '@code-pushup/models';

export const customReporterOptionsSchema = z.object({
outputFile: filePathSchema.optional(),
rawOutputFile: filePathSchema.optional(),
});

export type CustomReporterOptions = z.infer<typeof customReporterOptionsSchema>;

export function parseCustomReporterOptions(
optionsString?: string,
): CustomReporterOptions {
// eslint-disable-next-line functional/no-let
let rawJson;
try {
rawJson =
typeof optionsString === 'string' && optionsString !== ''
? (JSON.parse(optionsString) as Record<string, unknown>)
: {};
} catch (error) {
throw new Error(`The passed knip reporter options have to be a JSON parseable string. E.g. --reporter-options='{\\"prop\\":42}'
Option string: ${chalk.bold(optionsString)}
Error: ${(error as Error).message}`);
}

try {
return customReporterOptionsSchema.parse(rawJson);
} catch (error) {
throw new Error(`The reporter options options have to follow the schema.'
Error: ${(error as Error).message}`);
}
}
45 changes: 45 additions & 0 deletions examples/plugins/src/knip/src/reporter/model.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import chalk from 'chalk';
import { describe, expect, it } from 'vitest';
import { CustomReporterOptions, parseCustomReporterOptions } from './model';

describe('parseCustomReporterOptions', () => {
it('should return empty object if no reporter options are given', () => {
expect(parseCustomReporterOptions()).toStrictEqual({});
});

it('should return valid report options', () => {
expect(
parseCustomReporterOptions(
JSON.stringify({
outputFile: 'my-knip-report.json',
rawOutputFile: 'my-knip-raw-report.json',
} satisfies CustomReporterOptions),
),
).toStrictEqual({
outputFile: 'my-knip-report.json',
rawOutputFile: 'my-knip-raw-report.json',
});
});

it('should throw for invalid reporter-options argument', () => {
expect(() => parseCustomReporterOptions('{asd')).toThrow(
`The passed knip reporter options have to be a JSON parseable string. E.g. --reporter-options='{\\"prop\\":42}'`,
);
expect(() => parseCustomReporterOptions('{asd')).toThrow(
`Option string: ${chalk.bold('{asd')}`,
);
expect(() => parseCustomReporterOptions('{asd')).toThrow(
`Error: Unexpected token a in JSON at position 1`,
);
});

it('should throw for invalid options', () => {
const opt = JSON.stringify({
outputFile: '',
} satisfies CustomReporterOptions);
expect(() => parseCustomReporterOptions(opt)).toThrow(
'The reporter options options have to follow the schema.',
);
expect(() => parseCustomReporterOptions(opt)).toThrow('path is invalid');
});
});
38 changes: 38 additions & 0 deletions examples/plugins/src/knip/src/reporter/reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ReporterOptions } from 'knip';
import { writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { ensureDirectoryExists, ui } from '@code-pushup/utils';
import { KNIP_REPORT_NAME } from '../constants';
import { parseCustomReporterOptions } from './model';
import { DeepPartial } from './types';
import { knipToCpReport } from './utils';

/**
* @example
*
* npx knip --reporter ./code-pushup.reporter.ts --reporter-options='{\"outputFile\":\"my-knip-report.json\"}'
*
*/
export const knipReporter = async ({
report,
issues,
options,
}: DeepPartial<ReporterOptions>) => {
const reporterOptions = parseCustomReporterOptions(options);
const { outputFile = KNIP_REPORT_NAME, rawOutputFile } = reporterOptions;

if (rawOutputFile) {
await ensureDirectoryExists(dirname(rawOutputFile));
await writeFile(
rawOutputFile,
JSON.stringify({ report, issues, options: reporterOptions }, null, 2),
);
ui().logger.info(`Saved raw report to ${rawOutputFile}`);
}

const result = await knipToCpReport({ issues, report });

await ensureDirectoryExists(dirname(outputFile));
await writeFile(outputFile, JSON.stringify(result, null, 2));
ui().logger.info(`Saved report to ${outputFile}`);
};
Loading

0 comments on commit 8c8da11

Please sign in to comment.