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 +260 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +57 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +25 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +155 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +26 -0
- package/dist/reporters/compact.d.ts +7 -0
- package/dist/reporters/compact.js +70 -0
- package/dist/reporters/index.d.ts +13 -0
- package/dist/reporters/index.js +11 -0
- package/dist/reporters/symbols.d.ts +7 -0
- package/dist/reporters/symbols.js +58 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +2 -0
- package/dist/util/config.d.ts +14 -0
- package/dist/util/config.js +49 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +67 -0
- package/package.json +62 -0
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
|
+

|
|
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
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
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,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,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
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -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,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
|
+
}
|