knip 0.1.2 → 0.2.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
@@ -12,8 +12,9 @@ requires an analysis of all the right files in the project.
12
12
 
13
13
  This is where Knip comes in:
14
14
 
15
- - [x] Resolves all (unused) files in your project and reports **unused files and exports**.
15
+ - [x] Resolves all (unused) files in your project and reports **unused files, dependencies and exports**.
16
16
  - [x] Verifies that exported symbols are actually used in other files, even when part of an imported namespace.
17
+ - [x] Finds dependencies not listed in `package.json`.
17
18
  - [x] Finds duplicate exports of the same symbol.
18
19
  - [x] Supports JavaScript inside TypeScript projects (`"allowJs": true`)
19
20
  - [ ] Supports JavaScript-only projects with CommonJS and ESM (no `tsconfig.json`) - TODO
@@ -54,21 +55,20 @@ npx knip
54
55
 
55
56
  This will analyze the project and output unused files, exports, types and duplicate exports.
56
57
 
57
- Use `--only files` when configuring knip for faster initial results.
58
+ Use `--include files` when configuring knip the first time for faster initial results.
58
59
 
59
60
  ## How It Works
60
61
 
61
- knip works by creating two sets of files:
62
+ Knip works by creating two sets of files:
62
63
 
63
64
  1. Production code is the set of files resolved from the `entryFiles`.
64
65
  2. They are matched against the set of `projectFiles`.
65
- 3. The subset of project files that are not production code will be reported as unused files (in red).
66
+ 3. The subset of project files that is not production code will be reported as unused files (in red).
66
67
  4. Then the production code (in blue) will be scanned for unused exports.
67
68
 
68
69
  ![How it works](./assets/how-it-works.drawio.svg)
69
70
 
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`).
71
+ Please read on if you think you have too many results: [too many false positives?](#too-many-false-positives)
72
72
 
73
73
  ## Options
74
74
 
@@ -79,78 +79,91 @@ knip [options]
79
79
  Options:
80
80
  -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
81
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
82
+ --include Report only listed issue group(s) (see below)
83
+ --exclude Exclude issue group(s) from report (see below)
84
+ --dev Include `devDependencies` in report(s) (default: false)
85
85
  --no-progress Don't show dynamic progress updates
86
+ --max-issues Maximum number of issues before non-zero exit code (default: 0)
86
87
  --reporter Select reporter: symbols, compact (default: symbols)
87
88
  --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
88
89
 
90
+ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
91
+
89
92
  Examples:
90
93
 
91
94
  $ knip
92
- $ knip --cwd packages/client --only files
95
+ $ knip --cwd packages/client --include files
93
96
  $ knip -c ./knip.js --reporter compact --jsdoc public
94
97
 
95
98
  More info: https://github.com/webpro/knip
96
99
  ```
97
100
 
101
+ 🚀 Knip is considerably faster when only the `files` and/or `duplicates` groups are included.
102
+
98
103
  ## Reading the report
99
104
 
100
105
  After analyzing all the files resolved from the `entryFiles` against the `projectFiles`, the report contains the
101
106
  following groups of issues:
102
107
 
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**)
108
+ - `files` - Unused files: did not find references to this file
109
+ - `dependencies` - Unused dependencies: did not find references to this dependency
110
+ - `unlisted` - Unlisted dependencies: this dependency is used, but not listed in package.json (1)
111
+ - `exports` - Unused exports: did not find references to this exported variable
112
+ - `nsExports` - Unused exports in namespaces: did not find direct references to this exported variable (2)
113
+ - `types` - Unused types: did not find references to this exported type
114
+ - `nsTypes` - Unused types in namespaces: did not find direct references to this exported variable (2)
115
+ - `duplicates` - Duplicate exports: the same thing is exported more than once with different names
111
116
 
112
- Each group type (in **bold**) can be used in the `--only` and `--exclude` arguments to slice & dice the report to your
113
- needs.
117
+ Each group type can be an `--include` or `--exclude` to slice & dice the report to your needs.
114
118
 
