knip 0.1.2 → 0.3.0

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 CHANGED
@@ -1,22 +1,25 @@
1
1
  # ✂️ Knip
2
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:
3
+ Knip scans your JavaScript and TypeScript projects for **unused files, dependencies and exports**. Things that should be
4
+ eliminated. Less code means better performance and less to maintain, important for both UX and DX!
5
+
6
+ For comparison, ESLint finds unused variables inside files in isolation, but this will not be flagged:
5
7
 
6
8
  ```ts
7
9
  export const myVar = true;
8
10
  ```
9
11
 
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
+ Unused files will also not be detected by ESLint. So how do you know which files, dependencies and exports are no longer
13
+ used? This requires an analysis of all the right files in the project.
12
14
 
13
15
  This is where Knip comes in:
14
16
 
15
- - [x] Resolves all (unused) files in your project and reports **unused files and exports**.
17
+ - [x] Resolves all (unused) files in your project and reports **unused files, dependencies and exports**.
16
18
  - [x] Verifies that exported symbols are actually used in other files, even when part of an imported namespace.
19
+ - [x] Finds dependencies not listed in `package.json`.
17
20
  - [x] Finds duplicate exports of the same symbol.
18
21
  - [x] Supports JavaScript inside TypeScript projects (`"allowJs": true`)
19
- - [ ] Supports JavaScript-only projects with CommonJS and ESM (no `tsconfig.json`) - TODO
22
+ - [x] Supports JavaScript-only projects using ESM (without a `tsconfig.json`)
20
23
 
21
24
  Knip really shines in larger projects where you have non-production files (such as `/docs`, `/tools` and `/scripts`).
22
25
  The `includes` setting in `tsconfig.json` is often too broad, resulting in too many false negatives. Similar projects
@@ -54,21 +57,20 @@ npx knip
54
57
 
55
58
  This will analyze the project and output unused files, exports, types and duplicate exports.
56
59
 
57
- Use `--only files` when configuring knip for faster initial results.
60
+ Use `--include files` when configuring knip the first time for faster initial results.
58
61
 
59
62
  ## How It Works
60
63
 
61
- knip works by creating two sets of files:
64
+ Knip works by creating two sets of files:
62
65
 
63
66
  1. Production code is the set of files resolved from the `entryFiles`.
64
67
  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).
68
+ 3. The subset of project files that is not production code will be reported as unused files (in red).
66
69
  4. Then the production code (in blue) will be scanned for unused exports.
67
70
 
68
71
  ![How it works](./assets/how-it-works.drawio.svg)
69
72
 
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`).
73
+ Please read on if you think you have too many results: [too many false positives?](#too-many-false-positives)
72
74
 
73
75
  ## Options
74
76
 
@@ -78,79 +80,117 @@ knip [options]
78
80
 
79
81
  Options:
80
82
  -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
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
+ --dev Include `devDependencies` in report(s) (default: false)
85
88
  --no-progress Don't show dynamic progress updates
89
+ --max-issues Maximum number of issues before non-zero exit code (default: 0)
86
90
  --reporter Select reporter: symbols, compact (default: symbols)
87
91
  --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
88
92
 
93
+ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
94
+
89
95
  Examples:
90
96
 
91
97
  $ knip
92
- $ knip --cwd packages/client --only files
98
+ $ knip --dir packages/client --include files
93
99
  $ knip -c ./knip.js --reporter compact --jsdoc public
94
100
 
95
101
  More info: https://github.com/webpro/knip
96
102
  ```
97
103
 
104
+ 🚀 Knip is considerably faster when only the `files` and/or `duplicates` groups are included.
105
+
98
106
  ## Reading the report
99
107
 
100
108
  After analyzing all the files resolved from the `entryFiles` against the `projectFiles`, the report contains the
101
109
  following groups of issues:
102
110
 
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
+ - `files` - Unused files: did not find references to this file
112
+ - `dependencies` - Unused dependencies: did not find references to this dependency
113
+ - `unlisted` - Unlisted dependencies: this dependency is used, but not listed in package.json (1)
114
+ - `exports` - Unused exports: did not find references to this exported variable
115
+ - `nsExports` - Unused exports in namespaces: did not find direct references to this exported variable (2)
116
+ - `types` - Unused types: did not find references to this exported type
117
+ - `nsTypes` - Unused types in namespaces: did not find direct references to this exported variable (2)
118
+ - `duplicates` - Duplicate exports: the same thing is exported more than once with different names
111
119
 
112
- Each group type (in **bold**) can be used in the `--only` and `--exclude` arguments to slice & dice the report to your
113
- needs.
120
+ Each group type can be an `--include` or `--exclude` to slice & dice the report to your needs.
114
121
 
115
- 🚀 The process is considerably faster when reporting only the `files` and/or `duplicates` groups.
122
+ 1. This may also include dependencies that could not be resolved properly, such as `local/dir/file.ts`.
123
+ 2. The variable or type is not referenced directly, and has become a member of a namespace. That's why Knip is not sure
124
+ whether this export can be removed, so please look into it:
116
125
 
117
126
  ## Now what?
118
127
 
119
- After verifying that files reported as unused are indeed not referenced anywhere, they can be deleted.
128
+ As always, make sure to backup files or use Git before deleting files or making changes. Run tests to verify results.
120
129
 
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.
130
+ - Unused files can be deleted.
131
+ - Unused dependencies can be removed from `package.json`.
132
+ - Unlisted dependencies should be added to `package.json`.
133
+ - Unused exports and types: remove the `export` keyword in front of unused exports. Then you (or tools such as ESLint)
134
+ can see whether the variable or type is used within its own file. If this is not the case, it can be removed.
123
135
 
124
136
  🔁 Repeat the process to reveal new unused files and exports. Sometimes it's so liberating to delete things.
125
137
 
126
- ## More configuration examples
138
+ ## Too many false positives?
139
+
140
+ The default configuration for Knip is very strict and targets production code. For best results, it is recommended to
141
+ exclude files such as tests from the project files. Here's why: when including tests and other non-production files,
142
+ they may prevent production files from being reported as unused.
127
143
 
128
- ### Test files
144
+ Excluding non-production files from the `projectFiles` allows Knip to understand what production code can be removed
145
+ (including dependent files!).
129
146
 
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!).
147
+ Non-production code includes files such as end-to-end tests, tooling, scripts, Storybook stories, etc.
133
148
 
