Skip to content

Commit 2d294e8

Browse files
authored
Merge pull request #4 from code-pushup/add-knip-core-logic
feat: add knip core logic
2 parents e84eb25 + 393ba0e commit 2d294e8

File tree

93 files changed

+5785
-3050
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+5785
-3050
lines changed

.github/workflows/ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ jobs:
3333
- run: git branch --track main origin/main
3434
if: ${{ github.event_name == 'pull_request' }}
3535

36-
- run: npx nx-cloud record -- nx format:check
37-
- run: npx nx affected -t lint test build
36+
- run: npx nx format:check
37+
- run: npx nx affected -t lint unit-test integration-test build

package-lock.json

+4,086-2,583
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,20 @@
4646
"packages/*"
4747
],
4848
"dependencies": {
49-
"@code-pushup/cli": "^0.47.0",
50-
"@code-pushup/core": "^0.47.0",
51-
"@code-pushup/models": "^0.47.0",
52-
"@code-pushup/utils": "^0.47.0",
49+
"@code-pushup/cli": "^0.57.0",
50+
"@code-pushup/core": "^0.57.0",
51+
"@code-pushup/models": "^0.57.0",
52+
"@code-pushup/utils": "^0.57.0",
5353
"@nx/devkit": "19.4.3",
54+
"@poppinss/cliui": "^6.4.1",
5455
"ansis": "^3.8.1",
5556
"knip": "^5.42.0",
5657
"memfs": "^4.17.0",
5758
"zod": "^3.24.1"
59+
},
60+
"optionalDependencies": {
61+
"@nx/nx-darwin-arm64": "^16.9.1",
62+
"@nx/nx-darwin-x64": "^16.10.0",
63+
"@nx/nx-linux-x64-gnu": "16.7.4"
5864
}
5965
}

packages/plugin-knip/.eslintrc.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
"files": ["*.json"],
1313
"parser": "jsonc-eslint-parser",
1414
"rules": {
15-
"@nx/dependency-checks": ["error"]
15+
"@nx/dependency-checks": [
16+
"error",
17+
{
18+
"ignoredDependencies": ["@code-pushup/test-utils"]
19+
}
20+
]
1621
}
1722
}
1823
]