115
- 🚀 The process is considerably faster when reporting only the `files` and/or `duplicates` groups.
119
+ 1. This may also include dependencies that could not be resolved properly, such as `local/dir/file.ts`.
120
+ 2. The variable or type is not referenced directly, and has become a member of a namespace. That's why Knip is not sure
121
+ whether this export can be removed, so please look into it:
116
122
 
117
123
  ## Now what?
118
124
 
119
- After verifying that files reported as unused are indeed not referenced anywhere, they can be deleted.
125
+ As always, make sure to backup files or use Git before deleting files or making changes. Run tests to verify results.
120
126
 
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.
127
+ - Unused files can be deleted.
128
+ - Unused dependencies can be removed from `package.json`.
129
+ - Unlisted dependencies should be added to `package.json`.
130
+ - Unused exports and types: remove the `export` keyword in front of unused exports. Then you (or tools such as ESLint)
131
+ can see whether the variable or type is used within its own file. If this is not the case, it can be removed.
123
132
 
124
133
  🔁 Repeat the process to reveal new unused files and exports. Sometimes it's so liberating to delete things.
125
134
 
126
- ## More configuration examples
135
+ ## Too many false positives?
127
136
 
128
- ### Test files
137
+ The default configuration for Knip is very strict and targets production code. For best results, it is recommended to
138
+ exclude files such as tests from the project files. Here's why: when including tests and other non-production files,
139
+ they may prevent production files from being reported as unused.
129
140
 
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!).
141
+ Excluding non-production files from the `projectFiles` allows Knip to understand what production code can be removed
142
+ (including dependent files!).
133
143
 
134
- The same goes for any type of non-production files, such as Storybook stories or end-to-end tests.
144
+ Non-production code includes files such as end-to-end tests, tooling, scripts, Storybook stories, etc.
135
145
 
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`:
146
+ To include both production and test files to analyze the project as a whole, include both sets of files to `entryFiles`,
147
+ and add `dev: true`:
138
148
 
139
149
  ```json
140
150
  {
141
- "entryFiles": ["src/index.ts", "src/**/*.spec.ts"],
142
- "projectFiles": ["src/**/*.ts", "!**/*.e2e.ts"]
151
+ "dev": true,
152
+ "entryFiles": ["src/index.ts", "src/**/*.spec.ts", "src/**/*.e2e.ts"],
153
+ "projectFiles": ["src/**/*.ts"]
143
154
  }
144
155
  ```
145
156
 
146
- In theory this idea could be extended to report some kind of test coverage.
157
+ Knip will now report unused files and exports for the combined set of files as configured in `entryFiles`.
158
+
159
+ ## More configuration examples
147
160
 
148
161
  ### Monorepos
149
162
 
150
163
  #### Separate packages
151
164
 
152
- In repos with multiple packages, the `--cwd` option comes in handy. With similar package structures, the packages can be
153
- configured using globs:
165
+ In repos with multiple (published) packages, the `--cwd` option comes in handy. With similar package structures, the
166
+ packages can be configured using globs:
154
167
 
155
168
  ```json