134
- The same goes for any type of non-production files, such as Storybook stories or end-to-end tests.
149
+ Think of it the same way as you would split `dependencies` and `devDependencies` in `package.json`.
135
150
 
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`:
151
+ To include both production and test files to analyze the project as a whole, include both sets of files to `entryFiles`,
152
+ and add `dev: true` to a file named such as `knip.dev.json`:
138
153
 
139
154
  ```json
140
155
  {
141
- "entryFiles": ["src/index.ts", "src/**/*.spec.ts"],
142
- "projectFiles": ["src/**/*.ts", "!**/*.e2e.ts"]
156
+ "dev": true,
157
+ "entryFiles": ["src/index.ts", "src/**/*.spec.ts", "src/**/*.e2e.ts"],
158
+ "projectFiles": ["src/**/*.ts"]
143
159
  }
144
160
  ```
145
161
 
146
- In theory this idea could be extended to report some kind of test coverage.
162
+ Use `-c knip.dev.json` and unused files and exports for the combined set of files as configured in `entryFiles` will be
163
+ reported.
164
+
165
+ An alternative way to store `dev` configuration is in this example `package.json`:
166
+
167
+ ```json
168
+ {
169
+ "name": "my-package",
170
+ "scripts": {
171
+ "knip": "knip"
172
+ },
173
+ "knip": {
174
+ "entryFiles": ["src/index.ts"],
175
+ "projectFiles": ["src/**/*.ts", "!**/*.spec.ts"],
176
+ "dev": {
177
+ "entryFiles": ["src/index.ts", "src/**/*.spec.ts", "src/**/*.e2e.ts"],
178
+ "projectFiles": ["src/**/*.ts"]
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ This way, the `--dev` flag will use the `dev` options (and also add `devDependencies` to the `dependencies` report).
185
+
186
+ ## More configuration examples
147
187
 
148
188
  ### Monorepos
149
189
 
150
190
  #### Separate packages
151
191
 
152
- In repos with multiple packages, the `--cwd` option comes in handy. With similar package structures, the packages can be
153
- configured using globs:
192
+ In repos with multiple (published) packages, the `--cwd` option comes in handy. With similar package structures, the
193
+ packages can be configured using globs:
154
194
 
155
195
  ```json
156
196
  {
@@ -166,15 +206,15 @@ Packages can also be explicitly configured per package directory.
166
206
  To scan the packages separately, using the first match from the configuration file:
167
207
 
168
208
  ```
169
- knip --cwd packages/client --config knip.json
170
- knip --cwd packages/services --config knip.json
209
+ knip --cwd packages/client
210
+ knip --cwd packages/services
171
211
  ```
172
212
 
173
213
  #### Connected projects
174
214
 
175
215
  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:
216
+ configuration for an Nx project using Next.js, Jest and Storybook. This configuration file can also be a JavaScript
217
+ file, which allows to add logic and/or comments (e.g. `knip.js`):
178
218
 
179
219
  ```js
180
220
  const entryFiles = ['apps/**/pages/**/*.{js,ts,tsx}'];
@@ -204,10 +244,14 @@ can be tweaked further to the project structure.
204
244
  ### Default reporter
205
245
 
206
246
  ```
207
- $ knip --config ./knip.json
247
+ $ knip
208
248
  --- UNUSED FILES (2)
209
249
  src/chat/helpers.ts
210
250
  src/components/SideBar.tsx
251
+ --- UNUSED DEPENDENCIES (1)
252
+ moment
253
+ --- UNLISTED DEPENDENCIES (1)
254
+ react
211
255
  --- UNUSED EXPORTS (5)
212
256
  lowercaseFirstLetter src/common/src/string/index.ts
213
257
  RegistrationBox src/components/Registration.tsx
@@ -227,10 +271,14 @@ ProductsList, default src/components/Products.tsx
227
271
  ### Compact
228
272
 
229
273
  ```
230
- $ knip --config ./knip.json --reporter compact
274
+ $ knip --reporter compact
231
275
  --- UNUSED FILES (2)
232
276
  src/chat/helpers.ts
233
277
  src/components/SideBar.tsx
278
+ --- UNUSED DEPENDENCIES (1)
279
+ moment
280
+ --- UNLISTED DEPENDENCIES (1)
281
+ react
234
282
  --- UNUSED EXPORTS (4)
235
283
  src/common/src/string/index.ts: lowercaseFirstLetter
236
284
  src/components/Registration.tsx: RegistrationBox
package/dist/cli.js CHANGED
@@ -6,52 +6,92 @@ 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 typescript_1 = __importDefault(require("typescript"));
9
10
  const help_1 = require("./help");
10
11
  const config_1 = require("./util/config");
12
+ const path_1 = require("./util/path");
11
13
  const reporters_1 = __importDefault(require("./reporters"));
12
14
  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)({
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)({
14
16
  options: {
15
17
  help: { type: 'boolean' },
16
- cwd: { type: 'string' },
17
18
  config: { type: 'string', short: 'c' },
18
- only: { type: 'string', multiple: true },
19
+ tsConfig: { type: 'string', short: 't' },
20
+ dir: { type: 'string' },
21
+ include: { type: 'string', multiple: true },
19
22
  exclude: { type: 'string', multiple: true },
23
+ dev: { type: 'boolean' },
24
+ 'max-issues': { type: 'string' },
20
25
  'no-progress': { type: 'boolean' },
21
26
  reporter: { type: 'string' },
22
27
  jsdoc: { type: 'string', multiple: true },
23
- 'max-issues': { type: 'string' },
24
28
  },
25
29
  });
26
30
  if (help) {
27
31
  (0, help_1.printHelp)();
28
32
  process.exit(0);
29
33
  }
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));
34
+ const cwd = process.cwd();
35
+ const workingDir = dir ? node_path_1.default.resolve(dir) : cwd;
36
+ const isShowProgress = noProgress === false ? process.stdout.isTTY && typeof process.stdout.cursorTo === 'function' : !noProgress;
37
+ const printReport = reporter in reporters_1.default ? reporters_1.default[reporter] : require(node_path_1.default.join(workingDir, reporter));
38
38
  const main = async () => {
39
- const resolvedConfig = (0, config_1.resolveConfig)(configuration, cwdArg);
39
+ const localConfigurationPath = await (0, path_1.findFile)(workingDir, configFilePath);
40
+ const manifestPath = await (0, path_1.findFile)(workingDir, 'package.json');
41
+ const localConfiguration = localConfigurationPath && require(localConfigurationPath);
42
+ const manifest = manifestPath && require(manifestPath);
43
+ if (!localConfigurationPath && !manifest.knip) {
44
+ const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
45
+ console.error(`Unable to find ${configFilePath} or package.json#knip in ${location}\n`);
46
+ (0, help_1.printHelp)();
47
+ process.exit(1);
48
+ }
49
+ const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfiguration, { workingDir: dir, isDev });
40
50
  if (!resolvedConfig) {
41
51
  (0, help_1.printHelp)();
42
52
  process.exit(1);
43
53
  }
44
- const config = Object.assign({}, resolvedConfig, {
45
- cwd,
46
- include: (0, config_1.resolveIncludedFromArgs)(only, exclude),
54
+ const report = (0, config_1.resolveIncludedIssueGroups)(include, exclude, resolvedConfig);
55
+ let tsConfigPaths = [];
56
+ const tsConfigPath = await (0, path_1.findFile)(workingDir, tsConfigFilePath ?? 'tsconfig.json');
57
+ if (tsConfigFilePath && !tsConfigPath) {
58
+ console.error(`Unable to find ${tsConfigFilePath}\n`);
59
+ (0, help_1.printHelp)();
60
+ process.exit(1);
61
+ }
62
+ if (tsConfigPath) {
63
+ const tsConfig = typescript_1.default.readConfigFile(tsConfigPath, typescript_1.default.sys.readFile);
64
+ tsConfigPaths = tsConfig.config.compilerOptions?.paths
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`);
69
+ (0, help_1.printHelp)();
70
+ process.exit(1);
71
+ }
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,
47
81
  isShowProgress,
48
82
  jsDocOptions: {
49
83
  isReadPublicTag: jsdoc.includes('public'),
50
84
  },
51
- });
85
+ ...resolvedConfig,
86
+ };
52
87
  const { issues, counters } = await (0, _1.run)(config);
53
- report({ issues, cwd, config });
54
- if (counters.files > Number(maxIssues))
55
- process.exit(counters.files);
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);
95
+ }
56
96
  };
57
97
  main();
package/dist/help.js CHANGED
@@ -6,18 +6,22 @@ const printHelp = () => {
6
6
 
7
7
  Options:
8
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
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
+ --dev Include \`devDependencies\` in report(s) (default: false)
13
14
  --no-progress Don't show dynamic progress updates
15
+ --max-issues Maximum number of issues before non-zero exit code (default: 0)
14
16
  --reporter Select reporter: symbols, compact (default: symbols)
15
17
  --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
16
18
 
19
+ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
20
+
17
21
  Examples:
18
22
 
19
23
  $ knip
20
- $ knip --cwd packages/client --only files
24
+ $ knip --dir packages/client --include files
21
25
  $ knip -c ./knip.js --reporter compact --jsdoc public
22
26
 
23
27
  More info: https://github.com/webpro/knip`);
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export declare function run(configuration: Configuration): Promise<{
3
3
  issues: Issues;
4
4
  counters: {
5
5
  files: number;
6
+ dependencies: number;
7
+ devDependencies: number;
8
+ unresolved: number;
6
9
  exports: number;
7
10
  types: number;
8
11
  nsExports: number;
package/dist/index.js CHANGED
@@ -7,21 +7,27 @@ exports.run = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const ts_morph_1 = require("ts-morph");
9
9
  const ts_morph_helpers_1 = require("ts-morph-helpers");
10
- const util_1 = require("./util");
10
+ const project_1 = require("./util/project");
11
+ const type_1 = require("./util/type");
12
+ const dependencies_1 = require("./util/dependencies");
11
13
  const log_1 = require("./log");
12
14
  const lineRewriter = new log_1.LineRewriter();
13
15
  async function run(configuration) {
14
- const { cwd, isShowProgress, include, jsDocOptions } = configuration;
15
- const production = await (0, util_1.createProject)(cwd, configuration.entryFiles);
16
+ const { workingDir, isShowProgress, report, isDev, jsDocOptions } = configuration;
17
+ const { getUnresolvedDependencies, getUnusedDependencies, getUnusedDevDependencies } = (0, dependencies_1.getDependencyAnalyzer)(configuration);
18
+ const production = await (0, project_1.createProject)(configuration, configuration.entryFiles);
16
19
  const entryFiles = production.getSourceFiles();
17
20
  production.resolveSourceFileDependencies();
18
21
  const productionFiles = production.getSourceFiles();
19
- const project = await (0, util_1.createProject)(cwd, configuration.projectFiles);
22
+ const project = await (0, project_1.createProject)(configuration, configuration.projectFiles);
20
23
  const projectFiles = project.getSourceFiles();
21
- const [usedProductionFiles, unreferencedProductionFiles] = (0, util_1.partitionSourceFiles)(projectFiles, productionFiles);
22
- const [, usedNonEntryFiles] = (0, util_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
24
+ const [usedProductionFiles, unreferencedProductionFiles] = (0, project_1.partitionSourceFiles)(projectFiles, productionFiles);
25
+ const [usedEntryFiles, usedNonEntryFiles] = (0, project_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
23
26
  const issues = {
24
27
  files: new Set(unreferencedProductionFiles.map(file => file.getFilePath())),
28
+ dependencies: new Set(),
29
+ devDependencies: new Set(),
30
+ unresolved: {},
25
31
  exports: {},
26
32
  types: {},
27
33
  nsExports: {},
@@ -30,6 +36,9 @@ async function run(configuration) {
30
36
  };
31
37
  const counters = {
32
38
  files: issues.files.size,
39
+ dependencies: issues.dependencies.size,
40
+ devDependencies: issues.dependencies.size,
41
+ unresolved: 0,
33
42
  exports: 0,
34
43
  types: 0,
35
44
  nsExports: 0,
@@ -44,48 +53,72 @@ async function run(configuration) {
44
53
  const total = unreferencedProductionFiles.length + usedNonEntryFiles.length;
45
54
  const percentage = Math.floor((counter / total) * 100);
46
55
  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'));
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'));
53
63
  if (counter < total) {
54
64
  messages.push('');
55
- messages.push(`Processing: ${node_path_1.default.relative(cwd, item.filePath)}`);
65
+ messages.push(`Processing: ${node_path_1.default.relative(workingDir, item.filePath)}`);
56
66
  }
57
67
  lineRewriter.update(messages);
58
68
  };
59
- const addIssue = (issueType, issue) => {
69
+ const addSymbolIssue = (issueType, issue) => {
60
70
  const { filePath, symbol } = issue;
61
- const key = node_path_1.default.relative(cwd, filePath);
71
+ const key = node_path_1.default.relative(workingDir, filePath);
62
72
  issues[issueType][key] = issues[issueType][key] ?? {};
63
73
  issues[issueType][key][symbol] = issue;
64
74
  counters[issueType]++;
65
75
  updateProcessingOutput(issue);
66
76
  };
67
- if (include.exports || include.types || include.nsExports || include.nsTypes || include.duplicates) {
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) {
68
97
  usedNonEntryFiles.forEach(sourceFile => {
69
98
  const filePath = sourceFile.getFilePath();
99
+ if (report.dependencies || report.unlisted) {
100
+ const unresolvedDependencies = getUnresolvedDependencies(sourceFile);
101
+ unresolvedDependencies.forEach(issue => addSymbolIssue('unresolved', issue));
102
+ }
70
103
  const exportDeclarations = sourceFile.getExportedDeclarations();
71
- if (include.duplicates) {
104
+ if (report.duplicates) {
72
105
  const duplicateExports = (0, ts_morph_helpers_1.findDuplicateExportedNames)(sourceFile);
73
106
  duplicateExports.forEach(symbols => {
74
107
  const symbol = symbols.join('|');
75
- addIssue('duplicates', { filePath, symbol, symbols });
108
+ addSymbolIssue('duplicates', { filePath, symbol, symbols });
76
109
  });
77
110
  }
78
- if (include.exports || include.types || include.nsExports || include.nsTypes) {
111
+ if (report.exports || report.types || report.nsExports || report.nsTypes) {
79
112
  const uniqueExportedSymbols = new Set([...exportDeclarations.values()].flat());
80
113
  if (uniqueExportedSymbols.size === 1)
81
114
  return;
82
115
  exportDeclarations.forEach(declarations => {
83
116
  declarations.forEach(declaration => {
84
- const type = (0, util_1.getType)(declaration);
85
- if (!include.nsExports && !include.nsTypes) {
86
- if (!include.types && type)
117
+ const type = (0, type_1.getType)(declaration);
118
+ if (!report.nsExports && !report.nsTypes) {
119
+ if (!report.types && type)
87
120
  return;
88
- if (!include.exports && !type)
121
+ if (!report.exports && !type)
89
122
  return;
90
123
  }
91
124
  if (jsDocOptions.isReadPublicTag && ts_morph_1.ts.getJSDocPublicTag(declaration.compilerNode))
@@ -94,6 +127,8 @@ async function run(configuration) {
94
127
  if (declaration.isKind(ts_morph_1.ts.SyntaxKind.Identifier)) {
95
128
  identifier = declaration;
96
129
  }
130
+ else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.ArrowFunction)) {
131
+ }
97
132
  else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.FunctionDeclaration) ||
98
133
  declaration.isKind(ts_morph_1.ts.SyntaxKind.ClassDeclaration) ||
99
134
  declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration) ||
@@ -109,36 +144,36 @@ async function run(configuration) {
109
144
  }
110
145
  if (identifier) {
111
146
  const identifierText = identifier.getText();
112
- if (include.exports && issues.exports[filePath]?.[identifierText])
147
+ if (report.exports && issues.exports[filePath]?.[identifierText])
113
148
  return;
114
- if (include.types && issues.types[filePath]?.[identifierText])
149
+ if (report.types && issues.types[filePath]?.[identifierText])
115
150
  return;
116
- if (include.nsExports && issues.nsExports[filePath]?.[identifierText])
151
+ if (report.nsExports && issues.nsExports[filePath]?.[identifierText])
117
152
  return;
118
- if (include.nsTypes && issues.nsTypes[filePath]?.[identifierText])
153
+ if (report.nsTypes && issues.nsTypes[filePath]?.[identifierText])
119
154
  return;
120
155
  const refs = identifier.findReferences();
121
156
  if (refs.length === 0) {
122
- addIssue('exports', { filePath, symbol: identifierText });
157
+ addSymbolIssue('exports', { filePath, symbol: identifierText });
123
158
  }
124
159
  else {
125
160
  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();
161
+ const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === filePath;
127
162
  if (!isReferencedOnlyBySelf)
128
163
  return;
129
164
  if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
130
165
  if (type) {
131
- addIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
166
+ addSymbolIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
132
167
  }
133
168
  else {
134
- addIssue('nsExports', { filePath, symbol: identifierText });
169
+ addSymbolIssue('nsExports', { filePath, symbol: identifierText });
135
170
  }
136
171
  }
137
172
  else if (type) {
138
- addIssue('types', { filePath, symbol: identifierText, symbolType: type });
173
+ addSymbolIssue('types', { filePath, symbol: identifierText, symbolType: type });
139
174
  }
140
175
  else {
141
- addIssue('exports', { filePath, symbol: identifierText });
176
+ addSymbolIssue('exports', { filePath, symbol: identifierText });
142
177
  }
143
178
  }
144
179
  }
@@ -148,6 +183,14 @@ async function run(configuration) {
148
183
  counters.processed++;
149
184
  });
150
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
+ }
151
194
  if (isShowProgress)
152
195
  lineRewriter.resetLines();
153
196
  return { issues, counters };
@@ -1,7 +1,7 @@
1
1
  import type { Issues, Configuration } from '../types';
2
- declare const _default: ({ issues, config, cwd }: {
2
+ declare const _default: ({ issues, config, workingDir }: {
3
3
  issues: Issues;
4
4
  config: Configuration;
5
- cwd: string;
5
+ workingDir: string;
6
6
  }) => void;
7
7
  export default _default;
@@ -4,67 +4,82 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
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(', ')}` : ''}`);
7
+ const logIssueLine = (workingDir, filePath, symbols) => {
8
+ console.log(`${node_path_1.default.relative(workingDir, filePath)}${symbols ? `: ${symbols.join(', ')}` : ''}`);
9
9
  };
10
- const logIssueGroupResult = (issues, cwd, title) => {
10
+ const logIssueGroupResult = (issues, workingDir, title) => {
11
11
  title && console.log(`--- ${title} (${issues.length})`);
12
12
  if (issues.length) {
13
- issues.sort().forEach(filePath => logIssueLine(cwd, filePath));
13
+ issues.sort().forEach(value => console.log(value.startsWith('/') ? node_path_1.default.relative(workingDir, value) : value));
14
14
  }
15
15
  else {
16
- console.log('N/A');
16
+ console.log('Not found');
17
17
  }
18
18
  };
19
- const logIssueGroupResults = (issues, cwd, title) => {
19
+ const logIssueGroupResults = (issues, workingDir, title) => {
20
20
  title && console.log(`--- ${title} (${issues.length})`);
21
21
  if (issues.length) {
22
22
  const sortedByFilePath = issues.sort((a, b) => (a.filePath > b.filePath ? 1 : -1));
23
- sortedByFilePath.forEach(({ filePath, symbols }) => logIssueLine(cwd, filePath, symbols));
23
+ sortedByFilePath.forEach(({ filePath, symbols }) => logIssueLine(workingDir, filePath, symbols));
24
24
  }
25
25
  else {
26
- console.log('N/A');
26
+ console.log('Not found');
27
27
  }
28
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) {
29
+ exports.default = ({ issues, config, workingDir }) => {
30
+ const { report, isDev } = config;
31
+ const reportMultipleGroups = Object.values(report).filter(Boolean).length > 1;
32
+ if (report.files) {
33
33
  const unreferencedFiles = Array.from(issues.files);
34
- logIssueGroupResult(unreferencedFiles, cwd, reportMultipleGroups && 'UNREFERENCED FILES');
34
+ logIssueGroupResult(unreferencedFiles, workingDir, reportMultipleGroups && 'UNUSED FILES');
35
35
  }
36
- if (include.exports) {
36
+ if (report.dependencies) {
37
+ const unreferencedDependencies = Array.from(issues.dependencies);
38
+ logIssueGroupResult(unreferencedDependencies, workingDir, reportMultipleGroups && 'UNUSED DEPENDENCIES');
39
+ }
40
+ if (report.dependencies && isDev) {
41
+ const unreferencedDevDependencies = Array.from(issues.devDependencies);
42
+ logIssueGroupResult(unreferencedDevDependencies, workingDir, 'UNUSED DEV DEPENDENCIES');
43
+ }
44
+ if (report.unlisted) {
45
+ const unreferencedDependencies = Object.values(issues.unresolved).map(issues => {
46
+ const items = Object.values(issues);
47
+ return { ...items[0], symbols: items.map(i => i.symbol) };
48
+ });
49
+ logIssueGroupResults(unreferencedDependencies, workingDir, reportMultipleGroups && 'UNLISTED DEPENDENCIES');
50
+ }
51
+ if (report.exports) {
37
52
  const unreferencedExports = Object.values(issues.exports).map(issues => {
38
53
  const items = Object.values(issues);
39
54
  return { ...items[0], symbols: items.map(i => i.symbol) };
40
55
  });
41
- logIssueGroupResults(unreferencedExports, cwd, reportMultipleGroups && 'UNREFERENCED EXPORTS');
56
+ logIssueGroupResults(unreferencedExports, workingDir, reportMultipleGroups && 'UNUSED EXPORTS');
42
57
  }
43
- if (include.nsExports) {
58
+ if (report.nsExports) {
44
59
  const unreferencedNsExports = Object.values(issues.nsExports).map(issues => {
45
60
  const items = Object.values(issues);
46
61
  return { ...items[0], symbols: items.map(i => i.symbol) };
47
62
  });
48
- logIssueGroupResults(unreferencedNsExports, cwd, reportMultipleGroups && 'UNREFERENCED EXPORTS IN NAMESPACE');
63
+ logIssueGroupResults(unreferencedNsExports, workingDir, reportMultipleGroups && 'UNUSED EXPORTS IN NAMESPACE');
49
64
  }
50
- if (include.types) {
65
+ if (report.types) {
51
66
  const unreferencedTypes = Object.values(issues.types).map(issues => {
52
67
  const items = Object.values(issues);
53
68
  return { ...items[0], symbols: items.map(i => i.symbol) };
54
69
  });
55
- logIssueGroupResults(unreferencedTypes, cwd, reportMultipleGroups && 'UNREFERENCED TYPES');
70
+ logIssueGroupResults(unreferencedTypes, workingDir, reportMultipleGroups && 'UNUSED TYPES');
56
71
  }
57
- if (include.nsTypes) {
72
+ if (report.nsTypes) {
58
73
  const unreferencedNsTypes = Object.values(issues.nsTypes).map(issues => {
59
74
  const items = Object.values(issues);
60
75
  return { ...items[0], symbols: items.map(i => i.symbol) };
61
76
  });
62
- logIssueGroupResults(unreferencedNsTypes, cwd, reportMultipleGroups && 'UNREFERENCED TYPES IN NAMESPACE');
77
+ logIssueGroupResults(unreferencedNsTypes, workingDir, reportMultipleGroups && 'UNUSED TYPES IN NAMESPACE');
63
78
  }
64
- if (include.duplicates) {
79
+ if (report.duplicates) {
65
80
  const unreferencedDuplicates = Object.values(issues.duplicates)
66
81
  .map(issues => Object.values(issues))
67
82
  .flat();
68
- logIssueGroupResults(unreferencedDuplicates, cwd, reportMultipleGroups && 'DUPLICATE EXPORTS');
83
+ logIssueGroupResults(unreferencedDuplicates, workingDir, reportMultipleGroups && 'DUPLICATE EXPORTS');
69
84
  }
70
85
  };
@@ -1,13 +1,13 @@
1
1
  declare const _default: {
2
- symbols: ({ issues, config, cwd }: {
2
+ symbols: ({ issues, config, workingDir }: {
3
3
  issues: import("../types").Issues;
4
4
  config: import("../types").Configuration;
5
- cwd: string;
5
+ workingDir: string;
6
6
  }) => void;
7
- compact: ({ issues, config, cwd }: {
7
+ compact: ({ issues, config, workingDir }: {
8
8
  issues: import("../types").Issues;
9
9
  config: import("../types").Configuration;
10
- cwd: string;
10
+ workingDir: string;
11
11
  }) => void;
12
12
  };
13
13
  export default _default;
@@ -1,7 +1,7 @@
1
1
  import type { Issues, Configuration } from '../types';
2
- declare const _default: ({ issues, config, cwd }: {
2
+ declare const _default: ({ issues, config, workingDir }: {
3
3
  issues: Issues;
4
4
  config: Configuration;
5
- cwd: string;
5
+ workingDir: string;
6
6
  }) => void;
7
7
  export default _default;
@@ -4,55 +4,67 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_path_1 = __importDefault(require("node:path"));
7
- const logIssueLine = ({ issue, cwd, padding }) => {
7
+ const logIssueLine = ({ issue, workingDir, padding }) => {
8
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)}`);
9
+ console.log(`${symbols.padEnd(padding + 2)}${issue.symbolType?.padEnd(11) || ''}${node_path_1.default.relative(workingDir, issue.filePath)}`);
10
10
  };
11
- const logIssueGroupResult = (issues, cwd, title) => {
11
+ const logIssueGroupResult = (issues, workingDir, title) => {
12
12
  title && console.log(`--- ${title} (${issues.length})`);
13
13
  if (issues.length) {
14
- issues.sort().forEach(filePath => console.log(node_path_1.default.relative(cwd, filePath)));
14
+ issues.sort().forEach(value => console.log(value.startsWith('/') ? node_path_1.default.relative(workingDir, value) : value));
15
15
  }
16
16
  else {
17
- console.log('N/A');
17
+ console.log('Not found');
18
18
  }
19
19
  };
20
- const logIssueGroupResults = (issues, cwd, title) => {
20
+ const logIssueGroupResults = (issues, workingDir, title) => {
21
21
  title && console.log(`--- ${title} (${issues.length})`);
22
22
  if (issues.length) {
23
23
  const sortedByFilePath = issues.sort((a, b) => (a.filePath > b.filePath ? 1 : -1));
24
24
  const padding = [...issues].sort((a, b) => b.symbol.length - a.symbol.length)[0].symbol.length;
25
- sortedByFilePath.forEach(issue => logIssueLine({ issue, cwd, padding }));
25
+ sortedByFilePath.forEach(issue => logIssueLine({ issue, workingDir, padding }));
26
26
  }
27
27
  else {
28
- console.log('N/A');
28
+ console.log('Not found');
29
29
  }
30
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) {
31
+ exports.default = ({ issues, config, workingDir }) => {
32
+ const { report, isDev } = config;
33
+ const reportMultipleGroups = Object.values(report).filter(Boolean).length > 1;
34
+ if (report.files) {
35
35
  const unreferencedFiles = Array.from(issues.files);
36
- logIssueGroupResult(unreferencedFiles, cwd, reportMultipleGroups && 'UNUSED FILES');
36
+ logIssueGroupResult(unreferencedFiles, workingDir, reportMultipleGroups && 'UNUSED FILES');
37
37
  }
38
- if (include.exports) {
38
+ if (report.dependencies) {
39
+ const unreferencedDependencies = Array.from(issues.dependencies);
40
+ logIssueGroupResult(unreferencedDependencies, workingDir, reportMultipleGroups && 'UNUSED DEPENDENCIES');
41
+ }
42
+ if (report.dependencies && isDev) {
43
+ const unreferencedDevDependencies = Array.from(issues.devDependencies);
44
+ logIssueGroupResult(unreferencedDevDependencies, workingDir, 'UNUSED DEV DEPENDENCIES');
45
+ }
46
+ if (report.unlisted) {
47
+ const unresolvedDependencies = Object.values(issues.unresolved).map(Object.values).flat();
48
+ logIssueGroupResults(unresolvedDependencies, workingDir, reportMultipleGroups && 'UNLISTED DEPENDENCIES');
49
+ }
50
+ if (report.exports) {
39
51
  const unreferencedExports = Object.values(issues.exports).map(Object.values).flat();
40
- logIssueGroupResults(unreferencedExports, cwd, reportMultipleGroups && 'UNUSED EXPORTS');
52
+ logIssueGroupResults(unreferencedExports, workingDir, reportMultipleGroups && 'UNUSED EXPORTS');
41
53
  }
42
- if (include.nsExports) {
54
+ if (report.nsExports) {
43
55
  const unreferencedNsExports = Object.values(issues.nsExports).map(Object.values).flat();
44
- logIssueGroupResults(unreferencedNsExports, cwd, reportMultipleGroups && 'UNUSED EXPORTS IN NAMESPACE');
56
+ logIssueGroupResults(unreferencedNsExports, workingDir, reportMultipleGroups && 'UNUSED EXPORTS IN NAMESPACE');
45
57
  }
46
- if (include.types) {
58
+ if (report.types) {
47
59
  const unreferencedTypes = Object.values(issues.types).map(Object.values).flat();
48
- logIssueGroupResults(unreferencedTypes, cwd, reportMultipleGroups && 'UNUSED TYPES');
60
+ logIssueGroupResults(unreferencedTypes, workingDir, reportMultipleGroups && 'UNUSED TYPES');
49
61
  }
50
- if (include.nsTypes) {
62
+ if (report.nsTypes) {
51
63
  const unreferencedNsTypes = Object.values(issues.nsTypes).map(Object.values).flat();
52
- logIssueGroupResults(unreferencedNsTypes, cwd, reportMultipleGroups && 'UNUSED TYPES IN NAMESPACE');
64
+ logIssueGroupResults(unreferencedNsTypes, workingDir, reportMultipleGroups && 'UNUSED TYPES IN NAMESPACE');
53
65
  }
54
- if (include.duplicates) {
66
+ if (report.duplicates) {
55
67
  const unreferencedDuplicates = Object.values(issues.duplicates).map(Object.values).flat();
56
- logIssueGroupResults(unreferencedDuplicates, cwd, reportMultipleGroups && 'DUPLICATE EXPORTS');
68
+ logIssueGroupResults(unreferencedDuplicates, workingDir, reportMultipleGroups && 'DUPLICATE EXPORTS');
57
69
  }
58
70
  };
package/dist/types.d.ts CHANGED
@@ -1,15 +1,19 @@
1
- declare type FilePath = string;
2
1
  declare type SymbolType = 'type' | 'interface' | 'enum';
3
- declare type UnusedFileIssues = Set<FilePath>;
2
+ declare type UnusedFileIssues = Set<string>;
4
3
  declare type UnusedExportIssues = Record<string, Record<string, Issue>>;
4
+ declare type UnresolvedDependencyIssues = Record<string, Record<string, Issue>>;
5
+ declare type UnusedDependencyIssues = Set<string>;
5
6
  export declare type Issue = {
6
- filePath: FilePath;
7
+ filePath: string;
7
8
  symbol: string;
8
9
  symbols?: string[];
9
10
  symbolType?: SymbolType;
10
11
  };
11
12
  export declare type Issues = {
12
13
  files: UnusedFileIssues;
14
+ dependencies: UnusedDependencyIssues;
15
+ devDependencies: UnusedDependencyIssues;
16
+ unresolved: UnresolvedDependencyIssues;
13
17
  exports: UnusedExportIssues;
14
18
  types: UnusedExportIssues;
15
19
  nsExports: UnusedExportIssues;
@@ -17,19 +21,34 @@ export declare type Issues = {
17
21
  duplicates: UnusedExportIssues;
18
22
  };
19
23
  export declare type IssueType = keyof Issues;
20
- declare type LocalConfiguration = {
24
+ export declare type ProjectIssueType = Extract<IssueType, 'files' | 'dependencies' | 'devDependencies'>;
25
+ export declare type SymbolIssueType = Exclude<IssueType, ProjectIssueType>;
26
+ export declare type IssueGroup = 'files' | 'dependencies' | 'unlisted' | 'exports' | 'nsExports' | 'types' | 'nsTypes' | 'duplicates';
27
+ export declare type BaseLocalConfiguration = {
21
28
  entryFiles: string[];
22
29
  projectFiles: string[];
23
30
  };
31
+ export declare type LocalConfiguration = BaseLocalConfiguration & {
32
+ dev?: boolean | BaseLocalConfiguration;
33
+ entryFiles: string[];
34
+ projectFiles: string[];
35
+ include?: string[];
36
+ exclude?: string[];
37
+ };
38
+ export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
24
39
  export declare type Configuration = LocalConfiguration & {
25
- cwd: string;
26
- include: {
27
- [key in IssueType]: boolean;
40
+ workingDir: string;
41
+ report: {
42
+ [key in IssueGroup]: boolean;
28
43
  };
44
+ dependencies: string[];
45
+ devDependencies: string[];
46
+ isDev: boolean;
47
+ tsConfigFilePath: undefined | string;
48
+ tsConfigPaths: string[];
29
49
  isShowProgress: boolean;
30
50
  jsDocOptions: {
31
51
  isReadPublicTag: boolean;
32
52
  };
33
53
  };
34
- export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
35
54
  export {};
@@ -1,14 +1,15 @@
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[]) => {
1
+ import type { ImportedConfiguration, LocalConfiguration } from '../types';
2
+ export declare const resolveConfig: (importedConfiguration: ImportedConfiguration, options?: {
3
+ workingDir?: string;
4
+ isDev?: boolean;
5
+ }) => LocalConfiguration | undefined;
6
+ export declare const resolveIncludedIssueGroups: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => {
8
7
  files: boolean;
8
+ dependencies: boolean;
9
9
  exports: boolean;
10
10
  types: boolean;
11
11
  nsExports: boolean;
12
12
  nsTypes: boolean;
13
13
  duplicates: boolean;
14
+ unlisted: boolean;
14
15
  };
@@ -3,47 +3,49 @@ 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.resolveIncludedFromArgs = exports.resolveConfig = exports.importConfig = void 0;
7
- const node_path_1 = __importDefault(require("node:path"));
6
+ exports.resolveIncludedIssueGroups = exports.resolveConfig = void 0;
8
7
  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));
8
+ const resolveConfig = (importedConfiguration, options) => {
9
+ let resolvedConfig = importedConfiguration;
10
+ const { workingDir, isDev } = options ?? {};
11
+ const configKeys = Object.keys(importedConfiguration);
12
+ if (workingDir && !('projectFiles' in importedConfiguration)) {
13
+ const importedConfigKey = configKeys.find(pattern => micromatch_1.default.isMatch(workingDir.replace(/\/$/, ''), pattern));
30
14
  if (importedConfigKey) {
31
- return importedConfiguration[importedConfigKey];
15
+ resolvedConfig = importedConfiguration[importedConfigKey];
32
16
  }
33
17
  }
34
- if (!cwdArg && (!importedConfiguration.entryFiles || !importedConfiguration.projectFiles)) {
18
+ if (isDev && typeof resolvedConfig.dev === 'object' && 'projectFiles' in resolvedConfig.dev) {
19
+ resolvedConfig = resolvedConfig.dev;
20
+ }
21
+ if (!resolvedConfig.entryFiles || !resolvedConfig.projectFiles) {
35
22
  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');
23
+ console.info(`Add these properties at root level, or use --cwd and match one of: ${configKeys.join(', ')}\n`);
37
24
  return;
38
25
  }
39
- return importedConfiguration;
26
+ return resolvedConfig;
40
27
  };
41
28
  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));
29
+ const resolveIncludedIssueGroups = (includeArg, excludeArg, resolvedConfig) => {
30
+ const groups = [
31
+ 'files',
32
+ 'dependencies',
33
+ 'unlisted',
34
+ 'exports',
35
+ 'types',
36
+ 'nsExports',
37
+ 'nsTypes',
38
+ 'duplicates',
39
+ ];
40
+ const include = [includeArg, resolvedConfig?.include ?? []]
41
+ .flat()
42
+ .map(value => value.split(','))
43
+ .flat();
44
+ const exclude = [excludeArg, resolvedConfig?.exclude ?? []]
45
+ .flat()
46
+ .map(value => value.split(','))
47
+ .flat();
48
+ const includes = (include.length > 0 ? include : groups).filter((group) => !exclude.includes(group));
47
49
  return groups.reduce((r, group) => ((r[group] = includes.includes(group)), r), {});
48
50
  };
49
- exports.resolveIncludedFromArgs = resolveIncludedFromArgs;
51
+ exports.resolveIncludedIssueGroups = resolveIncludedIssueGroups;
@@ -0,0 +1,7 @@
1
+ import type { SourceFile } from 'ts-morph';
2
+ import type { Configuration, Issue } from '../types';
3
+ export declare const getDependencyAnalyzer: (configuration: Configuration) => {
4
+ getUnresolvedDependencies: (sourceFile: SourceFile) => Set<Issue>;
5
+ getUnusedDependencies: () => string[];
6
+ getUnusedDevDependencies: () => string[];
7
+ };
@@ -0,0 +1,45 @@
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.getDependencyAnalyzer = void 0;
7
+ const ts_morph_1 = require("ts-morph");
8
+ const is_builtin_module_1 = __importDefault(require("is-builtin-module"));
9
+ const micromatch_1 = __importDefault(require("micromatch"));
10
+ const compact = (collection) => Array.from(new Set(collection)).filter((value) => Boolean(value));
11
+ const getDependencyAnalyzer = (configuration) => {
12
+ const { dependencies, devDependencies, tsConfigPaths } = configuration;
13
+ const referencedDependencies = new Set();
14
+ const getUnresolvedDependencies = (sourceFile) => {
15
+ const unresolvedDependencies = new Set();
16
+ const importLiterals = sourceFile.getImportStringLiterals();
17
+ const requires = sourceFile
18
+ .getDescendantsOfKind(ts_morph_1.ts.SyntaxKind.CallExpression)
19
+ .filter(callExpression => callExpression.getExpression().getText() === 'require')
20
+ .map(expression => expression.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.StringLiteral));
21
+ const literals = compact([importLiterals, requires].flat());
22
+ literals.forEach(importLiteral => {
23
+ const moduleSpecifier = importLiteral.getLiteralText();
24
+ if (moduleSpecifier.startsWith('.'))
25
+ return;
26
+ if ((0, is_builtin_module_1.default)(moduleSpecifier))
27
+ return;
28
+ if (tsConfigPaths.length > 0 && micromatch_1.default.isMatch(moduleSpecifier, tsConfigPaths))
29
+ return;
30
+ const parts = moduleSpecifier.split('/').slice(0, 2);
31
+ const packageName = moduleSpecifier.startsWith('@') ? parts.join('/') : parts[0];
32
+ if (!dependencies.includes(packageName) && !devDependencies.includes(packageName)) {
33
+ unresolvedDependencies.add({ filePath: sourceFile.getFilePath(), symbol: moduleSpecifier });
34
+ }
35
+ if (dependencies.includes(packageName) || devDependencies.includes(packageName)) {
36
+ referencedDependencies.add(packageName);
37
+ }
38
+ });
39
+ return unresolvedDependencies;
40
+ };
41
+ const getUnusedDependencies = () => dependencies.filter(dependency => !referencedDependencies.has(dependency));
42
+ const getUnusedDevDependencies = () => devDependencies.filter(dependency => !referencedDependencies.has(dependency));
43
+ return { getUnresolvedDependencies, getUnusedDependencies, getUnusedDevDependencies };
44
+ };
45
+ exports.getDependencyAnalyzer = getDependencyAnalyzer;
@@ -0,0 +1 @@
1
+ export declare const findFile: (cwd: string, fileName: string) => Promise<string | undefined>;
@@ -0,0 +1,28 @@
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 = 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
+ const findFile = async (cwd, fileName) => {
19
+ const filePath = node_path_1.default.join(cwd, fileName);
20
+ if (await isFile(filePath)) {
21
+ return filePath;
22
+ }
23
+ else {
24
+ const parentDir = node_path_1.default.resolve(cwd, '..');
25
+ return parentDir === '/' ? undefined : (0, exports.findFile)(parentDir, fileName);
26
+ }
27
+ };
28
+ exports.findFile = findFile;
@@ -0,0 +1,5 @@
1
+ import { Project } from 'ts-morph';
2
+ import type { SourceFile } from 'ts-morph';
3
+ import type { Configuration } from '../types';
4
+ export declare const createProject: (configuration: Configuration, paths?: string | string[]) => Promise<Project>;
5
+ export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
@@ -3,25 +3,9 @@ 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.getType = exports.partitionSourceFiles = exports.createProject = void 0;
7
- const promises_1 = __importDefault(require("node:fs/promises"));
6
+ exports.partitionSourceFiles = exports.createProject = void 0;
8
7
  const node_path_1 = __importDefault(require("node:path"));
9
8
  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
9
  const resolvePaths = (cwd, patterns) => {
26
10
  return [patterns].flat().map(pattern => {
27
11
  if (pattern.startsWith('!'))
@@ -29,15 +13,16 @@ const resolvePaths = (cwd, patterns) => {
29
13
  return node_path_1.default.join(cwd, pattern);
30
14
  });
31
15
  };
32
- const createProject = async (cwd, paths) => {
33
- const tsConfigFilePath = await findFile(cwd, 'tsconfig.json');
16
+ const createProject = async (configuration, paths) => {
17
+ const { tsConfigFilePath, workingDir } = configuration;
18
+ const tsConfig = tsConfigFilePath ? { tsConfigFilePath } : { compilerOptions: { allowJs: true } };
34
19
  const workspace = new ts_morph_1.Project({
35
- tsConfigFilePath,
20
+ ...tsConfig,
36
21
  skipAddingFilesFromTsConfig: true,
37
22
  skipFileDependencyResolution: true,
38
23
  });
39
24
  if (paths)
40
- workspace.addSourceFilesAtPaths(resolvePaths(cwd, paths));
25
+ workspace.addSourceFilesAtPaths(resolvePaths(workingDir, paths));
41
26
  return workspace;
42
27
  };
43
28
  exports.createProject = createProject;
@@ -56,12 +41,3 @@ const partitionSourceFiles = (projectFiles, productionFiles) => {
56
41
  return [usedFiles, unusedFiles];
57
42
  };
58
43
  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;
@@ -0,0 +1,2 @@
1
+ import type { ExportedDeclarations } from 'ts-morph';
2
+ export declare const getType: (declaration: ExportedDeclarations) => "type" | "interface" | "enum" | undefined;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getType = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ const getType = (declaration) => {
6
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration))
7
+ return 'type';
8
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.InterfaceDeclaration))
9
+ return 'interface';
10
+ if (declaration.isKind(ts_morph_1.ts.SyntaxKind.EnumDeclaration))
11
+ return 'enum';
12
+ };
13
+ exports.getType = getType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
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
+ "is-builtin-module": "3.2.0",
41
42
  "micromatch": "4.0.5",
42
43
  "ts-morph": "16.0.0",
43
44
  "ts-morph-helpers": "0.5.0"
package/dist/util.d.ts DELETED
@@ -1,5 +0,0 @@
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;