knip 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # ✂️ Knip
2
+
3
+ Knip scans your TypeScript projects for **unused files and exports**. For comparison, ESLint finds unused variables
4
+ inside files in isolation, but this will not be flagged:
5
+
6
+ ```ts
7
+ export const myVar = true;
8
+ ```
9
+
10
+ Unused files will also not be detected by ESLint. So how do you know which files and exports are no longer used? This
11
+ requires an analysis of all the right files in the project.
12
+
13
+ This is where Knip comes in:
14
+
15
+ - [x] Resolves all (unused) files in your project and reports **unused files and exports**.
16
+ - [x] Verifies that exported symbols are actually used in other files, even when part of an imported namespace.
17
+ - [x] Finds duplicate exports of the same symbol.
18
+ - [x] Supports JavaScript inside TypeScript projects (`"allowJs": true`)
19
+ - [ ] Supports JavaScript-only projects with CommonJS and ESM (no `tsconfig.json`) - TODO
20
+
21
+ Knip really shines in larger projects where you have non-production files (such as `/docs`, `/tools` and `/scripts`).
22
+ The `includes` setting in `tsconfig.json` is often too broad, resulting in too many false negatives. Similar projects
23
+ either detect only unimported files, or only unused exports. Most of them don't work by configuring entry files, an
24
+ essential feature to produce good results. This also allows to unleash knip on a specific part of your project, and work
25
+ these separately.
26
+
27
+ ✂️ Knip is another fresh take on keeping your projects clean & tidy!
28
+
29
+ ## Installation
30
+
31
+ ```
32
+ npm install -D knip
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Create a configuration file, let's give it the default name `knip.json` with these contents:
38
+
39
+ ```json
40
+ {
41
+ "entryFiles": ["src/index.ts"],
42
+ "projectFiles": ["src/**/*.ts", "!**/*.spec.ts"]
43
+ }
44
+ ```
45
+
46
+ The `entryFiles` target the starting point(s) to resolve production code dependencies. The `projectFiles` should contain
47
+ all files it should match them against, including potentially unused files.
48
+
49
+ Then run the checks:
50
+
51
+ ```
52
+ npx knip
53
+ ```
54
+
55
+ This will analyze the project and output unused files, exports, types and duplicate exports.
56
+
57
+ Use `--only files` when configuring knip for faster initial results.
58
+
59
+ ## How It Works
60
+
61
+ knip works by creating two sets of files:
62
+
63
+ 1. Production code is the set of files resolved from the `entryFiles`.
64
+ 2. They are matched against the set of `projectFiles`.
65
+ 3. The subset of project files that are not production code will be reported as unused files (in red).
66
+ 4. Then the production code (in blue) will be scanned for unused exports.
67
+
68
+ ![How it works](./assets/how-it-works.drawio.svg)
69
+
70
+ Clean and actionable reports are achieved when non-production code such as tests are excluded from the `projectFiles`
71
+ (using negation patterns such as `!**/*.test.ts`).
72
+
73
+ ## Options
74
+
75
+ ```
76
+ ❯ npx knip
77
+ knip [options]
78
+
79
+ Options:
80
+ -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
81
+ --cwd Working directory (default: current working directory)
82
+ --max-issues Maximum number of unreferenced files until non-zero exit code (default: 1)
83
+ --only Report only listed issue group(s): files, exports, types, nsExports, nsTypes, duplicates
84
+ --exclude Exclude issue group(s) from report: files, exports, types, nsExports, nsTypes, duplicates
85
+ --no-progress Don't show dynamic progress updates
86
+ --reporter Select reporter: symbols, compact (default: symbols)
87
+ --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
88
+
89
+ Examples:
90
+
91
+ $ knip
92
+ $ knip --cwd packages/client --only files
93
+ $ knip -c ./knip.js --reporter compact --jsdoc public
94
+
95
+ More info: https://github.com/webpro/knip
96
+ ```
97
+
98
+ ## Reading the report
99
+
100
+ After analyzing all the files resolved from the `entryFiles` against the `projectFiles`, the report contains the
101
+ following groups of issues:
102
+
103
+ - Unused **files**: no references to this file have been found
104
+ - Unused **exports**: unable to find references to this exported variable
105
+ - Unused exports in namespaces (1): unable to find references to this exported variable, and it has become a member of a
106
+ re-exported namespace (**nsExports**)
107
+ - Unused types: no references to this exported type have been found
108
+ - Unused types in namespaces (1): this exported variable is not directly referenced, and it has become a member a
109
+ re-exported namespace (**nsTypes**)
110
+ - Duplicate exports - the same thing is exported more than once with different names (**duplicates**)
111
+
112
+ Each group type (in **bold**) can be used in the `--only` and `--exclude` arguments to slice & dice the report to your
113
+ needs.
114
+
115
+ 🚀 The process is considerably faster when reporting only the `files` and/or `duplicates` groups.
116
+
117
+ ## Now what?
118
+
119
+ After verifying that files reported as unused are indeed not referenced anywhere, they can be deleted.
120
+
121
+ Remove the `export` keyword in front of unused exports. Then you (or tools such as ESLint) can see whether the variable
122
+ or type is used within its own file. If this is not the case, it can be removed completely.
123
+
124
+ 🔁 Repeat the process to reveal new unused files and exports. Sometimes it's so liberating to delete things.
125
+
126
+ ## More configuration examples
127
+
128
+ ### Test files
129
+
130
+ For best results, it is recommended to exclude files such as tests from the project files. When including tests and
131
+ other non-production files, they may prevent production files from being reported as unused. Not including them will
132
+ make it clear what production files can be removed (including dependent files!).
133
+
134
+ The same goes for any type of non-production files, such as Storybook stories or end-to-end tests.
135
+
136
+ To report dangling files and exports that are not used by any of the production or test files, include both to the set
137
+ of `entryFiles`:
138
+
139
+ ```json
140
+ {
141
+ "entryFiles": ["src/index.ts", "src/**/*.spec.ts"],
142
+ "projectFiles": ["src/**/*.ts", "!**/*.e2e.ts"]
143
+ }
144
+ ```
145
+
146
+ In theory this idea could be extended to report some kind of test coverage.
147
+
148
+ ### Monorepos
149
+
150
+ #### Separate packages
151
+
152
+ In repos with multiple packages, the `--cwd` option comes in handy. With similar package structures, the packages can be
153
+ configured using globs:
154
+
155
+ ```json
156
+ {
157
+ "packages/*": {
158
+ "entryFiles": ["src/index.ts"],
159
+ "projectFiles": ["src/**/*.{ts,tsx}", "!**/*.spec.{ts,tsx}"]
160
+ }
161
+ }
162
+ ```
163
+
164
+ Packages can also be explicitly configured per package directory.
165
+
166
+ To scan the packages separately, using the first match from the configuration file:
167
+
168
+ ```
169
+ knip --cwd packages/client --config knip.json
170
+ knip --cwd packages/services --config knip.json
171
+ ```
172
+
173
+ #### Connected projects
174
+
175
+ A good example of a large project setup is a monorepo, such as created with Nx. Let's take an example project
176
+ configuration for an Nx project using Next.js, Jest and Storybook. This can also be a JavaScript file, which allows to
177
+ add logic and/or comments:
178
+
179
+ ```js
180
+ const entryFiles = ['apps/**/pages/**/*.{js,ts,tsx}'];
181
+
182
+ const projectFiles = [
183
+ '{apps,libs}/**/*.{ts,tsx}',
184
+ // Next.js
185
+ '!**/next.config.js',
186
+ '!**/apps/**/public/**',
187
+ '!**/apps/**/next-env.d.ts'
188
+ // Jest
189
+ '!**/jest.config.ts',
190
+ '!**/*.spec.{ts,tsx}',
191
+ // Storybook
192
+ '!**/.storybook/**',
193
+ '!**/*.stories.tsx',
194
+ ];
195
+
196
+ module.exports = { entryFiles, projectFiles };
197
+ ```
198
+
199
+ This should give good results about unused files and exports for the monorepo. After the first run, the configuration
200
+ can be tweaked further to the project structure.
201
+
202
+ ## Example Output
203
+
204
+ ### Default reporter
205
+
206
+ ```
207
+ $ knip --config ./knip.json
208
+ --- UNUSED FILES (2)
209
+ src/chat/helpers.ts
210
+ src/components/SideBar.tsx
211
+ --- UNUSED EXPORTS (5)
212
+ lowercaseFirstLetter src/common/src/string/index.ts
213
+ RegistrationBox src/components/Registration.tsx
214
+ clamp src/css.ts
215
+ restoreSession src/services/authentication.ts
216
+ PREFIX src/services/authentication.ts
217
+ --- UNUSED TYPES (4)
218
+ enum RegistrationServices src/components/Registration/registrationMachine.ts
219
+ type RegistrationAction src/components/Registration/registrationMachine.ts
220
+ type ComponentProps src/components/Registration.tsx
221
+ interface ProductDetail src/types/Product.ts
222
+ --- DUPLICATE EXPORTS (2)
223
+ Registration, default src/components/Registration.tsx
224
+ ProductsList, default src/components/Products.tsx
225
+ ```
226
+
227
+ ### Compact
228
+
229
+ ```
230
+ $ knip --config ./knip.json --reporter compact
231
+ --- UNUSED FILES (2)
232
+ src/chat/helpers.ts
233
+ src/components/SideBar.tsx
234
+ --- UNUSED EXPORTS (4)
235
+ src/common/src/string/index.ts: lowercaseFirstLetter
236
+ src/components/Registration.tsx: RegistrationBox
237
+ src/css.ts: clamp
238
+ src/services/authentication.ts: restoreSession, PREFIX
239
+ --- UNUSED TYPES (3)
240
+ src/components/Registration/registrationMachine.ts: RegistrationServices, RegistrationAction
241
+ src/components/Registration.tsx: ComponentProps
242
+ src/types/Product.ts: ProductDetail
243
+ --- DUPLICATE EXPORTS (2)
244
+ src/components/Registration.tsx: Registration, default
245
+ src/components/Products.tsx: ProductsList, default
246
+ ```
247
+
248
+ ## Why Yet Another unused file/export finder?
249
+
250
+ There are some fine modules available in the same category:
251
+
252
+ - [unimported](https://github.com/smeijer/unimported)
253
+ - [ts-unused-exports](https://github.com/pzavolinsky/ts-unused-exports)
254
+ - [no-unused-export](https://github.com/plantain-00/no-unused-export)
255
+ - [ts-prune](https://github.com/nadeesha/ts-prune)
256
+ - [find-unused-exports](https://github.com/jaydenseric/find-unused-exports)
257
+
258
+ However, the results where not always accurate, and none of them tick my boxes to find both unused files and exports. Or
259
+ let me configure entry files and scope the project files for clean results. Especially for larger projects this kind of
260
+ configuration is necessary. That's why I took another stab at it.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_util_1 = require("node:util");
9
+ const help_1 = require("./help");
10
+ const config_1 = require("./util/config");
11
+ const reporters_1 = __importDefault(require("./reporters"));
12
+ const _1 = require(".");
13
+ const { values: { help, cwd: cwdArg, config = 'knip.json', only = [], exclude = [], 'no-progress': noProgress = false, reporter = 'symbols', jsdoc = [], 'max-issues': maxIssues = '1', }, } = (0, node_util_1.parseArgs)({
14
+ options: {
15
+ help: { type: 'boolean' },
16
+ cwd: { type: 'string' },
17
+ config: { type: 'string', short: 'c' },
18
+ only: { type: 'string', multiple: true },
19
+ exclude: { type: 'string', multiple: true },
20
+ 'no-progress': { type: 'boolean' },
21
+ reporter: { type: 'string' },
22
+ jsdoc: { type: 'string', multiple: true },
23
+ 'max-issues': { type: 'string' },
24
+ },
25
+ });
26
+ if (help) {
27
+ (0, help_1.printHelp)();
28
+ process.exit(0);
29
+ }
30
+ const cwd = cwdArg ? node_path_1.default.resolve(cwdArg) : process.cwd();
31
+ const configuration = (0, config_1.importConfig)(cwd, config);
32
+ if (!configuration) {
33
+ (0, help_1.printHelp)();
34
+ process.exit(1);
35
+ }
36
+ const isShowProgress = noProgress !== false || (process.stdout.isTTY && typeof process.stdout.cursorTo === 'function');
37
+ const report = reporter in reporters_1.default ? reporters_1.default[reporter] : require(node_path_1.default.join(cwd, reporter));
38
+ const main = async () => {
39
+ const resolvedConfig = (0, config_1.resolveConfig)(configuration, cwdArg);
40
+ if (!resolvedConfig) {
41
+ (0, help_1.printHelp)();
42
+ process.exit(1);
43
+ }
44
+ const config = Object.assign({}, resolvedConfig, {
45
+ cwd,
46
+ include: (0, config_1.resolveIncludedFromArgs)(only, exclude),
47
+ isShowProgress,
48
+ jsDocOptions: {
49
+ isReadPublicTag: jsdoc.includes('public'),
50
+ },
51
+ });
52
+ const { issues, counters } = await (0, _1.run)(config);
53
+ report({ issues, cwd, config });
54
+ if (counters.files > Number(maxIssues))
55
+ process.exit(counters.files);
56
+ };
57
+ main();
package/dist/help.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const printHelp: () => void;
package/dist/help.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printHelp = void 0;
4
+ const printHelp = () => {
5
+ console.log(`knip [options]
6
+
7
+ Options:
8
+ -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
9
+ --cwd Working directory (default: current working directory)
10
+ --max-issues Maximum number of unreferenced files until non-zero exit code (default: 1)
11
+ --only Report only listed issue group(s): files, exports, types, nsExports, nsTypes, duplicates
12
+ --exclude Exclude issue group(s) from report: files, exports, types, nsExports, nsTypes, duplicates
13
+ --no-progress Don't show dynamic progress updates
14
+ --reporter Select reporter: symbols, compact (default: symbols)
15
+ --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
16
+
17
+ Examples:
18
+
19
+ $ knip
20
+ $ knip --cwd packages/client --only files
21
+ $ knip -c ./knip.js --reporter compact --jsdoc public
22
+
23
+ More info: https://github.com/webpro/knip`);
24
+ };
25
+ exports.printHelp = printHelp;
@@ -0,0 +1,13 @@
1
+ import type { Configuration, Issues } from './types';
2
+ export declare function run(configuration: Configuration): Promise<{
3
+ issues: Issues;
4
+ counters: {
5
+ files: number;
6
+ exports: number;
7
+ types: number;
8
+ nsExports: number;
9
+ nsTypes: number;
10
+ duplicates: number;
11
+ processed: number;
12
+ };
13
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const ts_morph_1 = require("ts-morph");
9
+ const ts_morph_helpers_1 = require("ts-morph-helpers");
10
+ const util_1 = require("./util");
11
+ const log_1 = require("./log");
12
+ const lineRewriter = new log_1.LineRewriter();
13
+ async function run(configuration) {
14
+ const { cwd, isShowProgress, include, jsDocOptions } = configuration;
15
+ const production = await (0, util_1.createProject)(cwd, configuration.entryFiles);
16
+ const entryFiles = production.getSourceFiles();
17
+ production.resolveSourceFileDependencies();
18
+ const productionFiles = production.getSourceFiles();
19
+ const project = await (0, util_1.createProject)(cwd, configuration.projectFiles);
20
+ const projectFiles = project.getSourceFiles();
21
+ const [usedProductionFiles, unreferencedProductionFiles] = (0, util_1.partitionSourceFiles)(projectFiles, productionFiles);
22
+ const [, usedNonEntryFiles] = (0, util_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
23
+ const issues = {
24
+ files: new Set(unreferencedProductionFiles.map(file => file.getFilePath())),
25
+ exports: {},
26
+ types: {},
27
+ nsExports: {},
28
+ nsTypes: {},
29
+ duplicates: {},
30
+ };
31
+ const counters = {
32
+ files: issues.files.size,
33
+ exports: 0,
34
+ types: 0,
35
+ nsExports: 0,
36
+ nsTypes: 0,
37
+ duplicates: 0,
38
+ processed: issues.files.size,
39
+ };
40
+ const updateProcessingOutput = (item) => {
41
+ if (!isShowProgress)
42
+ return;
43
+ const counter = unreferencedProductionFiles.length + counters.processed;
44
+ const total = unreferencedProductionFiles.length + usedNonEntryFiles.length;
45
+ const percentage = Math.floor((counter / total) * 100);
46
+ const messages = [(0, log_1.getLine)(`${percentage}%`, `of files processed (${counter} of ${total})`)];
47
+ include.files && messages.push((0, log_1.getLine)(unreferencedProductionFiles.length, 'unused files'));
48
+ include.exports && messages.push((0, log_1.getLine)(counters.exports, 'unused exports'));
49
+ include.nsExports && messages.push((0, log_1.getLine)(counters.nsExports, 'unused exports in namespace'));
50
+ include.types && messages.push((0, log_1.getLine)(counters.types, 'unused types'));
51
+ include.nsTypes && messages.push((0, log_1.getLine)(counters.nsTypes, 'unused types in namespace'));
52
+ include.duplicates && messages.push((0, log_1.getLine)(counters.duplicates, 'duplicate exports'));
53
+ if (counter < total) {
54
+ messages.push('');
55
+ messages.push(`Processing: ${node_path_1.default.relative(cwd, item.filePath)}`);
56
+ }
57
+ lineRewriter.update(messages);
58
+ };
59
+ const addIssue = (issueType, issue) => {
60
+ const { filePath, symbol } = issue;
61
+ const key = node_path_1.default.relative(cwd, filePath);
62
+ issues[issueType][key] = issues[issueType][key] ?? {};
63
+ issues[issueType][key][symbol] = issue;
64
+ counters[issueType]++;
65
+ updateProcessingOutput(issue);
66
+ };
67
+ if (include.exports || include.types || include.nsExports || include.nsTypes || include.duplicates) {
68
+ usedNonEntryFiles.forEach(sourceFile => {
69
+ const filePath = sourceFile.getFilePath();
70
+ const exportDeclarations = sourceFile.getExportedDeclarations();
71
+ if (include.duplicates) {
72
+ const duplicateExports = (0, ts_morph_helpers_1.findDuplicateExportedNames)(sourceFile);
73
+ duplicateExports.forEach(symbols => {
74
+ const symbol = symbols.join('|');
75
+ addIssue('duplicates', { filePath, symbol, symbols });
76
+ });
77
+ }
78
+ if (include.exports || include.types || include.nsExports || include.nsTypes) {
79
+ const uniqueExportedSymbols = new Set([...exportDeclarations.values()].flat());
80
+ if (uniqueExportedSymbols.size === 1)
81
+ return;
82
+ exportDeclarations.forEach(declarations => {
83
+ declarations.forEach(declaration => {
84
+ const type = (0, util_1.getType)(declaration);
85
+ if (!include.nsExports && !include.nsTypes) {
86
+ if (!include.types && type)
87
+ return;
88
+ if (!include.exports && !type)
89
+ return;
90
+ }
91
+ if (jsDocOptions.isReadPublicTag && ts_morph_1.ts.getJSDocPublicTag(declaration.compilerNode))
92
+ return;
93
+ let identifier;
94
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.Identifier)) {
95
+ identifier = declaration;
96
+ }
97
+ else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.FunctionDeclaration) ||
98
+ declaration.isKind(ts_morph_1.ts.SyntaxKind.ClassDeclaration) ||
99
+ declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration) ||
100
+ declaration.isKind(ts_morph_1.ts.SyntaxKind.InterfaceDeclaration) ||
101
+ declaration.isKind(ts_morph_1.ts.SyntaxKind.EnumDeclaration)) {
102
+ identifier = declaration.getFirstChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
103
+ }
104
+ else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.PropertyAccessExpression)) {
105
+ identifier = declaration.getLastChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
106
+ }
107
+ else {
108
+ identifier = declaration.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.Identifier);
109
+ }
110
+ if (identifier) {
111
+ const identifierText = identifier.getText();
112
+ if (include.exports && issues.exports[filePath]?.[identifierText])
113
+ return;
114
+ if (include.types && issues.types[filePath]?.[identifierText])
115
+ return;
116
+ if (include.nsExports && issues.nsExports[filePath]?.[identifierText])
117
+ return;
118
+ if (include.nsTypes && issues.nsTypes[filePath]?.[identifierText])
119
+ return;
120
+ const refs = identifier.findReferences();
121
+ if (refs.length === 0) {
122
+ addIssue('exports', { filePath, symbol: identifierText });
123
+ }
124
+ else {
125
+ const refFiles = new Set(refs.map(r => r.compilerObject.references.map(r => r.fileName)).flat());
126
+ const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === sourceFile.getFilePath();
127
+ if (!isReferencedOnlyBySelf)
128
+ return;
129
+ if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
130
+ if (type) {
131
+ addIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
132
+ }
133
+ else {
134
+ addIssue('nsExports', { filePath, symbol: identifierText });
135
+ }
136
+ }
137
+ else if (type) {
138
+ addIssue('types', { filePath, symbol: identifierText, symbolType: type });
139
+ }
140
+ else {
141
+ addIssue('exports', { filePath, symbol: identifierText });
142
+ }
143
+ }
144
+ }
145
+ });
146
+ });
147
+ }
148
+ counters.processed++;
149
+ });
150
+ }
151
+ if (isShowProgress)
152
+ lineRewriter.resetLines();
153
+ return { issues, counters };
154
+ }
155
+ exports.run = run;
package/dist/log.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare const getLine: (value: number | string, text: string) => string;
2
+ export declare class LineRewriter {
3
+ private lines;
4
+ private clearLines;
5
+ resetLines(): void;
6
+ update(messages: string[]): void;
7
+ }
package/dist/log.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LineRewriter = exports.getLine = void 0;
4
+ const getLine = (value, text) => `${String(value).padStart(5)} ${text}`;
5
+ exports.getLine = getLine;
6
+ class LineRewriter {
7
+ lines = 0;
8
+ clearLines(count) {
9
+ if (count > 0) {
10
+ for (let i = 0; i < count; i++) {
11
+ process.stdout.moveCursor(0, -1);
12
+ process.stdout.clearLine(1);
13
+ }
14
+ }
15
+ process.stdout.cursorTo(0);
16
+ }
17
+ resetLines() {
18
+ this.clearLines(this.lines);
19
+ }
20
+ update(messages) {
21
+ this.resetLines();
22
+ process.stdout.write(messages.join('\n') + '\n');
23
+ this.lines = messages.length;
24
+ }
25
+ }
26
+ exports.LineRewriter = LineRewriter;
@@ -0,0 +1,7 @@
1
+ import type { Issues, Configuration } from '../types';
2
+ declare const _default: ({ issues, config, cwd }: {
3
+ issues: Issues;
4
+ config: Configuration;
5
+ cwd: string;
6
+ }) => void;
7
+ export default _default;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ const logIssueLine = (cwd, filePath, symbols) => {
8
+ console.log(`${node_path_1.default.relative(cwd, filePath)}${symbols ? `: ${symbols.join(', ')}` : ''}`);
9
+ };
10
+ const logIssueGroupResult = (issues, cwd, title) => {
11
+ title && console.log(`--- ${title} (${issues.length})`);
12
+ if (issues.length) {
13
+ issues.sort().forEach(filePath => logIssueLine(cwd, filePath));
14
+ }
15
+ else {
16
+ console.log('N/A');
17
+ }
18
+ };
19
+ const logIssueGroupResults = (issues, cwd, title) => {
20
+ title && console.log(`--- ${title} (${issues.length})`);
21
+ if (issues.length) {
22
+ const sortedByFilePath = issues.sort((a, b) => (a.filePath > b.filePath ? 1 : -1));
23
+ sortedByFilePath.forEach(({ filePath, symbols }) => logIssueLine(cwd, filePath, symbols));
24
+ }
25
+ else {
26
+ console.log('N/A');
27
+ }
28
+ };
29
+ exports.default = ({ issues, config, cwd }) => {
30
+ const { include } = config;
31
+ const reportMultipleGroups = Object.values(include).filter(Boolean).length > 1;
32
+ if (include.files) {
33
+ const unreferencedFiles = Array.from(issues.files);
34
+ logIssueGroupResult(unreferencedFiles, cwd, reportMultipleGroups && 'UNREFERENCED FILES');
35
+ }
36
+ if (include.exports) {
37
+ const unreferencedExports = Object.values(issues.exports).map(issues => {
38
+ const items = Object.values(issues);
39
+ return { ...items[0], symbols: items.map(i => i.symbol) };
40
+ });
41
+ logIssueGroupResults(unreferencedExports, cwd, reportMultipleGroups && 'UNREFERENCED EXPORTS');
42
+ }
43
+ if (include.nsExports) {
44
+ const unreferencedNsExports = Object.values(issues.nsExports).map(issues => {
45
+ const items = Object.values(issues);
46
+ return { ...items[0], symbols: items.map(i => i.symbol) };
47
+ });
48
+ logIssueGroupResults(unreferencedNsExports, cwd, reportMultipleGroups && 'UNREFERENCED EXPORTS IN NAMESPACE');
49
+ }
50
+ if (include.types) {
51
+ const unreferencedTypes = Object.values(issues.types).map(issues => {
52
+ const items = Object.values(issues);
53
+ return { ...items[0], symbols: items.map(i => i.symbol) };
54
+ });
55
+ logIssueGroupResults(unreferencedTypes, cwd, reportMultipleGroups && 'UNREFERENCED TYPES');
56
+ }
57
+ if (include.nsTypes) {
58
+ const unreferencedNsTypes = Object.values(issues.nsTypes).map(issues => {
59
+ const items = Object.values(issues);
60
+ return { ...items[0], symbols: items.map(i => i.symbol) };
61
+ });
62
+ logIssueGroupResults(unreferencedNsTypes, cwd, reportMultipleGroups && 'UNREFERENCED TYPES IN NAMESPACE');
63
+ }
64
+ if (include.duplicates) {
65
+ const unreferencedDuplicates = Object.values(issues.duplicates)
66
+ .map(issues => Object.values(issues))
67
+ .flat();
68
+ logIssueGroupResults(unreferencedDuplicates, cwd, reportMultipleGroups && 'DUPLICATE EXPORTS');
69
+ }
70
+ };
@@ -0,0 +1,13 @@
1
+ declare const _default: {
2
+ symbols: ({ issues, config, cwd }: {
3
+ issues: import("../types").Issues;
4
+ config: import("../types").Configuration;
5
+ cwd: string;
6
+ }) => void;
7
+ compact: ({ issues, config, cwd }: {
8
+ issues: import("../types").Issues;
9
+ config: import("../types").Configuration;
10
+ cwd: string;
11
+ }) => void;
12
+ };
13
+ export default _default;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const symbols_1 = __importDefault(require("./symbols"));
7
+ const compact_1 = __importDefault(require("./compact"));
8
+ exports.default = {
9
+ symbols: symbols_1.default,
10
+ compact: compact_1.default,
11
+ };
@@ -0,0 +1,7 @@
1
+ import type { Issues, Configuration } from '../types';
2
+ declare const _default: ({ issues, config, cwd }: {
3
+ issues: Issues;
4
+ config: Configuration;
5
+ cwd: string;
6
+ }) => void;
7
+ export default _default;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ const logIssueLine = ({ issue, cwd, padding }) => {
8
+ const symbols = issue.symbols ? issue.symbols.join(', ') : issue.symbol;
9
+ console.log(`${symbols.padEnd(padding + 2)}${issue.symbolType?.padEnd(11) || ''}${node_path_1.default.relative(cwd, issue.filePath)}`);
10
+ };
11
+ const logIssueGroupResult = (issues, cwd, title) => {
12
+ title && console.log(`--- ${title} (${issues.length})`);
13
+ if (issues.length) {
14
+ issues.sort().forEach(filePath => console.log(node_path_1.default.relative(cwd, filePath)));
15
+ }
16
+ else {
17
+ console.log('N/A');
18
+ }
19
+ };
20
+ const logIssueGroupResults = (issues, cwd, title) => {
21
+ title && console.log(`--- ${title} (${issues.length})`);
22
+ if (issues.length) {
23
+ const sortedByFilePath = issues.sort((a, b) => (a.filePath > b.filePath ? 1 : -1));
24
+ const padding = [...issues].sort((a, b) => b.symbol.length - a.symbol.length)[0].symbol.length;
25
+ sortedByFilePath.forEach(issue => logIssueLine({ issue, cwd, padding }));
26
+ }
27
+ else {
28
+ console.log('N/A');
29
+ }
30
+ };
31
+ exports.default = ({ issues, config, cwd }) => {
32
+ const { include } = config;
33
+ const reportMultipleGroups = Object.values(include).filter(Boolean).length > 1;
34
+ if (include.files) {
35
+ const unreferencedFiles = Array.from(issues.files);
36
+ logIssueGroupResult(unreferencedFiles, cwd, reportMultipleGroups && 'UNUSED FILES');
37
+ }
38
+ if (include.exports) {
39
+ const unreferencedExports = Object.values(issues.exports).map(Object.values).flat();
40
+ logIssueGroupResults(unreferencedExports, cwd, reportMultipleGroups && 'UNUSED EXPORTS');
41
+ }
42
+ if (include.nsExports) {
43
+ const unreferencedNsExports = Object.values(issues.nsExports).map(Object.values).flat();
44
+ logIssueGroupResults(unreferencedNsExports, cwd, reportMultipleGroups && 'UNUSED EXPORTS IN NAMESPACE');
45
+ }
46
+ if (include.types) {
47
+ const unreferencedTypes = Object.values(issues.types).map(Object.values).flat();
48
+ logIssueGroupResults(unreferencedTypes, cwd, reportMultipleGroups && 'UNUSED TYPES');
49
+ }
50
+ if (include.nsTypes) {
51
+ const unreferencedNsTypes = Object.values(issues.nsTypes).map(Object.values).flat();
52
+ logIssueGroupResults(unreferencedNsTypes, cwd, reportMultipleGroups && 'UNUSED TYPES IN NAMESPACE');
53
+ }
54
+ if (include.duplicates) {
55
+ const unreferencedDuplicates = Object.values(issues.duplicates).map(Object.values).flat();
56
+ logIssueGroupResults(unreferencedDuplicates, cwd, reportMultipleGroups && 'DUPLICATE EXPORTS');
57
+ }
58
+ };
@@ -0,0 +1,35 @@
1
+ declare type FilePath = string;
2
+ declare type SymbolType = 'type' | 'interface' | 'enum';
3
+ declare type UnusedFileIssues = Set<FilePath>;
4
+ declare type UnusedExportIssues = Record<string, Record<string, Issue>>;
5
+ export declare type Issue = {
6
+ filePath: FilePath;
7
+ symbol: string;
8
+ symbols?: string[];
9
+ symbolType?: SymbolType;
10
+ };
11
+ export declare type Issues = {
12
+ files: UnusedFileIssues;
13
+ exports: UnusedExportIssues;
14
+ types: UnusedExportIssues;
15
+ nsExports: UnusedExportIssues;
16
+ nsTypes: UnusedExportIssues;
17
+ duplicates: UnusedExportIssues;
18
+ };
19
+ export declare type IssueType = keyof Issues;
20
+ declare type LocalConfiguration = {
21
+ entryFiles: string[];
22
+ projectFiles: string[];
23
+ };
24
+ export declare type Configuration = LocalConfiguration & {
25
+ cwd: string;
26
+ include: {
27
+ [key in IssueType]: boolean;
28
+ };
29
+ isShowProgress: boolean;
30
+ jsDocOptions: {
31
+ isReadPublicTag: boolean;
32
+ };
33
+ };
34
+ export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
35
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ import type { ImportedConfiguration } from '../types';
2
+ export declare const importConfig: (cwd: string, configArg: string) => any;
3
+ export declare const resolveConfig: (importedConfiguration: ImportedConfiguration, cwdArg?: string) => {
4
+ entryFiles: string[];
5
+ projectFiles: string[];
6
+ } | undefined;
7
+ export declare const resolveIncludedFromArgs: (onlyArg: string[], excludeArg: string[]) => {
8
+ files: boolean;
9
+ exports: boolean;
10
+ types: boolean;
11
+ nsExports: boolean;
12
+ nsTypes: boolean;
13
+ duplicates: boolean;
14
+ };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveIncludedFromArgs = exports.resolveConfig = exports.importConfig = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const micromatch_1 = __importDefault(require("micromatch"));
9
+ const importConfig = (cwd, configArg) => {
10
+ try {
11
+ const manifest = require(node_path_1.default.join(cwd, 'package.json'));
12
+ if ('knip' in manifest)
13
+ return manifest.knip;
14
+ else
15
+ throw new Error('Unable to find `knip` key in package.json');
16
+ }
17
+ catch (error) {
18
+ try {
19
+ return require(node_path_1.default.resolve(configArg));
20
+ }
21
+ catch (error) {
22
+ console.error(`Unable to find configuration at ${node_path_1.default.join(cwd, configArg)}\n`);
23
+ }
24
+ }
25
+ };
26
+ exports.importConfig = importConfig;
27
+ const resolveConfig = (importedConfiguration, cwdArg) => {
28
+ if (cwdArg && !('projectFiles' in importedConfiguration)) {
29
+ const importedConfigKey = Object.keys(importedConfiguration).find(pattern => micromatch_1.default.isMatch(cwdArg, pattern));
30
+ if (importedConfigKey) {
31
+ return importedConfiguration[importedConfigKey];
32
+ }
33
+ }
34
+ if (!cwdArg && (!importedConfiguration.entryFiles || !importedConfiguration.projectFiles)) {
35
+ console.error('Unable to find `entryFiles` and/or `projectFiles` in configuration.');
36
+ console.info('Add it at root level, or use the --cwd argument with a matching configuration.\n');
37
+ return;
38
+ }
39
+ return importedConfiguration;
40
+ };
41
+ exports.resolveConfig = resolveConfig;
42
+ const resolveIncludedFromArgs = (onlyArg, excludeArg) => {
43
+ const groups = ['files', 'exports', 'types', 'nsExports', 'nsTypes', 'duplicates'];
44
+ const only = onlyArg.map(value => value.split(',')).flat();
45
+ const exclude = excludeArg.map(value => value.split(',')).flat();
46
+ const includes = (only.length > 0 ? only : groups).filter((group) => !exclude.includes(group));
47
+ return groups.reduce((r, group) => ((r[group] = includes.includes(group)), r), {});
48
+ };
49
+ exports.resolveIncludedFromArgs = resolveIncludedFromArgs;
package/dist/util.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Project } from 'ts-morph';
2
+ import type { SourceFile, ExportedDeclarations } from 'ts-morph';
3
+ export declare const createProject: (cwd: string, paths?: string | string[]) => Promise<Project>;
4
+ export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
5
+ export declare const getType: (declaration: ExportedDeclarations) => "type" | "interface" | "enum" | undefined;
package/dist/util.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getType = exports.partitionSourceFiles = exports.createProject = void 0;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const ts_morph_1 = require("ts-morph");
10
+ const isFile = async (filePath) => {
11
+ try {
12
+ const stats = await promises_1.default.stat(filePath);
13
+ return stats.isFile();
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ };
19
+ const findFile = async (cwd, fileName) => {
20
+ const filePath = node_path_1.default.join(cwd, fileName);
21
+ if (await isFile(filePath))
22
+ return filePath;
23
+ return findFile(node_path_1.default.resolve(cwd, '..'), fileName);
24
+ };
25
+ const resolvePaths = (cwd, patterns) => {
26
+ return [patterns].flat().map(pattern => {
27
+ if (pattern.startsWith('!'))
28
+ return '!' + node_path_1.default.join(cwd, pattern.slice(1));
29
+ return node_path_1.default.join(cwd, pattern);
30
+ });
31
+ };
32
+ const createProject = async (cwd, paths) => {
33
+ const tsConfigFilePath = await findFile(cwd, 'tsconfig.json');
34
+ const workspace = new ts_morph_1.Project({
35
+ tsConfigFilePath,
36
+ skipAddingFilesFromTsConfig: true,
37
+ skipFileDependencyResolution: true,
38
+ });
39
+ if (paths)
40
+ workspace.addSourceFilesAtPaths(resolvePaths(cwd, paths));
41
+ return workspace;
42
+ };
43
+ exports.createProject = createProject;
44
+ const partitionSourceFiles = (projectFiles, productionFiles) => {
45
+ const productionFilePaths = productionFiles.map(file => file.getFilePath());
46
+ const usedFiles = [];
47
+ const unusedFiles = [];
48
+ projectFiles.forEach(projectFile => {
49
+ if (productionFilePaths.includes(projectFile.getFilePath())) {
50
+ usedFiles.push(projectFile);
51
+ }
52
+ else {
53
+ unusedFiles.push(projectFile);
54
+ }
55
+ });
56
+ return [usedFiles, unusedFiles];
57
+ };
58
+ exports.partitionSourceFiles = partitionSourceFiles;
59
+ const getType = (declaration) => {
60
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration))
61
+ return 'type';
62
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.InterfaceDeclaration))
63
+ return 'interface';
64
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.EnumDeclaration))
65
+ return 'enum';
66
+ };
67
+ exports.getType = getType;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "knip",
3
+ "version": "0.1.2",
4
+ "description": "Find unused files and exports in your TypeScript project",
5
+ "keywords": [
6
+ "find",
7
+ "detect",
8
+ "unused",
9
+ "files",
10
+ "exports",
11
+ "types",
12
+ "duplicates",
13
+ "typescript",
14
+ "maintenance",
15
+ "unimported"
16
+ ],
17
+ "repository": "github:webpro/knip",
18
+ "homepage": "https://github.com/webpro/knip",
19
+ "bugs": "https://github.com/webpro/knip/issues",
20
+ "main": "dist/index.js",
21
+ "bin": {
22
+ "knip": "dist/cli.js"
23
+ },
24
+ "scripts": {
25
+ "knip": "node ./dist/cli.js",
26
+ "test": "node --loader tsx --test test/*.spec.ts",
27
+ "watch": "tsc --watch",
28
+ "build": "rm -rf dist && tsc",
29
+ "prepublishOnly": "npm test && npm run build && npm run knip",
30
+ "release": "release-it"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "author": {
36
+ "name": "Lars Kappert",
37
+ "email": "lars@webpro.nl"
38
+ },
39
+ "license": "ISC",
40
+ "dependencies": {
41
+ "micromatch": "4.0.5",
42
+ "ts-morph": "16.0.0",
43
+ "ts-morph-helpers": "0.5.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/micromatch": "4.0.2",
47
+ "@types/node": "18.8.1",
48
+ "prettier": "2.7.1",
49
+ "release-it": "15.5.0",
50
+ "tsx": "3.9.0",
51
+ "typescript": "4.8.4"
52
+ },
53
+ "release-it": {
54
+ "github": {
55
+ "release": true
56
+ }
57
+ },
58
+ "engines": {
59
+ "node": ">=16.17.0"
60
+ },
61
+ "engineStrict": true
62
+ }