packages/plugin-knip/CONTRIBUTING.MD

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
The [knip](https://knip.dev/) plugin implementation is restricted due to [a bug that wont get fixed](https://github.com/webpro/knip/issues/551).
44

55
As we can't consume knip core logic because it produces an unwanted side effect (linked above), we have to implement a `RunnerConfig` instead of a `RunnerFunction`.
6-
Our current solution is to implement a [custom knip reporter](https://knip.dev/features/reporters#custom-reporters) which is bundled as an [additional entry point](./src/reporter.ts).
6+
Our current solution is to implement a [custom knip reporter](https://knip.dev/features/reporters#custom-reporters) which is bundled as an [additional entry point](src/lib/reporter.ts).
77
The produced output is internally consumed by the `RunnerConfig` and wrapped into a `PluginReport`.

packages/plugin-knip/mocks/fixtures/raw-knip.report.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export const rawReport: Pick<ReporterOptions, 'report' | 'issues' | 'options'> =
150150
'configurationGenerator|default': {
151151
type: 'duplicates',
152152
filePath:
153-
'/Users/michael_hladky/WebstormProjects/quality-metrics-cli/packages/nx-plugin/src/generators/configuration/generator.ts',
153+
'/Users/username/Projects/quality-metrics-cli/packages/nx-plugin/src/generators/configuration/generator.ts',
154154
symbol: 'configurationGenerator|default',
155155
symbols: [
156156
{

packages/plugin-knip/package.json

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
"name": "@code-pushup/knip-plugin",
33
"version": "0.0.0",
44
"license": "MIT",
5-
"dependencies": {},
6-
"peerDependencies": {}
5+
"dependencies": {
6+
"@code-pushup/models": "^0.57.0",
7+
"@code-pushup/utils": "^0.57.0",
8+
"ansis": "^3.8.1",
9+
"knip": "^5.42.0",
10+
"memfs": "^4.17.0",
11+
"tslib": "^2.3.0",
12+
"vite": "^5.0.0",
13+
"vitest": "^1.3.1",
14+
"zod": "^3.24.1"
15+
},
16+
"peerDependencies": {},
17+
"devDependencies": {}
718
}

packages/plugin-knip/project.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
"unit-test": {
2828
"executor": "@nx/vite:test",
2929
"options": {
30-
"config": "packages/plugin-knip/vite.config.unit.ts"
30+
"configFile": "packages/plugin-knip/vite.config.unit.ts"
3131
}
3232
},
3333
"integration-test": {
34+
"dependsOn": ["build"],
3435
"executor": "@nx/vite:test",
3536
"options": {
36-
"config": "packages/plugin-knip/vite.config.integration.ts"
37+
"configFile": "packages/plugin-knip/vite.config.integration.ts"
3738
}
3839
}
3940
},

packages/plugin-knip/src/index.ts

-2
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { auditSchema, groupSchema } from '@code-pushup/models';
3+
import {
4+
KNIP_AUDITS,
5+
KNIP_GROUP_ALL,
6+
KNIP_GROUP_DEPENDENCIES,
7+
KNIP_GROUP_EXPORTS,
8+
KNIP_GROUP_FILES,
9+
} from './constants';
10+
11+
describe('constants-AUDITS', () => {
12+
it.each(KNIP_AUDITS.map((audit) => [audit.slug, audit]))(
13+
'should be a valid %s audit meta info',
14+
(_, audit) => {
15+
expect(() => auditSchema.parse(audit)).not.toThrow();
16+
},
17+
);
18+
});
19+
20+
describe('constants-KNIP_GROUPS', () => {
21+
it('should be a valid file group info', () => {
22+
expect(() => groupSchema.parse(KNIP_GROUP_FILES)).not.toThrow();
23+
});
24+
25+
it('should be a valid exports group info', () => {
26+
expect(() => groupSchema.parse(KNIP_GROUP_EXPORTS)).not.toThrow();
27+
});
28+
29+
it('should be a valid dependencies group info', () => {
30+
expect(() => groupSchema.parse(KNIP_GROUP_DEPENDENCIES)).not.toThrow();
31+
});
32+
33+
it('should be a valid all group info', () => {
34+
expect(() => groupSchema.parse(KNIP_GROUP_ALL)).not.toThrow();
35+
});
36+
});

packages/plugin-knip/src/lib/constants.ts

+186
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,189 @@
1+
import { Audit, Group } from '@code-pushup/models';
2+
3+
export const KNIP_PLUGIN_SLUG = 'knip';
4+
export const KNIP_RAW_REPORT_NAME = 'knip-raw-report.json';
5+
export const KNIP_REPORT_NAME = 'knip-code-pushup-report.json';
6+
7+
const audits = [
8+
{
9+
slug: 'files',
10+
title: 'Unused Files',
11+
description: 'Unable to find a reference to this file',
12+
},
13+
{
14+
slug: 'dependencies',
15+
title: 'Unused Dependencies',
16+
description: 'Unable to find a reference to this dependency',
17+
},
18+
{
19+
slug: 'devdependencies',
20+
title: 'Unused Development Dependencies',
21+
description: 'Unable to find a reference to this devDependency',
22+
},
23+
{
24+
slug: 'optionalpeerdependencies',
25+
title: 'Referenced optional peerDependencies',
26+
description: 'Optional peer dependency is referenced',
27+
},
28+
{
29+
slug: 'unlisted',
30+
title: 'Unlisted dependencies',
31+
description: 'Used dependencies not listed in package.json',
32+
},
33+
{
34+
slug: 'binaries',
35+
title: 'Unlisted binaries',
36+
description: 'Binaries from dependencies not listed in package.json',
37+
},
38+
{
39+
slug: 'unresolved',
40+
title: 'Unresolved imports',
41+
description: 'Unable to resolve this (import) specifier',
42+
},
43+
{
44+
slug: 'exports',
45+
title: 'Unused exports',
46+
description: 'Unable to find a reference to this export',
47+
},
48+
{
49+
slug: 'types',
50+
title: 'Unused exported types',
51+
description: 'Unable to find a reference to this exported type',
52+
},
53+
{
54+
slug: 'nsexports',
55+
title: 'Exports in used namespace',
56+
description: 'Namespace with export is referenced, but not export itself',
57+
},
58+
{
59+
slug: 'nstypes',
60+
title: 'Exported types in used namespace',
61+
description: 'Namespace with type is referenced, but not type itself',
62+
},
63+
{
64+
slug: 'enummembers',
65+
title: 'Unused exported enum members',
66+
description: 'Unable to find a reference to this enum member',
67+
},
68+
{
69+
slug: 'classmembers',
70+
title: 'Unused exported class members',
71+
description: 'Unable to find a reference to this class member',
72+
},
73+
{
74+
slug: 'duplicates',
75+
title: 'Duplicate exports',
76+
description: 'This is exported more than once',
77+
},
78+
] as const satisfies Audit[]; // we use `as const satisfies` to get strict slug typing
79+
80+
export type KnipAudits = (typeof audits)[number]['slug'];
81+
82+
function docsLink(slug: KnipAudits): string {
83+
let anchor = '#';
84+
const base = 'https://knip.dev/guides/handling-issues';
85+
86+
switch (slug) {
87+
case 'files':
88+
anchor = '#unused-files';
89+
break;
90+
case 'dependencies':
91+
case 'devdependencies':
92+
anchor = '#unused-dependencies';
93+
break;
94+
case 'unlisted':
95+
anchor = '#unlisted-dependencies';
96+
break;
97+
case 'optionalpeerdependencies':
98+
anchor = '#referenced-optional-peerDependencies';
99+
break;
100+
case 'unresolved':
101+
anchor = '#unresolved-imports';
102+
break;
103+
case 'exports':
104+
case 'types':
105+
case 'nsexports':
106+
case 'nstypes':
107+
anchor = '#unused-exports';
108+
break;
109+
case 'enummembers':
110+
anchor = '#enum-members';
111+
break;
112+
case 'classmembers':
113+
anchor = '#class-members';
114+
break;
115+
// following cases also default:
116+
// - case 'binaries':
117+
// - case 'duplicates':
118+
default:
119+
return base;
120+
}
121+
122+
return `${base}${anchor}`;
123+
}
124+
125+
export const KNIP_AUDITS = audits.map((audit) => ({
126+
...audit,
127+
docsUrl: docsLink(audit.slug),
128+
}));
129+
130+
export const KNIP_GROUP_FILES = {
131+
slug: 'files',
132+
title: 'All file audits',
133+
description: 'Groups all file related audits',
134+
refs: [{ slug: 'files', weight: 1 }],
135+
} as const satisfies Group;
136+
137+
export const KNIP_GROUP_DEPENDENCIES = {
138+
slug: 'dependencies',
139+
title: 'All dependency audits',
140+
description: 'Groups all dependency related audits',
141+
refs: [
142+
{ slug: 'dependencies', weight: 1 },
143+
{ slug: 'devdependencies', weight: 1 },
144+
{ slug: 'binaries', weight: 1 },
145+
// critical as potentially breaking
146+
{ slug: 'optionalpeerdependencies', weight: 2 },
147+
{ slug: 'unlisted', weight: 2 },
148+
],
149+
} as const satisfies Group;
150+
151+
export const KNIP_GROUP_EXPORTS = {
152+
slug: 'exports',
153+
title: 'All exports related audits',
154+
description: 'Groups all dependency related knip audits',
155+
refs: [
156+
{ slug: 'unresolved', weight: 10 },
157+
{ slug: 'exports', weight: 10 },
158+
{ slug: 'types', weight: 10 },
159+
{ slug: 'nsexports', weight: 10 },
160+
{ slug: 'nstypes', weight: 10 },
161+
{ slug: 'enummembers', weight: 10 },
162+
{ slug: 'classmembers', weight: 10 },
163+
{ slug: 'duplicates', weight: 2 },
164+
],
165+
} as const satisfies Group;
166+
167+
export const KNIP_GROUP_ALL = {
168+
slug: 'all',
169+
title: 'All knip audits',
170+
description: 'Groups all knip audits into a group for easy use',
171+
refs: [
172+
...KNIP_GROUP_FILES.refs,
173+
...KNIP_GROUP_EXPORTS.refs,
174+
...KNIP_GROUP_DEPENDENCIES.refs,
175+
],
176+
} as const satisfies Group;
177+
178+
export const KNIP_GROUPS = [
179+
KNIP_GROUP_FILES,
180+
KNIP_GROUP_EXPORTS,
181+
KNIP_GROUP_DEPENDENCIES,
182+
KNIP_GROUP_ALL,
183+
] as const satisfies Group[]; // we use `as const satisfies` to get strict slug typing;
184+
185+
export type KnipGroups = (typeof KNIP_GROUPS)[number]['slug'];
186+
1187
import { IssueType as KnipIssueType } from 'knip/dist/types/issues';
2188

3189
/**

packages/plugin-knip/src/lib/index.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { knipPlugin } from './knip.plugin';
2+
3+
export { knipPlugin } from './knip.plugin';
4+
5+
export {
6+
KNIP_GROUP_DEPENDENCIES,
7+
KNIP_GROUP_FILES,
8+
KNIP_GROUP_ALL,
9+
KNIP_AUDITS,
10+
KNIP_PLUGIN_SLUG,
11+
KNIP_RAW_REPORT_NAME,
12+
KNIP_REPORT_NAME,
13+
} from './constants';
14+
15+
export { knipCategoryAuditRef, knipCategoryGroupRef } from './utils';
16+
export default knipPlugin;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { join } from 'node:path';
2+
import { PluginConfig } from '@code-pushup/models';
3+
import { KNIP_AUDITS, KNIP_GROUPS, KNIP_PLUGIN_SLUG } from './constants';
4+
import { RunnerOptions, createRunnerConfig } from './runner';
5+
6+
export type PluginOptions = RunnerOptions;
7+
8+
export function knipPlugin(options: PluginOptions = {}): PluginConfig {
9+
const {
10+
outputFile = join(
11+
'.code-pushup',
12+
KNIP_PLUGIN_SLUG,
13+
`knip-report-${Date.now()}.json`,
14+
),
15+
...runnerOptions
16+
} = options;
17+
return {
18+
slug: KNIP_PLUGIN_SLUG,
19+
title: 'Knip',
20+
icon: 'folder-javascript',
21+
description: 'A plugin to track dependencies and duplicates',
22+
runner: createRunnerConfig({
23+
...runnerOptions,
24+
outputFile,
25+
}),
26+
audits: KNIP_AUDITS,
27+
groups: KNIP_GROUPS,
28+
};
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { pluginConfigSchema } from '@code-pushup/models';
3+
import { KNIP_AUDITS, KNIP_GROUPS, KNIP_PLUGIN_SLUG } from './constants';
4+
import { knipPlugin } from './knip.plugin';
5+
6+
describe('knipPlugin-create-config-object', () => {
7+
it('should return valid PluginConfig', () => {
8+
const pluginConfig = knipPlugin({});
9+
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
10+
expect(pluginConfig).toEqual(
11+
expect.objectContaining({
12+
slug: KNIP_PLUGIN_SLUG,
13+
title: 'Knip',
14+
icon: 'folder-javascript',
15+
description: 'A plugin to track dependencies and duplicates',
16+
audits: KNIP_AUDITS,
17+
groups: KNIP_GROUPS,
18+
}),
19+
);
20+
});
21+
});

0 commit comments

Comments
 (0)