knip 0.3.0 → 0.4.1
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 +65 -31
- package/dist/cli.js +33 -60
- package/dist/help.js +13 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +71 -186
- package/dist/reporters/compact.d.ts +4 -3
- package/dist/reporters/compact.js +1 -2
- package/dist/reporters/index.d.ts +6 -4
- package/dist/reporters/symbols.d.ts +4 -3
- package/dist/reporters/symbols.js +1 -2
- package/dist/runner.d.ts +16 -0
- package/dist/runner.js +193 -0
- package/dist/types.d.ts +24 -9
- package/dist/util/config.d.ts +1 -10
- package/dist/util/config.js +1 -2
- package/dist/util/fs.d.ts +2 -0
- package/dist/util/fs.js +29 -0
- package/dist/util/path.d.ts +8 -1
- package/dist/util/path.js +19 -19
- package/dist/util/project.d.ts +5 -3
- package/dist/util/project.js +3 -16
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ✂️ Knip
|
|
2
2
|
|
|
3
|
-
Knip scans your JavaScript and TypeScript projects for **unused files, dependencies and exports
|
|
4
|
-
|
|
3
|
+
Knip scans your JavaScript and TypeScript projects for **unused files, dependencies and exports**: things that can be
|
|
4
|
+
removed! Less code means better performance and less to maintain, important for both UX and DX!
|
|
5
5
|
|
|
6
6
|
For comparison, ESLint finds unused variables inside files in isolation, but this will not be flagged:
|
|
7
7
|
|
|
@@ -79,16 +79,18 @@ Please read on if you think you have too many results: [too many false positives
|
|
|
79
79
|
knip [options]
|
|
80
80
|
|
|
81
81
|
Options:
|
|
82
|
-
-c/--config [file]
|
|
83
|
-
-t/--tsConfig [file]
|
|
84
|
-
--dir
|
|
85
|
-
--include
|
|
86
|
-
--exclude
|
|
87
|
-
--
|
|
88
|
-
--no-
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
82
|
+
-c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
|
|
83
|
+
-t/--tsConfig [file] TypeScript configuration path (default: ./tsconfig.json)
|
|
84
|
+
--dir Working directory (default: current working directory)
|
|
85
|
+
--include Report only listed issue group(s) (see below)
|
|
86
|
+
--exclude Exclude issue group(s) from report (see below)
|
|
87
|
+
--ignore Ignore files matching this glob pattern (can be set multiple times)
|
|
88
|
+
--no-gitignore Don't use .gitignore
|
|
89
|
+
--dev Include `devDependencies` in report(s)
|
|
90
|
+
--no-progress Don't show dynamic progress updates
|
|
91
|
+
--max-issues Maximum number of issues before non-zero exit code (default: 0)
|
|
92
|
+
--reporter Select reporter: symbols, compact (default: symbols)
|
|
93
|
+
--jsdoc Enable JSDoc parsing, with options: public
|
|
92
94
|
|
|
93
95
|
Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
|
|
94
96
|
|
|
@@ -97,6 +99,7 @@ Examples:
|
|
|
97
99
|
$ knip
|
|
98
100
|
$ knip --dir packages/client --include files
|
|
99
101
|
$ knip -c ./knip.js --reporter compact --jsdoc public
|
|
102
|
+
$ knip --ignore 'lib/**/*.ts' --ignore build
|
|
100
103
|
|
|
101
104
|
More info: https://github.com/webpro/knip
|
|
102
105
|
```
|
|
@@ -139,7 +142,7 @@ As always, make sure to backup files or use Git before deleting files or making
|
|
|
139
142
|
|
|
140
143
|
The default configuration for Knip is very strict and targets production code. For best results, it is recommended to
|
|
141
144
|
exclude files such as tests from the project files. Here's why: when including tests and other non-production files,
|
|
142
|
-
they may
|
|
145
|
+
they may import production files, which will prevent them from being reported as unused.
|
|
143
146
|
|
|
144
147
|
Excluding non-production files from the `projectFiles` allows Knip to understand what production code can be removed
|
|
145
148
|
(including dependent files!).
|
|
@@ -159,8 +162,7 @@ and add `dev: true` to a file named such as `knip.dev.json`:
|
|
|
159
162
|
}
|
|
160
163
|
```
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
reported.
|
|
165
|
+
Now use `-c knip.dev.json` to find unused files and exports for the combined set of files as configured in `entryFiles`.
|
|
164
166
|
|
|
165
167
|
An alternative way to store `dev` configuration is in this example `package.json`:
|
|
166
168
|
|
|
@@ -189,7 +191,7 @@ This way, the `--dev` flag will use the `dev` options (and also add `devDependen
|
|
|
189
191
|
|
|
190
192
|
#### Separate packages
|
|
191
193
|
|
|
192
|
-
In repos with multiple (published) packages, the `--
|
|
194
|
+
In repos with multiple (published) packages, the `--dir` option comes in handy. With similar package structures, the
|
|
193
195
|
packages can be configured using globs:
|
|
194
196
|
|
|
195
197
|
```json
|
|
@@ -206,8 +208,8 @@ Packages can also be explicitly configured per package directory.
|
|
|
206
208
|
To scan the packages separately, using the first match from the configuration file:
|
|
207
209
|
|
|
208
210
|
```
|
|
209
|
-
knip --
|
|
210
|
-
knip --
|
|
211
|
+
knip --dir packages/client
|
|
212
|
+
knip --dir packages/services
|
|
211
213
|
```
|
|
212
214
|
|
|
213
215
|
#### Connected projects
|
|
@@ -293,16 +295,48 @@ src/components/Registration.tsx: Registration, default
|
|
|
293
295
|
src/components/Products.tsx: ProductsList, default
|
|
294
296
|
```
|
|
295
297
|
|
|
296
|
-
## Why Yet Another unused file/export finder?
|
|
297
|
-
|
|
298
|
-
There are some
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
298
|
+
## Why Yet Another unused file/dependency/export finder?
|
|
299
|
+
|
|
300
|
+
There are already some great packages available. Getting good results when finding unused files, dependencies and
|
|
301
|
+
exports is not trivial. Repositories don't seem to get any smaller and with the rise of monorepos even more so. Tools
|
|
302
|
+
like this need to analyze potentially many and/or large files, which is memory and time-consuming. Although I normally
|
|
303
|
+
try to stick to the Unix philosophy, here I believe it's efficient to merge these issue reports into a single tool. When
|
|
304
|
+
building a dependency graph of the project, an abstract syntax tree for each file, and traversing all of this, why not
|
|
305
|
+
collect the various issues in one go?
|
|
306
|
+
|
|
307
|
+
## Comparison
|
|
308
|
+
|
|
309
|
+
This table is a work in progress, but here's a first impression. Based on their docs (please report any mistakes):
|
|
310
|
+
|
|
311
|
+
| Feature | **knip** | [depcheck][1] | [unimported][2] | [ts-unused-exports][3] | [ts-prune][4] | [find-unused-exports][5] |
|
|
312
|
+
| --------------------------- | :--------: | :-----------: | :-------------: | :--------------------: | :-----------: | :----------------------: |
|
|
313
|
+
| Unused files | ✅ | - | ✅ | - | - | - |
|
|
314
|
+
| Unused dependencies | ✅ | ✅ | ✅ | - | - | - |
|
|
315
|
+
| Unlisted dependencies | ✅ | ✅ | ✅ | - | - | - |
|
|
316
|
+
| Custom dependency resolvers | ❌ [#7][6] | ✅ | ❌ (1) | - | - | - |
|
|
317
|
+
| Unused exports | ✅ | - | - | ✅ | ✅ | ✅ |
|
|
318
|
+
| Duplicate exports | ✅ | - | - | ❌ | ❌ | ❌ |
|
|
319
|
+
| Search namespaces | ✅ | - | - | ✅ | ❌ | ❌ |
|
|
320
|
+
| Custom reporters | ✅ | - | - | - | - | - |
|
|
321
|
+
| Pure JavaScript/ESM | ✅ | ✅ | ✅ | - | - | ✅ |
|
|
322
|
+
| Configure entry files | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
323
|
+
| Support monorepo | 🟠 (2) | - | - | - | - | - |
|
|
324
|
+
| ESLint plugin available | - | - | - | ✅ | - | - |
|
|
325
|
+
|
|
326
|
+
✅ = Supported, ❌ = Not supported, - = Out of scope
|
|
327
|
+
|
|
328
|
+
1. unimported is strict and works based on production files and `dependencies`, so does not have custom dependency
|
|
329
|
+
resolvers which are usually only needed for `devDependencies`.
|
|
330
|
+
2. knip wants to [support monorepos](#monorepos) properly, the first steps in this direction are implemented.
|
|
331
|
+
|
|
332
|
+
## Knip?!
|
|
333
|
+
|
|
334
|
+
Knip is Dutch for a "cut". A Dutch expression is "to be ge**knip**t for something", which means to be perfectly suited
|
|
335
|
+
for the job. I'm motivated to make knip perfectly suited for the job of cutting projects to perfection! ✂️
|
|
336
|
+
|
|
337
|
+
[1]: https://github.com/depcheck/depcheck
|
|
338
|
+
[2]: https://github.com/smeijer/unimported
|
|
339
|
+
[3]: https://github.com/pzavolinsky/ts-unused-exports
|
|
340
|
+
[4]: https://github.com/nadeesha/ts-prune
|
|
341
|
+
[5]: https://github.com/jaydenseric/find-unused-exports
|
|
342
|
+
[6]: https://github.com/webpro/knip/issues/7
|
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const node_util_1 = require("node:util");
|
|
9
|
-
const
|
|
9
|
+
const _1 = require(".");
|
|
10
10
|
const help_1 = require("./help");
|
|
11
|
-
const config_1 = require("./util/config");
|
|
12
|
-
const path_1 = require("./util/path");
|
|
13
11
|
const reporters_1 = __importDefault(require("./reporters"));
|
|
14
|
-
const
|
|
15
|
-
const { values: { help, dir, config: configFilePath = 'knip.json', tsConfig: tsConfigFilePath, include = [], exclude = [], dev: isDev = false, 'no-progress': noProgress = false, reporter = 'symbols', jsdoc = [], 'max-issues': maxIssues = '0', }, } = (0, node_util_1.parseArgs)({
|
|
12
|
+
const { values: { help, dir, config: configFilePath = 'knip.json', tsConfig: tsConfigFilePath, include = [], exclude = [], ignore = [], 'no-gitignore': isNoGitIgnore = false, dev: isDev = false, 'no-progress': noProgress = false, reporter = 'symbols', 'max-issues': maxIssues = '0', jsdoc: jsDoc = [], }, } = (0, node_util_1.parseArgs)({
|
|
16
13
|
options: {
|
|
17
14
|
help: { type: 'boolean' },
|
|
18
15
|
config: { type: 'string', short: 'c' },
|
|
@@ -20,9 +17,11 @@ const { values: { help, dir, config: configFilePath = 'knip.json', tsConfig: tsC
|
|
|
20
17
|
dir: { type: 'string' },
|
|
21
18
|
include: { type: 'string', multiple: true },
|
|
22
19
|
exclude: { type: 'string', multiple: true },
|
|
20
|
+
ignore: { type: 'string', multiple: true },
|
|
21
|
+
'no-gitignore': { type: 'boolean' },
|
|
23
22
|
dev: { type: 'boolean' },
|
|
24
|
-
'max-issues': { type: 'string' },
|
|
25
23
|
'no-progress': { type: 'boolean' },
|
|
24
|
+
'max-issues': { type: 'string' },
|
|
26
25
|
reporter: { type: 'string' },
|
|
27
26
|
jsdoc: { type: 'string', multiple: true },
|
|
28
27
|
},
|
|
@@ -35,63 +34,37 @@ const cwd = process.cwd();
|
|
|
35
34
|
const workingDir = dir ? node_path_1.default.resolve(dir) : cwd;
|
|
36
35
|
const isShowProgress = noProgress === false ? process.stdout.isTTY && typeof process.stdout.cursorTo === 'function' : !noProgress;
|
|
37
36
|
const printReport = reporter in reporters_1.default ? reporters_1.default[reporter] : require(node_path_1.default.join(workingDir, reporter));
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
37
|
+
const run = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const { report, issues, counters } = await (0, _1.main)({
|
|
40
|
+
cwd,
|
|
41
|
+
workingDir,
|
|
42
|
+
configFilePath,
|
|
43
|
+
tsConfigFilePath,
|
|
44
|
+
include,
|
|
45
|
+
exclude,
|
|
46
|
+
ignore,
|
|
47
|
+
gitignore: !isNoGitIgnore,
|
|
48
|
+
isDev,
|
|
49
|
+
isShowProgress,
|
|
50
|
+
jsDoc,
|
|
51
|
+
});
|
|
52
|
+
printReport({ report, issues, workingDir, isDev });
|
|
53
|
+
const reportGroup = report.files ? 'files' : Object.keys(report).find(key => report[key]);
|
|
54
|
+
const counterGroup = reportGroup === 'unlisted' ? 'unresolved' : reportGroup;
|
|
55
|
+
if (counterGroup) {
|
|
56
|
+
const count = counters[counterGroup];
|
|
57
|
+
if (count > Number(maxIssues))
|
|
58
|
+
process.exit(count);
|
|
59
|
+
}
|
|
61
60
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
? Object.keys(tsConfig.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
|
|
66
|
-
: [];
|
|
67
|
-
if (tsConfig.error) {
|
|
68
|
-
console.error(`An error occured when reading ${node_path_1.default.relative(cwd, tsConfigPath)}.\n`);
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
console.error(error.message + '\n');
|
|
69
64
|
(0, help_1.printHelp)();
|
|
70
65
|
process.exit(1);
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
const config = {
|
|
74
|
-
workingDir,
|
|
75
|
-
report,
|
|
76
|
-
dependencies: Object.keys(manifest.dependencies ?? {}),
|
|
77
|
-
devDependencies: Object.keys(manifest.devDependencies ?? {}),
|
|
78
|
-
isDev: typeof resolvedConfig.dev === 'boolean' ? resolvedConfig.dev : isDev,
|
|
79
|
-
tsConfigFilePath,
|
|
80
|
-
tsConfigPaths,
|
|
81
|
-
isShowProgress,
|
|
82
|
-
jsDocOptions: {
|
|
83
|
-
isReadPublicTag: jsdoc.includes('public'),
|
|
84
|
-
},
|
|
85
|
-
...resolvedConfig,
|
|
86
|
-
};
|
|
87
|
-
const { issues, counters } = await (0, _1.run)(config);
|
|
88
|
-
printReport({ issues, workingDir, config });
|
|
89
|
-
const reportGroup = report.files ? 'files' : Object.keys(report).find(key => report[key]);
|
|
90
|
-
const counterGroup = reportGroup === 'unlisted' ? 'unresolved' : reportGroup;
|
|
91
|
-
if (counterGroup) {
|
|
92
|
-
const count = counters[counterGroup];
|
|
93
|
-
if (count > Number(maxIssues))
|
|
94
|
-
process.exit(count);
|
|
67
|
+
throw error;
|
|
95
68
|
}
|
|
96
69
|
};
|
|
97
|
-
|
|
70
|
+
run();
|
package/dist/help.js
CHANGED
|
@@ -5,16 +5,18 @@ const printHelp = () => {
|
|
|
5
5
|
console.log(`knip [options]
|
|
6
6
|
|
|
7
7
|
Options:
|
|
8
|
-
-c/--config [file]
|
|
9
|
-
-t/--tsConfig [file]
|
|
10
|
-
--dir
|
|
11
|
-
--include
|
|
12
|
-
--exclude
|
|
13
|
-
--
|
|
14
|
-
--no-
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
8
|
+
-c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
|
|
9
|
+
-t/--tsConfig [file] TypeScript configuration path (default: ./tsconfig.json)
|
|
10
|
+
--dir Working directory (default: current working directory)
|
|
11
|
+
--include Report only listed issue group(s) (see below)
|
|
12
|
+
--exclude Exclude issue group(s) from report (see below)
|
|
13
|
+
--ignore Ignore files matching this glob pattern (can be set multiple times)
|
|
14
|
+
--no-gitignore Don't use .gitignore
|
|
15
|
+
--dev Include \`devDependencies\` in report(s)
|
|
16
|
+
--no-progress Don't show dynamic progress updates
|
|
17
|
+
--max-issues Maximum number of issues before non-zero exit code (default: 0)
|
|
18
|
+
--reporter Select reporter: symbols, compact (default: symbols)
|
|
19
|
+
--jsdoc Enable JSDoc parsing, with options: public
|
|
18
20
|
|
|
19
21
|
Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
|
|
20
22
|
|
|
@@ -23,6 +25,7 @@ Examples:
|
|
|
23
25
|
$ knip
|
|
24
26
|
$ knip --dir packages/client --include files
|
|
25
27
|
$ knip -c ./knip.js --reporter compact --jsdoc public
|
|
28
|
+
$ knip --ignore 'lib/**/*.ts' --ignore build
|
|
26
29
|
|
|
27
30
|
More info: https://github.com/webpro/knip`);
|
|
28
31
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
3
|
-
|
|
1
|
+
import type { UnresolvedConfiguration } from './types';
|
|
2
|
+
export declare const main: (options: UnresolvedConfiguration) => Promise<{
|
|
3
|
+
report: import("./types").Report;
|
|
4
|
+
issues: import("./types").Issues;
|
|
4
5
|
counters: {
|
|
5
6
|
files: number;
|
|
6
7
|
dependencies: number;
|
package/dist/index.js
CHANGED
|
@@ -3,196 +3,81 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.main = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
const config_1 = require("./util/config");
|
|
10
|
+
const fs_1 = require("./util/fs");
|
|
11
|
+
const path_1 = require("./util/path");
|
|
10
12
|
const project_1 = require("./util/project");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
13
|
+
const runner_1 = require("./runner");
|
|
14
|
+
const main = async (options) => {
|
|
15
|
+
const { cwd, workingDir, configFilePath = 'knip.json', tsConfigFilePath, include, exclude, ignore, gitignore, isDev, isShowProgress, jsDoc, } = options;
|
|
16
|
+
const localConfigurationPath = configFilePath && (await (0, fs_1.findFile)(workingDir, configFilePath));
|
|
17
|
+
const manifestPath = await (0, fs_1.findFile)(workingDir, 'package.json');
|
|
18
|
+
const localConfiguration = localConfigurationPath && require(localConfigurationPath);
|
|
19
|
+
const manifest = manifestPath && require(manifestPath);
|
|
20
|
+
if (!localConfigurationPath && !manifest.knip) {
|
|
21
|
+
const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
|
|
22
|
+
throw new Error(`Unable to find ${configFilePath} or package.json#knip in ${location}`);
|
|
23
|
+
}
|
|
24
|
+
const dir = node_path_1.default.relative(cwd, workingDir);
|
|
25
|
+
const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfiguration, { workingDir: dir, isDev });
|
|
26
|
+
if (!resolvedConfig) {
|
|
27
|
+
throw new Error('Unable to find `entryFiles` and/or `projectFiles` in configuration.');
|
|
28
|
+
}
|
|
29
|
+
const report = (0, config_1.resolveIncludedIssueGroups)(include, exclude, resolvedConfig);
|
|
30
|
+
let tsConfigPaths = [];
|
|
31
|
+
const tsConfigPath = await (0, fs_1.findFile)(workingDir, tsConfigFilePath ?? 'tsconfig.json');
|
|
32
|
+
if (tsConfigFilePath && !tsConfigPath) {
|
|
33
|
+
throw new Error(`Unable to find ${tsConfigFilePath}`);
|
|
34
|
+
}
|
|
35
|
+
if (tsConfigPath) {
|
|
36
|
+
const tsConfig = typescript_1.default.readConfigFile(tsConfigPath, typescript_1.default.sys.readFile);
|
|
37
|
+
tsConfigPaths = tsConfig.config.compilerOptions?.paths
|
|
38
|
+
? Object.keys(tsConfig.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
|
|
39
|
+
: [];
|
|
40
|
+
if (tsConfig.error) {
|
|
41
|
+
throw new Error(`An error occured when reading ${node_path_1.default.relative(cwd, tsConfigPath)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const projectOptions = tsConfigFilePath ? { tsConfigFilePath } : { compilerOptions: { allowJs: true } };
|
|
45
|
+
const entryPaths = await (0, path_1.resolvePaths)({
|
|
46
|
+
cwd,
|
|
47
|
+
workingDir,
|
|
48
|
+
patterns: resolvedConfig.entryFiles,
|
|
49
|
+
ignore,
|
|
50
|
+
gitignore,
|
|
51
|
+
});
|
|
52
|
+
const production = (0, project_1.createProject)({ projectOptions, paths: entryPaths });
|
|
19
53
|
const entryFiles = production.getSourceFiles();
|
|
20
54
|
production.resolveSourceFileDependencies();
|
|
21
55
|
const productionFiles = production.getSourceFiles();
|
|
22
|
-
const
|
|
56
|
+
const projectPaths = await (0, path_1.resolvePaths)({
|
|
57
|
+
cwd,
|
|
58
|
+
workingDir,
|
|
59
|
+
patterns: resolvedConfig.projectFiles,
|
|
60
|
+
ignore,
|
|
61
|
+
gitignore,
|
|
62
|
+
});
|
|
63
|
+
const project = (0, project_1.createProject)({ projectOptions, paths: projectPaths });
|
|
23
64
|
const projectFiles = project.getSourceFiles();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
files: issues.files.size,
|
|
39
|
-
dependencies: issues.dependencies.size,
|
|
40
|
-
devDependencies: issues.dependencies.size,
|
|
41
|
-
unresolved: 0,
|
|
42
|
-
exports: 0,
|
|
43
|
-
types: 0,
|
|
44
|
-
nsExports: 0,
|
|
45
|
-
nsTypes: 0,
|
|
46
|
-
duplicates: 0,
|
|
47
|
-
processed: issues.files.size,
|
|
48
|
-
};
|
|
49
|
-
const updateProcessingOutput = (item) => {
|
|
50
|
-
if (!isShowProgress)
|
|
51
|
-
return;
|
|
52
|
-
const counter = unreferencedProductionFiles.length + counters.processed;
|
|
53
|
-
const total = unreferencedProductionFiles.length + usedNonEntryFiles.length;
|
|
54
|
-
const percentage = Math.floor((counter / total) * 100);
|
|
55
|
-
const messages = [(0, log_1.getLine)(`${percentage}%`, `of files processed (${counter} of ${total})`)];
|
|
56
|
-
report.files && messages.push((0, log_1.getLine)(unreferencedProductionFiles.length, 'unused files'));
|
|
57
|
-
report.unlisted && messages.push((0, log_1.getLine)(counters.unresolved, 'unlisted dependencies'));
|
|
58
|
-
report.exports && messages.push((0, log_1.getLine)(counters.exports, 'unused exports'));
|
|
59
|
-
report.nsExports && messages.push((0, log_1.getLine)(counters.nsExports, 'unused exports in namespace'));
|
|
60
|
-
report.types && messages.push((0, log_1.getLine)(counters.types, 'unused types'));
|
|
61
|
-
report.nsTypes && messages.push((0, log_1.getLine)(counters.nsTypes, 'unused types in namespace'));
|
|
62
|
-
report.duplicates && messages.push((0, log_1.getLine)(counters.duplicates, 'duplicate exports'));
|
|
63
|
-
if (counter < total) {
|
|
64
|
-
messages.push('');
|
|
65
|
-
messages.push(`Processing: ${node_path_1.default.relative(workingDir, item.filePath)}`);
|
|
66
|
-
}
|
|
67
|
-
lineRewriter.update(messages);
|
|
65
|
+
const config = {
|
|
66
|
+
workingDir,
|
|
67
|
+
report,
|
|
68
|
+
entryFiles,
|
|
69
|
+
productionFiles,
|
|
70
|
+
projectFiles,
|
|
71
|
+
dependencies: Object.keys(manifest.dependencies ?? {}),
|
|
72
|
+
devDependencies: Object.keys(manifest.devDependencies ?? {}),
|
|
73
|
+
isDev: typeof resolvedConfig.dev === 'boolean' ? resolvedConfig.dev : isDev,
|
|
74
|
+
tsConfigPaths,
|
|
75
|
+
isShowProgress,
|
|
76
|
+
jsDocOptions: {
|
|
77
|
+
isReadPublicTag: jsDoc.includes('public'),
|
|
78
|
+
},
|
|
68
79
|
};
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
issues[issueType][key][symbol] = issue;
|
|
74
|
-
counters[issueType]++;
|
|
75
|
-
updateProcessingOutput(issue);
|
|
76
|
-
};
|
|
77
|
-
const addProjectIssue = (issueType, issue) => {
|
|
78
|
-
if (!issues[issueType].has(issue.symbol)) {
|
|
79
|
-
issues[issueType].add(issue.symbol);
|
|
80
|
-
counters[issueType]++;
|
|
81
|
-
}
|
|
82
|
-
updateProcessingOutput(issue);
|
|
83
|
-
};
|
|
84
|
-
if (report.dependencies || report.unlisted) {
|
|
85
|
-
usedEntryFiles.forEach(sourceFile => {
|
|
86
|
-
const unresolvedDependencies = getUnresolvedDependencies(sourceFile);
|
|
87
|
-
unresolvedDependencies.forEach(issue => addSymbolIssue('unresolved', issue));
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
if (report.dependencies ||
|
|
91
|
-
report.unlisted ||
|
|
92
|
-
report.exports ||
|
|
93
|
-
report.types ||
|
|
94
|
-
report.nsExports ||
|
|
95
|
-
report.nsTypes ||
|
|
96
|
-
report.duplicates) {
|
|
97
|
-
usedNonEntryFiles.forEach(sourceFile => {
|
|
98
|
-
const filePath = sourceFile.getFilePath();
|
|
99
|
-
if (report.dependencies || report.unlisted) {
|
|
100
|
-
const unresolvedDependencies = getUnresolvedDependencies(sourceFile);
|
|
101
|
-
unresolvedDependencies.forEach(issue => addSymbolIssue('unresolved', issue));
|
|
102
|
-
}
|
|
103
|
-
const exportDeclarations = sourceFile.getExportedDeclarations();
|
|
104
|
-
if (report.duplicates) {
|
|
105
|
-
const duplicateExports = (0, ts_morph_helpers_1.findDuplicateExportedNames)(sourceFile);
|
|
106
|
-
duplicateExports.forEach(symbols => {
|
|
107
|
-
const symbol = symbols.join('|');
|
|
108
|
-
addSymbolIssue('duplicates', { filePath, symbol, symbols });
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
if (report.exports || report.types || report.nsExports || report.nsTypes) {
|
|
112
|
-
const uniqueExportedSymbols = new Set([...exportDeclarations.values()].flat());
|
|
113
|
-
if (uniqueExportedSymbols.size === 1)
|
|
114
|
-
return;
|
|
115
|
-
exportDeclarations.forEach(declarations => {
|
|
116
|
-
declarations.forEach(declaration => {
|
|
117
|
-
const type = (0, type_1.getType)(declaration);
|
|
118
|
-
if (!report.nsExports && !report.nsTypes) {
|
|
119
|
-
if (!report.types && type)
|
|
120
|
-
return;
|
|
121
|
-
if (!report.exports && !type)
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (jsDocOptions.isReadPublicTag && ts_morph_1.ts.getJSDocPublicTag(declaration.compilerNode))
|
|
125
|
-
return;
|
|
126
|
-
let identifier;
|
|
127
|
-
if (declaration.isKind(ts_morph_1.ts.SyntaxKind.Identifier)) {
|
|
128
|
-
identifier = declaration;
|
|
129
|
-
}
|
|
130
|
-
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.ArrowFunction)) {
|
|
131
|
-
}
|
|
132
|
-
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.FunctionDeclaration) ||
|
|
133
|
-
declaration.isKind(ts_morph_1.ts.SyntaxKind.ClassDeclaration) ||
|
|
134
|
-
declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration) ||
|
|
135
|
-
declaration.isKind(ts_morph_1.ts.SyntaxKind.InterfaceDeclaration) ||
|
|
136
|
-
declaration.isKind(ts_morph_1.ts.SyntaxKind.EnumDeclaration)) {
|
|
137
|
-
identifier = declaration.getFirstChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
138
|
-
}
|
|
139
|
-
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.PropertyAccessExpression)) {
|
|
140
|
-
identifier = declaration.getLastChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
identifier = declaration.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
144
|
-
}
|
|
145
|
-
if (identifier) {
|
|
146
|
-
const identifierText = identifier.getText();
|
|
147
|
-
if (report.exports && issues.exports[filePath]?.[identifierText])
|
|
148
|
-
return;
|
|
149
|
-
if (report.types && issues.types[filePath]?.[identifierText])
|
|
150
|
-
return;
|
|
151
|
-
if (report.nsExports && issues.nsExports[filePath]?.[identifierText])
|
|
152
|
-
return;
|
|
153
|
-
if (report.nsTypes && issues.nsTypes[filePath]?.[identifierText])
|
|
154
|
-
return;
|
|
155
|
-
const refs = identifier.findReferences();
|
|
156
|
-
if (refs.length === 0) {
|
|
157
|
-
addSymbolIssue('exports', { filePath, symbol: identifierText });
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
const refFiles = new Set(refs.map(r => r.compilerObject.references.map(r => r.fileName)).flat());
|
|
161
|
-
const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === filePath;
|
|
162
|
-
if (!isReferencedOnlyBySelf)
|
|
163
|
-
return;
|
|
164
|
-
if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
|
|
165
|
-
if (type) {
|
|
166
|
-
addSymbolIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
addSymbolIssue('nsExports', { filePath, symbol: identifierText });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (type) {
|
|
173
|
-
addSymbolIssue('types', { filePath, symbol: identifierText, symbolType: type });
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
addSymbolIssue('exports', { filePath, symbol: identifierText });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
counters.processed++;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
if (report.dependencies) {
|
|
187
|
-
const unusedDependencies = getUnusedDependencies();
|
|
188
|
-
unusedDependencies.forEach(symbol => addProjectIssue('dependencies', { filePath: '', symbol }));
|
|
189
|
-
if (isDev) {
|
|
190
|
-
const unusedDevDependencies = getUnusedDevDependencies();
|
|
191
|
-
unusedDevDependencies.forEach(symbol => addProjectIssue('devDependencies', { filePath: '', symbol }));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (isShowProgress)
|
|
195
|
-
lineRewriter.resetLines();
|
|
196
|
-
return { issues, counters };
|
|
197
|
-
}
|
|
198
|
-
exports.run = run;
|
|
80
|
+
const { issues, counters } = await (0, runner_1.findIssues)(config);
|
|
81
|
+
return { report, issues, counters };
|
|
82
|
+
};
|
|
83
|
+
exports.main = main;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { Issues,
|
|
2
|
-
declare const _default: ({
|
|
1
|
+
import type { Issues, Report } from '../types';
|
|
2
|
+
declare const _default: ({ report, issues, workingDir, isDev, }: {
|
|
3
|
+
report: Report;
|
|
3
4
|
issues: Issues;
|
|
4
|
-
config: Configuration;
|
|
5
5
|
workingDir: string;
|
|
6
|
+
isDev: boolean;
|
|
6
7
|
}) => void;
|
|
7
8
|
export default _default;
|
|
@@ -26,8 +26,7 @@ const logIssueGroupResults = (issues, workingDir, title) => {
|
|
|
26
26
|
console.log('Not found');
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
exports.default = ({
|
|
30
|
-
const { report, isDev } = config;
|
|
29
|
+
exports.default = ({ report, issues, workingDir, isDev, }) => {
|
|
31
30
|
const reportMultipleGroups = Object.values(report).filter(Boolean).length > 1;
|
|
32
31
|
if (report.files) {
|
|
33
32
|
const unreferencedFiles = Array.from(issues.files);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
-
symbols: ({
|
|
2
|
+
symbols: ({ report, issues, workingDir, isDev, }: {
|
|
3
|
+
report: import("../types").Report;
|
|
3
4
|
issues: import("../types").Issues;
|
|
4
|
-
config: import("../types").Configuration;
|
|
5
5
|
workingDir: string;
|
|
6
|
+
isDev: boolean;
|
|
6
7
|
}) => void;
|
|
7
|
-
compact: ({
|
|
8
|
+
compact: ({ report, issues, workingDir, isDev, }: {
|
|
9
|
+
report: import("../types").Report;
|
|
8
10
|
issues: import("../types").Issues;
|
|
9
|
-
config: import("../types").Configuration;
|
|
10
11
|
workingDir: string;
|
|
12
|
+
isDev: boolean;
|
|
11
13
|
}) => void;
|
|
12
14
|
};
|
|
13
15
|
export default _default;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { Issues,
|
|
2
|
-
declare const _default: ({
|
|
1
|
+
import type { Issues, Report } from '../types';
|
|
2
|
+
declare const _default: ({ report, issues, workingDir, isDev, }: {
|
|
3
|
+
report: Report;
|
|
3
4
|
issues: Issues;
|
|
4
|
-
config: Configuration;
|
|
5
5
|
workingDir: string;
|
|
6
|
+
isDev: boolean;
|
|
6
7
|
}) => void;
|
|
7
8
|
export default _default;
|
|
@@ -28,8 +28,7 @@ const logIssueGroupResults = (issues, workingDir, title) => {
|
|
|
28
28
|
console.log('Not found');
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
-
exports.default = ({
|
|
32
|
-
const { report, isDev } = config;
|
|
31
|
+
exports.default = ({ report, issues, workingDir, isDev, }) => {
|
|
33
32
|
const reportMultipleGroups = Object.values(report).filter(Boolean).length > 1;
|
|
34
33
|
if (report.files) {
|
|
35
34
|
const unreferencedFiles = Array.from(issues.files);
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Configuration, Issues } from './types';
|
|
2
|
+
export declare function findIssues(configuration: Configuration): Promise<{
|
|
3
|
+
issues: Issues;
|
|
4
|
+
counters: {
|
|
5
|
+
files: number;
|
|
6
|
+
dependencies: number;
|
|
7
|
+
devDependencies: number;
|
|
8
|
+
unresolved: number;
|
|
9
|
+
exports: number;
|
|
10
|
+
types: number;
|
|
11
|
+
nsExports: number;
|
|
12
|
+
nsTypes: number;
|
|
13
|
+
duplicates: number;
|
|
14
|
+
processed: number;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
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.findIssues = 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 project_1 = require("./util/project");
|
|
11
|
+
const type_1 = require("./util/type");
|
|
12
|
+
const dependencies_1 = require("./util/dependencies");
|
|
13
|
+
const log_1 = require("./log");
|
|
14
|
+
const lineRewriter = new log_1.LineRewriter();
|
|
15
|
+
async function findIssues(configuration) {
|
|
16
|
+
const { workingDir, isShowProgress, report, isDev, jsDocOptions } = configuration;
|
|
17
|
+
const { entryFiles, productionFiles, projectFiles } = configuration;
|
|
18
|
+
const { getUnresolvedDependencies, getUnusedDependencies, getUnusedDevDependencies } = (0, dependencies_1.getDependencyAnalyzer)(configuration);
|
|
19
|
+
const [usedProductionFiles, unreferencedProductionFiles] = (0, project_1.partitionSourceFiles)(projectFiles, productionFiles);
|
|
20
|
+
const [usedEntryFiles, usedNonEntryFiles] = (0, project_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
|
|
21
|
+
const issues = {
|
|
22
|
+
files: new Set(unreferencedProductionFiles.map(file => file.getFilePath())),
|
|
23
|
+
dependencies: new Set(),
|
|
24
|
+
devDependencies: new Set(),
|
|
25
|
+
unresolved: {},
|
|
26
|
+
exports: {},
|
|
27
|
+
types: {},
|
|
28
|
+
nsExports: {},
|
|
29
|
+
nsTypes: {},
|
|
30
|
+
duplicates: {},
|
|
31
|
+
};
|
|
32
|
+
const counters = {
|
|
33
|
+
files: issues.files.size,
|
|
34
|
+
dependencies: issues.dependencies.size,
|
|
35
|
+
devDependencies: issues.dependencies.size,
|
|
36
|
+
unresolved: 0,
|
|
37
|
+
exports: 0,
|
|
38
|
+
types: 0,
|
|
39
|
+
nsExports: 0,
|
|
40
|
+
nsTypes: 0,
|
|
41
|
+
duplicates: 0,
|
|
42
|
+
processed: issues.files.size,
|
|
43
|
+
};
|
|
44
|
+
const updateProcessingOutput = (item) => {
|
|
45
|
+
if (!isShowProgress)
|
|
46
|
+
return;
|
|
47
|
+
const counter = unreferencedProductionFiles.length + counters.processed;
|
|
48
|
+
const total = unreferencedProductionFiles.length + usedNonEntryFiles.length;
|
|
49
|
+
const percentage = Math.floor((counter / total) * 100);
|
|
50
|
+
const messages = [(0, log_1.getLine)(`${percentage}%`, `of files processed (${counter} of ${total})`)];
|
|
51
|
+
report.files && messages.push((0, log_1.getLine)(unreferencedProductionFiles.length, 'unused files'));
|
|
52
|
+
report.unlisted && messages.push((0, log_1.getLine)(counters.unresolved, 'unlisted dependencies'));
|
|
53
|
+
report.exports && messages.push((0, log_1.getLine)(counters.exports, 'unused exports'));
|
|
54
|
+
report.nsExports && messages.push((0, log_1.getLine)(counters.nsExports, 'unused exports in namespace'));
|
|
55
|
+
report.types && messages.push((0, log_1.getLine)(counters.types, 'unused types'));
|
|
56
|
+
report.nsTypes && messages.push((0, log_1.getLine)(counters.nsTypes, 'unused types in namespace'));
|
|
57
|
+
report.duplicates && messages.push((0, log_1.getLine)(counters.duplicates, 'duplicate exports'));
|
|
58
|
+
if (counter < total) {
|
|
59
|
+
messages.push('');
|
|
60
|
+
messages.push(`Processing: ${node_path_1.default.relative(workingDir, item.filePath)}`);
|
|
61
|
+
}
|
|
62
|
+
lineRewriter.update(messages);
|
|
63
|
+
};
|
|
64
|
+
const addSymbolIssue = (issueType, issue) => {
|
|
65
|
+
const { filePath, symbol } = issue;
|
|
66
|
+
const key = node_path_1.default.relative(workingDir, filePath);
|
|
67
|
+
issues[issueType][key] = issues[issueType][key] ?? {};
|
|
68
|
+
issues[issueType][key][symbol] = issue;
|
|
69
|
+
counters[issueType]++;
|
|
70
|
+
updateProcessingOutput(issue);
|
|
71
|
+
};
|
|
72
|
+
const addProjectIssue = (issueType, issue) => {
|
|
73
|
+
if (!issues[issueType].has(issue.symbol)) {
|
|
74
|
+
issues[issueType].add(issue.symbol);
|
|
75
|
+
counters[issueType]++;
|
|
76
|
+
}
|
|
77
|
+
updateProcessingOutput(issue);
|
|
78
|
+
};
|
|
79
|
+
if (report.dependencies || report.unlisted) {
|
|
80
|
+
usedEntryFiles.forEach(sourceFile => {
|
|
81
|
+
const unresolvedDependencies = getUnresolvedDependencies(sourceFile);
|
|
82
|
+
unresolvedDependencies.forEach(issue => addSymbolIssue('unresolved', issue));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (report.dependencies ||
|
|
86
|
+
report.unlisted ||
|
|
87
|
+
report.exports ||
|
|
88
|
+
report.types ||
|
|
89
|
+
report.nsExports ||
|
|
90
|
+
report.nsTypes ||
|
|
91
|
+
report.duplicates) {
|
|
92
|
+
usedNonEntryFiles.forEach(sourceFile => {
|
|
93
|
+
const filePath = sourceFile.getFilePath();
|
|
94
|
+
if (report.dependencies || report.unlisted) {
|
|
95
|
+
const unresolvedDependencies = getUnresolvedDependencies(sourceFile);
|
|
96
|
+
unresolvedDependencies.forEach(issue => addSymbolIssue('unresolved', issue));
|
|
97
|
+
}
|
|
98
|
+
const exportDeclarations = sourceFile.getExportedDeclarations();
|
|
99
|
+
if (report.duplicates) {
|
|
100
|
+
const duplicateExports = (0, ts_morph_helpers_1.findDuplicateExportedNames)(sourceFile);
|
|
101
|
+
duplicateExports.forEach(symbols => {
|
|
102
|
+
const symbol = symbols.join('|');
|
|
103
|
+
addSymbolIssue('duplicates', { filePath, symbol, symbols });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (report.exports || report.types || report.nsExports || report.nsTypes) {
|
|
107
|
+
const uniqueExportedSymbols = new Set([...exportDeclarations.values()].flat());
|
|
108
|
+
if (uniqueExportedSymbols.size === 1)
|
|
109
|
+
return;
|
|
110
|
+
exportDeclarations.forEach(declarations => {
|
|
111
|
+
declarations.forEach(declaration => {
|
|
112
|
+
const type = (0, type_1.getType)(declaration);
|
|
113
|
+
if (!report.nsExports && !report.nsTypes) {
|
|
114
|
+
if (!report.types && type)
|
|
115
|
+
return;
|
|
116
|
+
if (!report.exports && !type)
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (jsDocOptions.isReadPublicTag && ts_morph_1.ts.getJSDocPublicTag(declaration.compilerNode))
|
|
120
|
+
return;
|
|
121
|
+
let identifier;
|
|
122
|
+
if (declaration.isKind(ts_morph_1.ts.SyntaxKind.Identifier)) {
|
|
123
|
+
identifier = declaration;
|
|
124
|
+
}
|
|
125
|
+
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.ArrowFunction)) {
|
|
126
|
+
}
|
|
127
|
+
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.FunctionDeclaration) ||
|
|
128
|
+
declaration.isKind(ts_morph_1.ts.SyntaxKind.ClassDeclaration) ||
|
|
129
|
+
declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration) ||
|
|
130
|
+
declaration.isKind(ts_morph_1.ts.SyntaxKind.InterfaceDeclaration) ||
|
|
131
|
+
declaration.isKind(ts_morph_1.ts.SyntaxKind.EnumDeclaration)) {
|
|
132
|
+
identifier = declaration.getFirstChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
133
|
+
}
|
|
134
|
+
else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.PropertyAccessExpression)) {
|
|
135
|
+
identifier = declaration.getLastChildByKindOrThrow(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
identifier = declaration.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.Identifier);
|
|
139
|
+
}
|
|
140
|
+
if (identifier) {
|
|
141
|
+
const identifierText = identifier.getText();
|
|
142
|
+
if (report.exports && issues.exports[filePath]?.[identifierText])
|
|
143
|
+
return;
|
|
144
|
+
if (report.types && issues.types[filePath]?.[identifierText])
|
|
145
|
+
return;
|
|
146
|
+
if (report.nsExports && issues.nsExports[filePath]?.[identifierText])
|
|
147
|
+
return;
|
|
148
|
+
if (report.nsTypes && issues.nsTypes[filePath]?.[identifierText])
|
|
149
|
+
return;
|
|
150
|
+
const refs = identifier.findReferences();
|
|
151
|
+
if (refs.length === 0) {
|
|
152
|
+
addSymbolIssue('exports', { filePath, symbol: identifierText });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const refFiles = new Set(refs.map(r => r.compilerObject.references.map(r => r.fileName)).flat());
|
|
156
|
+
const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === filePath;
|
|
157
|
+
if (!isReferencedOnlyBySelf)
|
|
158
|
+
return;
|
|
159
|
+
if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
|
|
160
|
+
if (type) {
|
|
161
|
+
addSymbolIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
addSymbolIssue('nsExports', { filePath, symbol: identifierText });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (type) {
|
|
168
|
+
addSymbolIssue('types', { filePath, symbol: identifierText, symbolType: type });
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
addSymbolIssue('exports', { filePath, symbol: identifierText });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
counters.processed++;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (report.dependencies) {
|
|
182
|
+
const unusedDependencies = getUnusedDependencies();
|
|
183
|
+
unusedDependencies.forEach(symbol => addProjectIssue('dependencies', { filePath: '', symbol }));
|
|
184
|
+
if (isDev) {
|
|
185
|
+
const unusedDevDependencies = getUnusedDevDependencies();
|
|
186
|
+
unusedDevDependencies.forEach(symbol => addProjectIssue('devDependencies', { filePath: '', symbol }));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (isShowProgress)
|
|
190
|
+
lineRewriter.resetLines();
|
|
191
|
+
return { issues, counters };
|
|
192
|
+
}
|
|
193
|
+
exports.findIssues = findIssues;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SourceFile } from 'ts-morph';
|
|
1
2
|
declare type SymbolType = 'type' | 'interface' | 'enum';
|
|
2
3
|
declare type UnusedFileIssues = Set<string>;
|
|
3
4
|
declare type UnusedExportIssues = Record<string, Record<string, Issue>>;
|
|
@@ -20,31 +21,45 @@ export declare type Issues = {
|
|
|
20
21
|
nsTypes: UnusedExportIssues;
|
|
21
22
|
duplicates: UnusedExportIssues;
|
|
22
23
|
};
|
|
23
|
-
|
|
24
|
+
declare type IssueType = keyof Issues;
|
|
24
25
|
export declare type ProjectIssueType = Extract<IssueType, 'files' | 'dependencies' | 'devDependencies'>;
|
|
25
26
|
export declare type SymbolIssueType = Exclude<IssueType, ProjectIssueType>;
|
|
26
27
|
export declare type IssueGroup = 'files' | 'dependencies' | 'unlisted' | 'exports' | 'nsExports' | 'types' | 'nsTypes' | 'duplicates';
|
|
27
|
-
|
|
28
|
+
declare type BaseLocalConfiguration = {
|
|
28
29
|
entryFiles: string[];
|
|
29
30
|
projectFiles: string[];
|
|
30
31
|
};
|
|
31
32
|
export declare type LocalConfiguration = BaseLocalConfiguration & {
|
|
32
33
|
dev?: boolean | BaseLocalConfiguration;
|
|
33
|
-
entryFiles: string[];
|
|
34
|
-
projectFiles: string[];
|
|
35
34
|
include?: string[];
|
|
36
35
|
exclude?: string[];
|
|
37
36
|
};
|
|
38
37
|
export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
|
|
39
|
-
export declare type
|
|
38
|
+
export declare type UnresolvedConfiguration = {
|
|
39
|
+
cwd: string;
|
|
40
40
|
workingDir: string;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
configFilePath?: string;
|
|
42
|
+
tsConfigFilePath?: string;
|
|
43
|
+
include: string[];
|
|
44
|
+
exclude: string[];
|
|
45
|
+
ignore: string[];
|
|
46
|
+
gitignore: boolean;
|
|
47
|
+
isDev: boolean;
|
|
48
|
+
isShowProgress: boolean;
|
|
49
|
+
jsDoc: string[];
|
|
50
|
+
};
|
|
51
|
+
export declare type Report = {
|
|
52
|
+
[key in IssueGroup]: boolean;
|
|
53
|
+
};
|
|
54
|
+
export declare type Configuration = {
|
|
55
|
+
workingDir: string;
|
|
56
|
+
report: Report;
|
|
57
|
+
projectFiles: SourceFile[];
|
|
58
|
+
productionFiles: SourceFile[];
|
|
59
|
+
entryFiles: SourceFile[];
|
|
44
60
|
dependencies: string[];
|
|
45
61
|
devDependencies: string[];
|
|
46
62
|
isDev: boolean;
|
|
47
|
-
tsConfigFilePath: undefined | string;
|
|
48
63
|
tsConfigPaths: string[];
|
|
49
64
|
isShowProgress: boolean;
|
|
50
65
|
jsDocOptions: {
|
package/dist/util/config.d.ts
CHANGED
|
@@ -3,13 +3,4 @@ export declare const resolveConfig: (importedConfiguration: ImportedConfiguratio
|
|
|
3
3
|
workingDir?: string;
|
|
4
4
|
isDev?: boolean;
|
|
5
5
|
}) => LocalConfiguration | undefined;
|
|
6
|
-
export declare const resolveIncludedIssueGroups: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) =>
|
|
7
|
-
files: boolean;
|
|
8
|
-
dependencies: boolean;
|
|
9
|
-
exports: boolean;
|
|
10
|
-
types: boolean;
|
|
11
|
-
nsExports: boolean;
|
|
12
|
-
nsTypes: boolean;
|
|
13
|
-
duplicates: boolean;
|
|
14
|
-
unlisted: boolean;
|
|
15
|
-
};
|
|
6
|
+
export declare const resolveIncludedIssueGroups: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => import("../types").Report;
|
package/dist/util/config.js
CHANGED
|
@@ -19,8 +19,7 @@ const resolveConfig = (importedConfiguration, options) => {
|
|
|
19
19
|
resolvedConfig = resolvedConfig.dev;
|
|
20
20
|
}
|
|
21
21
|
if (!resolvedConfig.entryFiles || !resolvedConfig.projectFiles) {
|
|
22
|
-
console.
|
|
23
|
-
console.info(`Add these properties at root level, or use --cwd and match one of: ${configKeys.join(', ')}\n`);
|
|
22
|
+
console.info(`Add these properties at root level, or use --dir and match one of: ${configKeys.join(', ')}\n`);
|
|
24
23
|
return;
|
|
25
24
|
}
|
|
26
25
|
return resolvedConfig;
|
package/dist/util/fs.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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.findFile = exports.isFile = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const isFile = async (filePath) => {
|
|
10
|
+
try {
|
|
11
|
+
const stats = await promises_1.default.stat(filePath);
|
|
12
|
+
return stats.isFile();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
exports.isFile = isFile;
|
|
19
|
+
const findFile = async (cwd, fileName) => {
|
|
20
|
+
const filePath = node_path_1.default.join(cwd, fileName);
|
|
21
|
+
if (await (0, exports.isFile)(filePath)) {
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const parentDir = node_path_1.default.resolve(cwd, '..');
|
|
26
|
+
return parentDir === '/' ? undefined : (0, exports.findFile)(parentDir, fileName);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
exports.findFile = findFile;
|
package/dist/util/path.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const prependDirToPattern: (workingDir: string, pattern: string) => string;
|
|
2
|
+
export declare const resolvePaths: ({ cwd, workingDir, patterns, ignore, gitignore, }: {
|
|
3
|
+
cwd: string;
|
|
4
|
+
workingDir: string;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
ignore: string[];
|
|
7
|
+
gitignore: boolean;
|
|
8
|
+
}) => Promise<string[]>;
|
package/dist/util/path.js
CHANGED
|
@@ -3,26 +3,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
6
|
+
exports.resolvePaths = exports.prependDirToPattern = void 0;
|
|
8
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
8
|
+
let _globby;
|
|
9
|
+
const glob = async function (patterns, options) {
|
|
10
|
+
if (!_globby) {
|
|
11
|
+
const { globby } = await eval('import("globby")');
|
|
12
|
+
_globby = globby;
|
|
16
13
|
}
|
|
14
|
+
return _globby(patterns, options);
|
|
17
15
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const parentDir = node_path_1.default.resolve(cwd, '..');
|
|
25
|
-
return parentDir === '/' ? undefined : (0, exports.findFile)(parentDir, fileName);
|
|
26
|
-
}
|
|
16
|
+
const prependDirToPattern = (workingDir, pattern) => {
|
|
17
|
+
if (pattern.startsWith('!'))
|
|
18
|
+
return '!' + node_path_1.default.join(workingDir, pattern.slice(1));
|
|
19
|
+
return node_path_1.default.join(workingDir, pattern);
|
|
27
20
|
};
|
|
28
|
-
exports.
|
|
21
|
+
exports.prependDirToPattern = prependDirToPattern;
|
|
22
|
+
const resolvePaths = async ({ cwd, workingDir, patterns, ignore, gitignore, }) => glob(patterns.map(pattern => (0, exports.prependDirToPattern)(node_path_1.default.relative(cwd, workingDir), pattern)), {
|
|
23
|
+
cwd,
|
|
24
|
+
ignore,
|
|
25
|
+
gitignore,
|
|
26
|
+
absolute: true,
|
|
27
|
+
});
|
|
28
|
+
exports.resolvePaths = resolvePaths;
|
package/dist/util/project.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Project } from 'ts-morph';
|
|
2
|
-
import type { SourceFile } from 'ts-morph';
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type { ProjectOptions, SourceFile } from 'ts-morph';
|
|
3
|
+
export declare const createProject: ({ projectOptions, paths }: {
|
|
4
|
+
projectOptions: ProjectOptions;
|
|
5
|
+
paths?: string[] | undefined;
|
|
6
|
+
}) => Project;
|
|
5
7
|
export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
|
package/dist/util/project.js
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.partitionSourceFiles = exports.createProject = void 0;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
4
|
const ts_morph_1 = require("ts-morph");
|
|
9
|
-
const
|
|
10
|
-
return [patterns].flat().map(pattern => {
|
|
11
|
-
if (pattern.startsWith('!'))
|
|
12
|
-
return '!' + node_path_1.default.join(cwd, pattern.slice(1));
|
|
13
|
-
return node_path_1.default.join(cwd, pattern);
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
const createProject = async (configuration, paths) => {
|
|
17
|
-
const { tsConfigFilePath, workingDir } = configuration;
|
|
18
|
-
const tsConfig = tsConfigFilePath ? { tsConfigFilePath } : { compilerOptions: { allowJs: true } };
|
|
5
|
+
const createProject = ({ projectOptions, paths }) => {
|
|
19
6
|
const workspace = new ts_morph_1.Project({
|
|
20
|
-
...
|
|
7
|
+
...projectOptions,
|
|
21
8
|
skipAddingFilesFromTsConfig: true,
|
|
22
9
|
skipFileDependencyResolution: true,
|
|
23
10
|
});
|
|
24
11
|
if (paths)
|
|
25
|
-
workspace.addSourceFilesAtPaths(
|
|
12
|
+
workspace.addSourceFilesAtPaths(paths);
|
|
26
13
|
return workspace;
|
|
27
14
|
};
|
|
28
15
|
exports.createProject = createProject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Find unused files and exports in your TypeScript project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"find",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "ISC",
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"globby": "13.1.2",
|
|
41
42
|
"is-builtin-module": "3.2.0",
|
|
42
43
|
"micromatch": "4.0.5",
|
|
43
44
|
"ts-morph": "16.0.0",
|