156
169
  {
@@ -173,8 +186,8 @@ knip --cwd packages/services --config knip.json
173
186
  #### Connected projects
174
187
 
175
188
  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:
189
+ configuration for an Nx project using Next.js, Jest and Storybook. This configuration file can also be a JavaScript
190
+ file, which allows to add logic and/or comments (e.g. `knip.js`):
178
191
 
179
192
  ```js
180
193
  const entryFiles = ['apps/**/pages/**/*.{js,ts,tsx}'];
package/dist/cli.js CHANGED
@@ -6,52 +6,88 @@ 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, cwd: cwdArg, config: configFilePath = 'knip.json', 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
+ cwd: { type: 'string' },
20
+ include: { type: 'string', multiple: true },
19
21
  exclude: { type: 'string', multiple: true },
22
+ dev: { type: 'boolean' },
23
+ 'max-issues': { type: 'string' },
20
24
  'no-progress': { type: 'boolean' },
21
25
  reporter: { type: 'string' },
22
26
  jsdoc: { type: 'string', multiple: true },
23
- 'max-issues': { type: 'string' },
24
27
  },
25
28
  });
26
29
  if (help) {
27
30
  (0, help_1.printHelp)();
28
31
  process.exit(0);
29
32
  }
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));
33
+ const cwd = process.cwd();
34
+ const workingDir = cwdArg ? node_path_1.default.resolve(cwdArg) : cwd;
35
+ const isShowProgress = noProgress === false ? process.stdout.isTTY && typeof process.stdout.cursorTo === 'function' : !noProgress;
36
+ const printReport = reporter in reporters_1.default ? reporters_1.default[reporter] : require(node_path_1.default.join(workingDir, reporter));
38
37
  const main = async () => {
39
- const resolvedConfig = (0, config_1.resolveConfig)(configuration, cwdArg);
38
+ const localConfigurationPath = await (0, path_1.findFile)(workingDir, configFilePath);
39
+ const manifestPath = await (0, path_1.findFile)(workingDir, 'package.json');
40
+ if (!manifestPath || !localConfigurationPath) {
41
+ const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
42
+ console.error(`Unable to find ${configFilePath} (or package.json#knip) in ${location}\n`);
43
+ (0, help_1.printHelp)();
44
+ process.exit(1);
45
+ }
46
+ const localConfiguration = require(localConfigurationPath);
47
+ const manifest = require(manifestPath);
48
+ const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfiguration, cwdArg);
40
49
  if (!resolvedConfig) {
41
50
  (0, help_1.printHelp)();
42
51
  process.exit(1);
43
52
  }
44
- const config = Object.assign({}, resolvedConfig, {
45
- cwd,
46
- include: (0, config_1.resolveIncludedFromArgs)(only, exclude),
53
+ const report = (0, config_1.resolveIncludedIssueGroups)(include, exclude, resolvedConfig);
54
+ const tsConfigFile = await (0, path_1.findFile)(workingDir, 'tsconfig.json');
55
+ if (!tsConfigFile) {
56
+ const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
57
+ console.error(`Unable to find ${tsConfigFile} or package.json in ${location}\n`);
58
+ (0, help_1.printHelp)();
59
+ process.exit(1);
60
+ }
61
+ const tsConfig = typescript_1.default.readConfigFile(tsConfigFile, typescript_1.default.sys.readFile);
62
+ const tsConfigPaths = tsConfig.config.compilerOptions?.paths
63
+ ? Object.keys(tsConfig.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
64
+ : [];
65
+ if (tsConfig.error) {
66
+ console.error(`An error occured when reading ${node_path_1.default.relative(cwd, tsConfigFile)}.\n`);
67
+ (0, help_1.printHelp)();
68
+ process.exit(1);
69
+ }
70
+ const config = {
71
+ workingDir,
72
+ report,
73
+ dependencies: Object.keys(manifest.dependencies ?? {}),
74
+ devDependencies: Object.keys(manifest.devDependencies ?? {}),
75
+ isDev: resolvedConfig.dev ?? isDev,
76
+ tsConfigPaths,
47
77
  isShowProgress,
48
78
  jsDocOptions: {
49
79
  isReadPublicTag: jsdoc.includes('public'),
50
80
  },
51
- });
81
+ ...resolvedConfig,
82
+ };
52
83
  const { issues, counters } = await (0, _1.run)(config);
53
- report({ issues, cwd, config });
54
- if (counters.files > Number(maxIssues))
55
- process.exit(counters.files);
84
+ printReport({ issues, workingDir, config });
85
+ const reportGroup = report.files ? 'files' : Object.keys(report).find(key => report[key]);
86
+ const counterGroup = reportGroup === 'unlisted' ? 'unresolved' : reportGroup;
87
+ if (counterGroup) {
88
+ const count = counters[counterGroup];
89
+ if (count > Number(maxIssues))
90
+ process.exit(count);
91
+ }
56
92
  };
57
93
  main();
package/dist/help.js CHANGED
@@ -7,17 +7,20 @@ const printHelp = () => {
7
7
  Options:
8
8
  -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
9
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
10
+ --include Report only listed issue group(s) (see below)
11
+ --exclude Exclude issue group(s) from report (see below)
12
+ --dev Include \`devDependencies\` in report(s) (default: false)
13
13
  --no-progress Don't show dynamic progress updates
14
+ --max-issues Maximum number of issues before non-zero exit code (default: 0)
14
15
  --reporter Select reporter: symbols, compact (default: symbols)
15
16
  --jsdoc Enable JSDoc parsing, with options: public (default: disabled)
16
17
 
18
+ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes, duplicates
19
+
17
20
  Examples:
18
21
 
19
22
  $ knip
20
- $ knip --cwd packages/client --only files
23
+ $ knip --cwd packages/client --include files
21
24
  $ knip -c ./knip.js --reporter compact --jsdoc public
22
25
 
23
26
  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)(workingDir, 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)(workingDir, 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))
@@ -109,17 +142,17 @@ async function run(configuration) {
109
142
  }
110
143
  if (identifier) {
111
144
  const identifierText = identifier.getText();
112
- if (include.exports && issues.exports[filePath]?.[identifierText])
145
+ if (report.exports && issues.exports[filePath]?.[identifierText])
113
146
  return;
114
- if (include.types && issues.types[filePath]?.[identifierText])
147
+ if (report.types && issues.types[filePath]?.[identifierText])
115
148
  return;
116
- if (include.nsExports && issues.nsExports[filePath]?.[identifierText])
149
+ if (report.nsExports && issues.nsExports[filePath]?.[identifierText])
117
150
  return;
118
- if (include.nsTypes && issues.nsTypes[filePath]?.[identifierText])
151
+ if (report.nsTypes && issues.nsTypes[filePath]?.[identifierText])
119
152
  return;
120
153
  const refs = identifier.findReferences();
121
154
  if (refs.length === 0) {
122
- addIssue('exports', { filePath, symbol: identifierText });
155
+ addSymbolIssue('exports', { filePath, symbol: identifierText });
123
156
  }
124
157
  else {
125
158
  const refFiles = new Set(refs.map(r => r.compilerObject.references.map(r => r.fileName)).flat());
@@ -128,17 +161,17 @@ async function run(configuration) {
128
161
  return;
129
162
  if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
130
163
  if (type) {
131
- addIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
164
+ addSymbolIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
132
165
  }
133
166
  else {
134
- addIssue('nsExports', { filePath, symbol: identifierText });
167
+ addSymbolIssue('nsExports', { filePath, symbol: identifierText });
135
168
  }
136
169
  }
137
170
  else if (type) {
138
- addIssue('types', { filePath, symbol: identifierText, symbolType: type });
171
+ addSymbolIssue('types', { filePath, symbol: identifierText, symbolType: type });
139
172
  }
140
173
  else {
141
- addIssue('exports', { filePath, symbol: identifierText });
174
+ addSymbolIssue('exports', { filePath, symbol: identifierText });
142
175
  }
143
176
  }
144
177
  }
@@ -148,6 +181,14 @@ async function run(configuration) {
148
181
  counters.processed++;
149
182
  });
150
183
  }
184
+ if (report.dependencies) {
185
+ const unusedDependencies = getUnusedDependencies();
186
+ unusedDependencies.forEach(symbol => addProjectIssue('dependencies', { filePath: '', symbol }));
187
+ if (isDev) {
188
+ const unusedDevDependencies = getUnusedDevDependencies();
189
+ unusedDevDependencies.forEach(symbol => addProjectIssue('devDependencies', { filePath: '', symbol }));
190
+ }
191
+ }
151
192
  if (isShowProgress)
152
193
  lineRewriter.resetLines();
153
194
  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, reportMultipleGroups && '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, reportMultipleGroups && '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,29 @@ 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 LocalConfiguration = {
28
+ dev?: boolean;
21
29
  entryFiles: string[];
22
30
  projectFiles: string[];
31
+ include?: string[];
32
+ exclude?: string[];
23
33
  };
34
+ export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
24
35
  export declare type Configuration = LocalConfiguration & {
25
- cwd: string;
26
- include: {
27
- [key in IssueType]: boolean;
36
+ workingDir: string;
37
+ report: {
38
+ [key in IssueGroup]: boolean;
28
39
  };
40
+ dependencies: string[];
41
+ devDependencies: string[];
42
+ isDev: boolean;
43
+ tsConfigPaths: string[];
29
44
  isShowProgress: boolean;
30
45
  jsDocOptions: {
31
46
  isReadPublicTag: boolean;
32
47
  };
33
48
  };
34
- export declare type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;
35
49
  export {};
@@ -1,14 +1,12 @@
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, cwdArg?: string) => LocalConfiguration | undefined;
3
+ export declare const resolveIncludedIssueGroups: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => {
8
4
  files: boolean;
5
+ dependencies: boolean;
9
6
  exports: boolean;
10
7
  types: boolean;
11
8
  nsExports: boolean;
12
9
  nsTypes: boolean;
13
10
  duplicates: boolean;
11
+ unlisted: boolean;
14
12
  };
@@ -3,47 +3,44 @@ 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
8
  const resolveConfig = (importedConfiguration, cwdArg) => {
9
+ const configKeys = Object.keys(importedConfiguration);
28
10
  if (cwdArg && !('projectFiles' in importedConfiguration)) {
29
- const importedConfigKey = Object.keys(importedConfiguration).find(pattern => micromatch_1.default.isMatch(cwdArg, pattern));
11
+ const importedConfigKey = configKeys.find(pattern => micromatch_1.default.isMatch(cwdArg.replace(/\/$/, ''), pattern));
30
12
  if (importedConfigKey) {
31
13
  return importedConfiguration[importedConfigKey];
32
14
  }
33
15
  }
34
16
  if (!cwdArg && (!importedConfiguration.entryFiles || !importedConfiguration.projectFiles)) {
35
17
  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');
18
+ console.info(`Add it at root level, or use --cwd and match one of: ${configKeys.join(', ')}\n`);
37
19
  return;
38
20
  }
39
21
  return importedConfiguration;
40
22
  };
41
23
  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));
24
+ const resolveIncludedIssueGroups = (includeArg, excludeArg, resolvedConfig) => {
25
+ const groups = [
26
+ 'files',
27
+ 'dependencies',
28
+ 'unlisted',
29
+ 'exports',
30
+ 'types',
31
+ 'nsExports',
32
+ 'nsTypes',
33
+ 'duplicates',
34
+ ];
35
+ const include = [includeArg, resolvedConfig?.include ?? []]
36
+ .flat()
37
+ .map(value => value.split(','))
38
+ .flat();
39
+ const exclude = [excludeArg, resolvedConfig?.exclude ?? []]
40
+ .flat()
41
+ .map(value => value.split(','))
42
+ .flat();
43
+ const includes = (include.length > 0 ? include : groups).filter((group) => !exclude.includes(group));
47
44
  return groups.reduce((r, group) => ((r[group] = includes.includes(group)), r), {});
48
45
  };
49
- exports.resolveIncludedFromArgs = resolveIncludedFromArgs;
46
+ 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;
@@ -1,5 +1,4 @@
1
1
  import { Project } from 'ts-morph';
2
- import type { SourceFile, ExportedDeclarations } from 'ts-morph';
2
+ import type { SourceFile } from 'ts-morph';
3
3
  export declare const createProject: (cwd: string, paths?: string | string[]) => Promise<Project>;
4
4
  export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
5
- export declare const getType: (declaration: ExportedDeclarations) => "type" | "interface" | "enum" | undefined;
@@ -3,25 +3,10 @@ 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
- };
9
+ const path_1 = require("./path");
25
10
  const resolvePaths = (cwd, patterns) => {
26
11
  return [patterns].flat().map(pattern => {
27
12
  if (pattern.startsWith('!'))
@@ -30,7 +15,7 @@ const resolvePaths = (cwd, patterns) => {
30
15
  });
31
16
  };
32
17
  const createProject = async (cwd, paths) => {
33
- const tsConfigFilePath = await findFile(cwd, 'tsconfig.json');
18
+ const tsConfigFilePath = await (0, path_1.findFile)(cwd, 'tsconfig.json');
34
19
  const workspace = new ts_morph_1.Project({
35
20
  tsConfigFilePath,
36
21
  skipAddingFilesFromTsConfig: true,
@@ -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.2.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"