knip 2.21.1 → 2.22.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
@@ -624,7 +624,7 @@ Tip: back up files or use an VCS like Git before deleting files or making change
624
624
 
625
625
  Repeat the process to reveal new unused files and exports. It's so liberating to remove unused things!
626
626
 
627
- Getting too many reported issues and false positives? Read more about [handling issues][50] describing potential causes
627
+ Getting too many reported issues and false positives? Read more about [handling issues][48] describing potential causes
628
628
  for false positives, and how to handle them.
629
629
 
630
630
  ## Command Line Options
@@ -640,7 +640,7 @@ for false positives, and how to handle them.
640
640
  --production Analyze only production source files (e.g. no tests, devDependencies, exported types)
641
641
  --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)
642
642
  --ignore-internal Ignore exports with tag @internal (JSDoc/TSDoc)
643
- --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)
643
+ -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)
644
644
  --no-gitignore Don't use .gitignore
645
645
  --include Report only provided issue type(s), can be comma-separated or repeated (1)
646
646
  --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)
@@ -675,13 +675,13 @@ for false positives, and how to handle them.
675
675
 
676
676
  ## Potential boost with `--no-gitignore`
677
677
 
678
- To increase performance in a large monorepo, check out [Potential boost with `--no-gitignore`][51].
678
+ To increase performance in a large monorepo, check out [Potential boost with `--no-gitignore`][50].
679
679
 
680
680
  ## Comparison & Migration
681
681
 
682
682
  This table is an ongoing comparison. Based on their docs (please report any mistakes):
683
683
 
684
- | Feature | **knip** | [depcheck][52] | [unimported][53] | [ts-unused-exports][54] | [ts-prune][55] |
684
+ | Feature | **knip** | [depcheck][51] | [unimported][52] | [ts-unused-exports][53] | [ts-prune][54] |
685
685
  | :---------------------- | :------: | :------------: | :--------------: | :---------------------: | :------------: |
686
686
  | Unused files | ✅ | - | ✅ | - | - |
687
687
  | Unused dependencies | ✅ | ✅ | ✅ | - | - |
@@ -739,25 +739,25 @@ The following commands are similar:
739
739
 
740
740
  Many thanks to some of the early adopters of Knip:
741
741
 
742
- - [Block Protocol][56]
743
- - [DeepmergeTS][57]
744
- - [eslint-plugin-functional][58]
745
- - [freeCodeCamp.org][59]
746
- - [is-immutable-type][60]
747
- - [IsaacScript][61]
748
- - [Nuxt][62]
749
- - [Owncast][63]
750
- - [release-it][64]
751
- - [Template TypeScript Node Package][65]
752
- - [Tipi][66]
742
+ - [Block Protocol][55]
743
+ - [DeepmergeTS][56]
744
+ - [eslint-plugin-functional][57]
745
+ - [freeCodeCamp.org][58]
746
+ - [is-immutable-type][59]
747
+ - [IsaacScript][60]
748
+ - [Nuxt][61]
749
+ - [Owncast][62]
750
+ - [release-it][63]
751
+ - [Template TypeScript Node Package][64]
752
+ - [Tipi][65]
753
753
 
754
754
  ## Articles, etc.
755
755
 
756
756
  - Discord: hang out in [The Knip Barn][9]
757
- - Ask your questions in the [Knip knowledge base][67] (powered by OpenAI and [7-docs][68], experimental!)
758
- - Smashing Magazine: [Knip: An Automated Tool For Finding Unused Files, Exports, And Dependencies][69]
759
- - Effective TypeScript: [Recommendation Update: ✂️ Use knip to detect dead code and types][70]
760
- - Josh Goldberg: [Speeding Up Centered Part 4: Unused Code Bloat][71]
757
+ - Ask your questions in the [Knip knowledge base][66] (powered by OpenAI and [7-docs][67], experimental!)
758
+ - Smashing Magazine: [Knip: An Automated Tool For Finding Unused Files, Exports, And Dependencies][68]
759
+ - Effective TypeScript: [Recommendation Update: ✂️ Use knip to detect dead code and types][69]
760
+ - Josh Goldberg: [Speeding Up Centered Part 4: Unused Code Bloat][70]
761
761
 
762
762
  ## Why "Knip"?
763
763
 
@@ -775,7 +775,7 @@ each file, and traversing all of this, why not collect the various issues in one
775
775
 
776
776
  Special thanks to the wonderful people who have contributed to this project:
777
777
 
778
- [![Contributors][73]][72]
778
+ [![Contributors][72]][71]
779
779
 
780
780
  [1]: #workspaces
781
781
  [2]: #plugins
@@ -824,32 +824,31 @@ Special thanks to the wonderful people who have contributed to this project:
824
824
  [45]: https://nx.dev/concepts/integrated-vs-package-based
825
825
  [46]: ./docs/writing-a-plugin.md
826
826
  [47]: ./docs/compilers.md
827
- [48]: #handling-issues
827
+ [48]: ./docs/handling-issues.md
828
828
  [49]: ./docs/reporters-and-preprocessors.md
829
- [50]: ./docs/handling-issues.md
830
- [51]: ./docs/perf-boost-with-no-gitignore.md
831
- [52]: https://github.com/depcheck/depcheck
832
- [53]: https://github.com/smeijer/unimported
833
- [54]: https://github.com/pzavolinsky/ts-unused-exports
834
- [55]: https://github.com/nadeesha/ts-prune
835
- [56]: https://github.com/blockprotocol/blockprotocol
836
- [57]: https://github.com/RebeccaStevens/deepmerge-ts
837
- [58]: https://github.com/eslint-functional/eslint-plugin-functional
838
- [59]: https://github.com/freeCodeCamp/freeCodeCamp
839
- [60]: https://github.com/RebeccaStevens/is-immutable-type
840
- [61]: https://github.com/IsaacScript/isaacscript
841
- [62]: https://github.com/nuxt/nuxt
842
- [63]: https://github.com/owncast/owncast
843
- [64]: https://github.com/release-it/release-it
844
- [65]: https://github.com/JoshuaKGoldberg/template-typescript-node-package
845
- [66]: https://github.com/meienberger/runtipi
846
- [67]: https://knip.deno.dev
847
- [68]: https://github.com/7-docs/7-docs
848
- [69]: https://www.smashingmagazine.com/2023/08/knip-automated-tool-find-unused-files-exports-dependencies/
849
- [70]: https://effectivetypescript.com/2023/07/29/knip/
850
- [71]: https://www.joshuakgoldberg.com/blog/speeding-up-centered-part-4-unused-code-bloat/
851
- [72]: https://github.com/webpro/knip/graphs/contributors
852
- [73]: https://contrib.rocks/image?repo=webpro/knip
829
+ [50]: ./docs/perf-boost-with-no-gitignore.md
830
+ [51]: https://github.com/depcheck/depcheck
831
+ [52]: https://github.com/smeijer/unimported
832
+ [53]: https://github.com/pzavolinsky/ts-unused-exports
833
+ [54]: https://github.com/nadeesha/ts-prune
834
+ [55]: https://github.com/blockprotocol/blockprotocol
835
+ [56]: https://github.com/RebeccaStevens/deepmerge-ts
836
+ [57]: https://github.com/eslint-functional/eslint-plugin-functional
837
+ [58]: https://github.com/freeCodeCamp/freeCodeCamp
838
+ [59]: https://github.com/RebeccaStevens/is-immutable-type
839
+ [60]: https://github.com/IsaacScript/isaacscript
840
+ [61]: https://github.com/nuxt/nuxt
841
+ [62]: https://github.com/owncast/owncast
842
+ [63]: https://github.com/release-it/release-it
843
+ [64]: https://github.com/JoshuaKGoldberg/template-typescript-node-package
844
+ [65]: https://github.com/meienberger/runtipi
845
+ [66]: https://knip.deno.dev
846
+ [67]: https://github.com/7-docs/7-docs
847
+ [68]: https://www.smashingmagazine.com/2023/08/knip-automated-tool-find-unused-files-exports-dependencies/
848
+ [69]: https://effectivetypescript.com/2023/07/29/knip/
849
+ [70]: https://www.joshuakgoldberg.com/blog/speeding-up-centered-part-4-unused-code-bloat/
850
+ [71]: https://github.com/webpro/knip/graphs/contributors
851
+ [72]: https://contrib.rocks/image?repo=webpro/knip
853
852
  [plugin-ava]: ./src/plugins/ava
854
853
  [plugin-babel]: ./src/plugins/babel
855
854
  [plugin-capacitor]: ./src/plugins/capacitor
@@ -32,6 +32,7 @@ export declare class DependencyDeputy {
32
32
  scripts: string[];
33
33
  dependencies: string[];
34
34
  peerDependencies: string[];
35
+ optionalPeerDependencies: string[];
35
36
  optionalDependencies: string[];
36
37
  devDependencies: string[];
37
38
  allDependencies: string[];
@@ -46,12 +47,15 @@ export declare class DependencyDeputy {
46
47
  addReferencedBinary(workspaceName: string, binaryName: string): void;
47
48
  addPeerDependencies(workspaceName: string, peerDependencies: Map<string, Set<string>>): void;
48
49
  getPeerDependenciesOf(workspaceName: string, dependency: string): string[];
50
+ getPeerDependencies(workspaceName: string): string[];
51
+ getOptionalPeerDependencies(workspaceName: string): string[];
49
52
  maybeAddReferencedExternalDependency(workspace: Workspace, packageName: string): boolean;
50
53
  maybeAddReferencedBinary(workspace: Workspace, binaryName: string): boolean;
51
54
  private isInDependencies;
52
55
  settleDependencyIssues(): {
53
56
  dependencyIssues: Issue[];
54
57
  devDependencyIssues: Issue[];
58
+ optionalPeerDependencyIssues: Issue[];
55
59
  };
56
60
  getConfigurationHints(): {
57
61
  configurationHints: ConfigurationHints;
@@ -22,6 +22,11 @@ export class DependencyDeputy {
22
22
  const dependencies = Object.keys(manifest.dependencies ?? {});
23
23
  const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
24
24
  const optionalDependencies = Object.keys(manifest.optionalDependencies ?? {});
25
+ const optionalPeerDependencies = manifest.peerDependenciesMeta
26
+ ? peerDependencies.filter(peerDependency => manifest.peerDependenciesMeta &&
27
+ peerDependency in manifest.peerDependenciesMeta &&
28
+ manifest.peerDependenciesMeta[peerDependency].optional)
29
+ : [];
25
30
  const devDependencies = Object.keys(manifest.devDependencies ?? {});
26
31
  const allDependencies = [...dependencies, ...devDependencies, ...peerDependencies, ...optionalDependencies];
27
32
  this._manifests.set(name, {
@@ -32,6 +37,7 @@ export class DependencyDeputy {
32
37
  scripts,
33
38
  dependencies,
34
39
  peerDependencies,
40
+ optionalPeerDependencies,
35
41
  optionalDependencies,
36
42
  devDependencies,
37
43
  allDependencies,
@@ -79,6 +85,18 @@ export class DependencyDeputy {
79
85
  getPeerDependenciesOf(workspaceName, dependency) {
80
86
  return Array.from(this.peerDependencies.get(workspaceName)?.get(dependency) ?? []);
81
87
  }
88
+ getPeerDependencies(workspaceName) {
89
+ const manifest = this._manifests.get(workspaceName);
90
+ if (!manifest)
91
+ return [];
92
+ return manifest.peerDependencies;
93
+ }
94
+ getOptionalPeerDependencies(workspaceName) {
95
+ const manifest = this._manifests.get(workspaceName);
96
+ if (!manifest)
97
+ return [];
98
+ return manifest.optionalPeerDependencies;
99
+ }
82
100
  maybeAddReferencedExternalDependency(workspace, packageName) {
83
101
  if (isBuiltin(packageName))
84
102
  return true;
@@ -133,6 +151,7 @@ export class DependencyDeputy {
133
151
  settleDependencyIssues() {
134
152
  const dependencyIssues = [];
135
153
  const devDependencyIssues = [];
154
+ const optionalPeerDependencyIssues = [];
136
155
  for (const [workspaceName, { manifestPath, ignoreDependencies, ignoreBinaries }] of this._manifests.entries()) {
137
156
  const referencedDependencies = this.referencedDependencies.get(workspaceName);
138
157
  const installedBinaries = this.getInstalledBinaries(workspaceName);
@@ -175,6 +194,7 @@ export class DependencyDeputy {
175
194
  const isNotReferencedDependency = (dependency) => !isReferencedDependency(dependency);
176
195
  const pd = this.getProductionDependencies(workspaceName);
177
196
  const dd = this.getDevDependencies(workspaceName);
197
+ const od = this.getOptionalPeerDependencies(workspaceName);
178
198
  pd.filter(isNotIgnoredDependency)
179
199
  .filter(isNotIgnoredBinary)
180
200
  .filter(isNotReferencedDependency)
@@ -183,8 +203,12 @@ export class DependencyDeputy {
183
203
  .filter(isNotIgnoredBinary)
184
204
  .filter(isNotReferencedDependency)
185
205
  .forEach(symbol => devDependencyIssues.push({ type: 'devDependencies', filePath: manifestPath, symbol }));
206
+ od.filter(isNotIgnoredDependency)
207
+ .filter(isNotIgnoredBinary)
208
+ .filter(p => isReferencedDependency(p))
209
+ .forEach(symbol => optionalPeerDependencyIssues.push({ type: 'optionalPeerDependencies', filePath: manifestPath, symbol }));
186
210
  }
187
- return { dependencyIssues, devDependencyIssues };
211
+ return { dependencyIssues, devDependencyIssues, optionalPeerDependencyIssues };
188
212
  }
189
213
  getConfigurationHints() {
190
214
  const configurationHints = new Set();
@@ -199,12 +223,13 @@ export class DependencyDeputy {
199
223
  const dependencies = this.isStrict
200
224
  ? this.getProductionDependencies(workspaceName)
201
225
  : [...this.getProductionDependencies(workspaceName), ...this.getDevDependencies(workspaceName)];
226
+ const peerDependencies = this.getPeerDependencies(workspaceName);
202
227
  const isReferencedDep = (name) => referencedDependencies?.has(name) && dependencies.includes(name);
203
228
  const isReferencedBin = (name) => referencedBinaries?.has(name) && installedBinaries?.has(name);
204
229
  ignoreDependencies
205
230
  .filter(packageName => IGNORED_DEPENDENCIES.includes(packageName) ||
206
231
  (workspaceName !== ROOT_WORKSPACE_NAME && this.ignoreDependencies.includes(packageName)) ||
207
- isReferencedDep(packageName))
232
+ (!peerDependencies.includes(packageName) && isReferencedDep(packageName)))
208
233
  .forEach(identifier => configurationHints.add({ workspaceName, identifier, type: 'ignoreDependencies' }));
209
234
  ignoreBinaries
210
235
  .filter(binaryName => IGNORED_GLOBAL_BINARIES.includes(binaryName) ||
@@ -216,11 +241,13 @@ export class DependencyDeputy {
216
241
  const dependencies = this.isStrict
217
242
  ? this.getProductionDependencies(ROOT_WORKSPACE_NAME)
218
243
  : [...this.getProductionDependencies(ROOT_WORKSPACE_NAME), ...this.getDevDependencies(ROOT_WORKSPACE_NAME)];
244
+ const peerDependencies = this.getPeerDependencies(ROOT_WORKSPACE_NAME);
219
245
  Object.keys(rootIgnoreBinaries)
220
246
  .filter(key => IGNORED_GLOBAL_BINARIES.includes(key) || (rootIgnoreBinaries[key] !== 0 && installedBinaries?.has(key)))
221
247
  .forEach(identifier => configurationHints.add({ workspaceName: ROOT_WORKSPACE_NAME, identifier, type: 'ignoreBinaries' }));
222
248
  Object.keys(rootIgnoreDependencies)
223
- .filter(key => IGNORED_DEPENDENCIES.includes(key) || (rootIgnoreDependencies[key] !== 0 && dependencies.includes(key)))
249
+ .filter(key => IGNORED_DEPENDENCIES.includes(key) ||
250
+ (rootIgnoreDependencies[key] !== 0 && !peerDependencies.includes(key) && dependencies.includes(key)))
224
251
  .forEach(identifier => configurationHints.add({ workspaceName: ROOT_WORKSPACE_NAME, identifier, type: 'ignoreDependencies' }));
225
252
  return { configurationHints };
226
253
  }
@@ -60,7 +60,5 @@ export declare class ProjectPrincipal {
60
60
  };
61
61
  findUnusedMembers(filePath: string, members: ExportItemMember[]): string[];
62
62
  private findReferences;
63
- isPublicExport(exportedItem: ExportItem): boolean;
64
- getJSDocTags(exportedItem: ExportItem): string[];
65
63
  }
66
64
  export {};
@@ -2,7 +2,7 @@ import { isGitIgnoredSync } from 'globby';
2
2
  import ts from 'typescript';
3
3
  import { DEFAULT_EXTENSIONS } from './constants.js';
4
4
  import { IGNORED_FILE_EXTENSIONS } from './constants.js';
5
- import { isInModuleBlock } from './typescript/ast-helpers.js';
5
+ import { getJSDocTags, isInModuleBlock } from './typescript/ast-helpers.js';
6
6
  import { createHosts } from './typescript/createHosts.js';
7
7
  import { getImportsAndExports } from './typescript/getImportsAndExports.js';
8
8
  import { SourceFileManager } from './typescript/SourceFileManager.js';
@@ -178,7 +178,7 @@ export class ProjectPrincipal {
178
178
  findUnusedMembers(filePath, members) {
179
179
  return members
180
180
  .filter(member => {
181
- if (this.isPublicExport(member))
181
+ if (getJSDocTags(member.node).includes('@public'))
182
182
  return false;
183
183
  const referencedSymbols = this.findReferences(filePath, member.pos);
184
184
  const files = referencedSymbols
@@ -194,11 +194,4 @@ export class ProjectPrincipal {
194
194
  findReferences(filePath, pos) {
195
195
  return this.backend.lsFindReferences(filePath, pos) ?? [];
196
196
  }
197
- isPublicExport(exportedItem) {
198
- const tags = this.getJSDocTags(exportedItem);
199
- return tags.includes('@public');
200
- }
201
- getJSDocTags(exportedItem) {
202
- return ts.getJSDocTags(exportedItem.node).map(node => node.getText().match(/@\S+/)[0]);
203
- }
204
197
  }
@@ -15,7 +15,7 @@ export const getBinariesFromScript = (script, { cwd, manifest, knownGlobalsOnly
15
15
  if (commandExpansions.length > 0) {
16
16
  return commandExpansions.flatMap(expansion => getBinariesFromNodes(expansion.commandAST.commands)) ?? [];
17
17
  }
18
- if (!binary || binary === '.' || binary === 'source')
18
+ if (!binary || binary === '.' || binary === 'source' || binary === '[')
19
19
  return [];
20
20
  if (binary.startsWith('-') || binary.startsWith('"') || binary.startsWith('..'))
21
21
  return [];
@@ -34,13 +34,15 @@ export const getBinariesFromScript = (script, { cwd, manifest, knownGlobalsOnly
34
34
  case 'LogicalExpression':
35
35
  return getBinariesFromNodes([node.left, node.right]);
36
36
  case 'If':
37
- return getBinariesFromNodes([...node.clause.commands, ...node.then.commands, ...(node.else?.commands ?? [])]);
37
+ return getBinariesFromNodes([node.clause, node.then, ...(node.else ? [node.else] : [])]);
38
38
  case 'For':
39
39
  return getBinariesFromNodes(node.do.commands);
40
40
  case 'CompoundList':
41
41
  return getBinariesFromNodes(node.commands);
42
42
  case 'Pipeline':
43
43
  return getBinariesFromNodes(node.commands);
44
+ case 'Function':
45
+ return getBinariesFromNodes(node.body.commands);
44
46
  default:
45
47
  return [];
46
48
  }
@@ -53,7 +53,7 @@ const commands = [
53
53
  ];
54
54
  export const resolve = (_binary, args, { manifest }) => {
55
55
  const scripts = manifest.scripts ? Object.keys(manifest.scripts) : [];
56
- const parsed = parseArgs(args, {});
56
+ const parsed = parseArgs(args, { alias: { recursive: 'r' }, boolean: ['recursive'] });
57
57
  const [command, binary] = parsed._;
58
58
  if (scripts.includes(command) || commands.includes(command))
59
59
  return [];
package/dist/constants.js CHANGED
@@ -60,6 +60,7 @@ export const ISSUE_TYPES = [
60
60
  'files',
61
61
  'dependencies',
62
62
  'devDependencies',
63
+ 'optionalPeerDependencies',
63
64
  'unlisted',
64
65
  'binaries',
65
66
  'unresolved',
@@ -75,6 +76,7 @@ export const ISSUE_TYPE_TITLE = {
75
76
  files: 'Unused files',
76
77
  dependencies: 'Unused dependencies',
77
78
  devDependencies: 'Unused devDependencies',
79
+ optionalPeerDependencies: 'Referenced optional peerDependencies',
78
80
  unlisted: 'Unlisted dependencies',
79
81
  binaries: 'Unlisted binaries',
80
82
  unresolved: 'Unresolved imports',
package/dist/index.js CHANGED
@@ -208,8 +208,10 @@ export const main = async (unresolvedConfiguration) => {
208
208
  }
209
209
  }
210
210
  duplicate.forEach(symbols => {
211
- const symbol = symbols.join('|');
212
- collector.addIssue({ type: 'duplicates', filePath, symbol, symbols });
211
+ if (symbols.length > 1) {
212
+ const symbol = symbols.join('|');
213
+ collector.addIssue({ type: 'duplicates', filePath, symbol, symbols });
214
+ }
213
215
  });
214
216
  external.forEach(specifier => {
215
217
  const packageName = getPackageNameFromModuleSpecifier(specifier);
@@ -244,6 +246,15 @@ export const main = async (unresolvedConfiguration) => {
244
246
  const unusedFiles = principal.getUnreferencedFiles();
245
247
  collector.addFilesIssues(unusedFiles);
246
248
  collector.addFileCounts({ processed: analyzedFiles.size, unused: unusedFiles.length });
249
+ const isSymbolImported = (symbol, importingModule) => {
250
+ if (!importingModule)
251
+ return false;
252
+ if (importingModule.symbols.has(symbol))
253
+ return true;
254
+ const { isReExport, isReExportedBy } = importingModule;
255
+ const hasSymbol = (file) => isSymbolImported(symbol, importedSymbols.get(file));
256
+ return isReExport ? Array.from(isReExportedBy).some(hasSymbol) : false;
257
+ };
247
258
  const isExportedInEntryFile = (importedModule) => {
248
259
  if (!importedModule)
249
260
  return false;
@@ -265,15 +276,14 @@ export const main = async (unresolvedConfiguration) => {
265
276
  for (const [filePath, exportItems] of exportedSymbols.entries()) {
266
277
  if (!isIncludeEntryExports && principal.entryPaths.has(filePath))
267
278
  continue;
268
- const importedModule = importedSymbols.get(filePath);
279
+ const importingModule = importedSymbols.get(filePath);
269
280
  for (const [symbol, exportedItem] of exportItems.entries()) {
270
- const jsDocTags = principal.getJSDocTags(exportedItem);
271
- if (jsDocTags.includes('@public') || jsDocTags.includes('@beta'))
281
+ if (exportedItem.jsDocTags.includes('@public') || exportedItem.jsDocTags.includes('@beta'))
272
282
  continue;
273
- if (isIgnoreInternal && jsDocTags.includes('@internal'))
283
+ if (isIgnoreInternal && exportedItem.jsDocTags.includes('@internal'))
274
284
  continue;
275
- if (importedModule?.symbols.has(symbol)) {
276
- if (importedModule.isReExport && isExportedInEntryFile(importedModule))
285
+ if (importingModule && isSymbolImported(symbol, importingModule)) {
286
+ if (importingModule.isReExport && isExportedInEntryFile(importingModule))
277
287
  continue;
278
288
  if (report.enumMembers && exportedItem.type === 'enum' && exportedItem.members) {
279
289
  if (isProduction)
@@ -289,8 +299,8 @@ export const main = async (unresolvedConfiguration) => {
289
299
  }
290
300
  continue;
291
301
  }
292
- const isStar = Boolean(importedModule?.isStar);
293
- const isReExportedByEntryFile = !isIncludeEntryExports && isStar && isExportedInEntryFile(importedModule);
302
+ const isStar = Boolean(importingModule?.isStar);
303
+ const isReExportedByEntryFile = !isIncludeEntryExports && isStar && isExportedInEntryFile(importingModule);
294
304
  if (!isReExportedByEntryFile && !isExportedItemReferenced(exportedItem, filePath)) {
295
305
  if (['enum', 'type', 'interface'].includes(exportedItem.type)) {
296
306
  if (isProduction)
@@ -308,11 +318,12 @@ export const main = async (unresolvedConfiguration) => {
308
318
  }
309
319
  }
310
320
  if (isReportDependencies) {
311
- const { dependencyIssues, devDependencyIssues } = deputy.settleDependencyIssues();
321
+ const { dependencyIssues, devDependencyIssues, optionalPeerDependencyIssues } = deputy.settleDependencyIssues();
312
322
  const { configurationHints } = deputy.getConfigurationHints();
313
323
  dependencyIssues.forEach(issue => collector.addIssue(issue));
314
324
  if (!isProduction)
315
325
  devDependencyIssues.forEach(issue => collector.addIssue(issue));
326
+ optionalPeerDependencyIssues.forEach(issue => collector.addIssue(issue));
316
327
  configurationHints.forEach(hint => collector.addConfigurationHint(hint));
317
328
  }
318
329
  const unusedIgnoredWorkspaces = chief.getUnusedIgnoredWorkspaces();
@@ -5,7 +5,7 @@ import { _resolve } from '../../util/require.js';
5
5
  import { fallback } from './fallback.js';
6
6
  const getDependencies = (config) => {
7
7
  const extendsSpecifiers = config.extends ? [config.extends].flat().map(resolveExtendSpecifier) : [];
8
- if (extendsSpecifiers.includes('eslint-plugin-prettier'))
8
+ if (extendsSpecifiers.some(specifier => specifier?.startsWith('eslint-plugin-prettier')))
9
9
  extendsSpecifiers.push('eslint-config-prettier');
10
10
  const plugins = config.plugins ? config.plugins.map(resolvePluginSpecifier) : [];
11
11
  const parser = config.parser;
@@ -1,18 +1,13 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { _getDependenciesFromScripts } from '../../binaries/index.js';
3
+ import { getGitHookPaths } from '../../util/git.js';
3
4
  import { timerify } from '../../util/Performance.js';
4
5
  import { hasDependency } from '../../util/plugin.js';
5
- import { getGitHooksPath } from './helpers.js';
6
6
  export const NAME = 'husky';
7
7
  export const ENABLERS = ['husky'];
8
8
  export const isEnabled = ({ dependencies }) => hasDependency(dependencies, ENABLERS);
9
- const gitHooksPath = getGitHooksPath();
10
- export const CONFIG_FILE_PATTERNS = [
11
- `${gitHooksPath}/prepare-commit-msg`,
12
- `${gitHooksPath}/commit-msg`,
13
- `${gitHooksPath}/pre-{applypatch,commit,merge-commit,push,rebase,receive}`,
14
- `${gitHooksPath}/post-{checkout,commit,merge,rewrite}`,
15
- ];
9
+ const gitHookPaths = getGitHookPaths('.husky');
10
+ export const CONFIG_FILE_PATTERNS = [...gitHookPaths];
16
11
  const findHuskyDependencies = async (configFilePath, { cwd, manifest }) => {
17
12
  const script = readFileSync(configFilePath);
18
13
  return _getDependenciesFromScripts(String(script), {
@@ -1,20 +1,27 @@
1
+ import { readFileSync } from 'fs';
1
2
  import { _getDependenciesFromScripts } from '../../binaries/index.js';
3
+ import { fromBinary } from '../../binaries/util.js';
4
+ import { getGitHookPaths } from '../../util/git.js';
2
5
  import { getValuesByKeyDeep } from '../../util/object.js';
3
6
  import { timerify } from '../../util/Performance.js';
4
7
  import { hasDependency, load } from '../../util/plugin.js';
5
8
  export const NAME = 'Lefthook';
6
9
  export const ENABLERS = ['lefthook', '@arkweid/lefthook'];
7
10
  export const isEnabled = ({ dependencies }) => hasDependency(dependencies, ENABLERS);
8
- export const CONFIG_FILE_PATTERNS = ['lefthook.yml'];
11
+ const gitHookPaths = getGitHookPaths();
12
+ export const CONFIG_FILE_PATTERNS = ['lefthook.yml', ...gitHookPaths];
9
13
  const findLefthookDependencies = async (configFilePath, { cwd, manifest }) => {
10
- const config = await load(configFilePath);
11
- if (!config)
12
- return [];
13
- const scripts = getValuesByKeyDeep(config, 'run').filter((value) => typeof value === 'string');
14
- return _getDependenciesFromScripts(scripts, {
15
- cwd,
16
- manifest,
17
- knownGlobalsOnly: true,
18
- });
14
+ if (configFilePath.endsWith('.yml')) {
15
+ const config = await load(configFilePath);
16
+ if (!config)
17
+ return [];
18
+ const scripts = getValuesByKeyDeep(config, 'run').filter((value) => typeof value === 'string');
19
+ return _getDependenciesFromScripts(scripts, { cwd, manifest, knownGlobalsOnly: true });
20
+ }
21
+ const script = readFileSync(configFilePath, 'utf8');
22
+ const scriptDependencies = _getDependenciesFromScripts([script], { cwd, manifest, knownGlobalsOnly: false });
23
+ const dependencies = manifest.devDependencies ? Object.keys(manifest.devDependencies) : [];
24
+ const matches = scriptDependencies.find(dep => dependencies.includes(fromBinary(dep)));
25
+ return matches ? [matches] : [];
19
26
  };
20
27
  export const findDependencies = timerify(findLefthookDependencies);
@@ -22,6 +22,7 @@ export default async ({ report, issues, options }) => {
22
22
  ...(report.files && { files: false }),
23
23
  ...(report.dependencies && { dependencies: [] }),
24
24
  ...(report.devDependencies && { devDependencies: [] }),
25
+ ...(report.optionalPeerDependencies && { optionalPeerDependencies: [] }),
25
26
  ...(report.unlisted && { unlisted: [] }),
26
27
  ...(report.unresolved && { unresolved: [] }),
27
28
  ...((report.exports || report.nsExports) && { exports: [] }),
@@ -7,6 +7,7 @@ export type ExportItem = {
7
7
  pos: number;
8
8
  type: SymbolType;
9
9
  members?: ExportItemMember[];
10
+ jsDocTags?: string[];
10
11
  };
11
12
  export type ExportItemMember = {
12
13
  node: ts.Node;
@@ -14,6 +15,6 @@ export type ExportItemMember = {
14
15
  pos: number;
15
16
  type: SymbolType;
16
17
  };
17
- export type ExportItems = Map<string, ExportItem>;
18
+ export type ExportItems = Map<string, Required<ExportItem>>;
18
19
  export type Exports = Map<FilePath, ExportItems>;
19
20
  export {};
@@ -22,6 +22,7 @@ export type Issues = {
22
22
  files: IssueSet;
23
23
  dependencies: IssueRecords;
24
24
  devDependencies: IssueRecords;
25
+ optionalPeerDependencies: IssueRecords;
25
26
  unlisted: IssueRecords;
26
27
  binaries: IssueRecords;
27
28
  unresolved: IssueRecords;
@@ -4,6 +4,7 @@ type WorkspaceManifest = {
4
4
  scripts: string[];
5
5
  dependencies: string[];
6
6
  peerDependencies: string[];
7
+ optionalPeerDependencies: string[];
7
8
  optionalDependencies: string[];
8
9
  devDependencies: string[];
9
10
  allDependencies: string[];
@@ -23,4 +23,5 @@ export declare function findAncestor<T>(node: ts.Node | undefined, callback: (el
23
23
  export declare function findDescendants<T>(node: ts.Node | undefined, callback: (element: ts.Node) => boolean | 'STOP'): T[];
24
24
  export declare const isDeclarationFileExtension: (extension: string) => boolean;
25
25
  export declare const isInModuleBlock: (node: ts.Node) => boolean;
26
+ export declare const getJSDocTags: (node: ts.Node) => string[];
26
27
  export {};
@@ -98,3 +98,4 @@ export const isInModuleBlock = (node) => {
98
98
  }
99
99
  return false;
100
100
  };
101
+ export const getJSDocTags = (node) => ts.getJSDocTags(node).map(node => node.getText().match(/@\S+/)[0]);
@@ -3,7 +3,7 @@ import ts from 'typescript';
3
3
  import { getOrSet } from '../util/map.js';
4
4
  import { isMaybePackageName } from '../util/modules.js';
5
5
  import { isInNodeModules } from '../util/path.js';
6
- import { isDeclarationFileExtension, isAccessExpression, getAccessExpressionName } from './ast-helpers.js';
6
+ import { isDeclarationFileExtension, isAccessExpression, getAccessExpressionName, getJSDocTags, } from './ast-helpers.js';
7
7
  import getExportVisitors from './visitors/exports/index.js';
8
8
  import { getJSXImplicitImportBase } from './visitors/helpers.js';
9
9
  import getImportVisitors from './visitors/imports/index.js';
@@ -86,20 +86,25 @@ export const getImportsAndExports = (sourceFile, options) => {
86
86
  }
87
87
  }
88
88
  };
89
- const addExport = ({ node, identifier, type, pos, members }) => {
89
+ const addExport = ({ node, identifier, type, pos, members = [] }) => {
90
90
  if (options.skipExports)
91
91
  return;
92
+ const jsDocTags = getJSDocTags(node);
92
93
  if (exports.has(identifier)) {
93
94
  const item = exports.get(identifier);
94
- exports.set(identifier, { ...item, node, type, pos, members });
95
+ const crew = [...item.members, ...members];
96
+ const tags = [...item.jsDocTags, ...jsDocTags];
97
+ exports.set(identifier, { ...item, node, type, pos, members: crew, jsDocTags: tags });
95
98
  }
96
99
  else {
97
- exports.set(identifier, { node, type, pos, members });
100
+ exports.set(identifier, { node, type, pos, members, jsDocTags });
101
+ }
102
+ if (!jsDocTags.includes('@alias')) {
103
+ if (ts.isExportAssignment(node))
104
+ maybeAddAliasedExport(node.expression, 'default');
105
+ if (ts.isVariableDeclaration(node))
106
+ maybeAddAliasedExport(node.initializer, identifier);
98
107
  }
99
- if (ts.isExportAssignment(node))
100
- maybeAddAliasedExport(node.expression, 'default');
101
- if (ts.isVariableDeclaration(node))
102
- maybeAddAliasedExport(node.initializer, identifier);
103
108
  };
104
109
  const maybeAddAliasedExport = (node, alias) => {
105
110
  const identifier = node?.getText();
@@ -1,4 +1,4 @@
1
- export declare const helpText = "\u2702\uFE0F Find unused files, dependencies and exports in your JavaScript and TypeScript projects\n\nUsage: knip [options]\n\nOptions:\n -c, --config [file] Configuration file path (default: [.]knip.json[c], knip.js, knip.ts or package.json#knip)\n -t, --tsConfig [file] TypeScript configuration path (default: tsconfig.json)\n --production Analyze only production source files (e.g. no tests, devDependencies, exported types)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n --ignore-internal Ignore exports with tag @internal (JSDoc/TSDoc)\n --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --no-gitignore Don't use .gitignore\n --include Report only provided issue type(s), can be comma-separated or repeated (1)\n --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)\n --dependencies Shortcut for --include dependencies,unlisted,unresolved\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --include-entry-exports Include entry files when reporting unused exports\n -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)\n --preprocessor Preprocess the results before providing it to the reporter(s), can be repeated\n --reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --no-config-hints Suppress configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of issues before non-zero exit code (default: 0)\n -d, --debug Show debug output\n --debug-file-filter Filter for files in debug output (regex as string)\n --performance Measure count and running time of expensive functions and display stats table\n -h, --help Print this help text\n -V, --version Print version\n\n(1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, classMembers, types, nsTypes, enumMembers, duplicates\n\nExamples:\n\n$ knip\n$ knip --production\n$ knip --workspace packages/client --include files,dependencies\n$ knip -c ./config/knip.json --reporter compact\n$ knip --reporter codeowners --reporter-options '{\"path\":\".github/CODEOWNERS\"}'\n$ knip --debug --debug-file-filter '(specific|particular)-module'\n\nMore documentation and bug reports: https://github.com/webpro/knip";
1
+ export declare const helpText = "\u2702\uFE0F Find unused files, dependencies and exports in your JavaScript and TypeScript projects\n\nUsage: knip [options]\n\nOptions:\n -c, --config [file] Configuration file path (default: [.]knip.json[c], knip.js, knip.ts or package.json#knip)\n -t, --tsConfig [file] TypeScript configuration path (default: tsconfig.json)\n --production Analyze only production source files (e.g. no tests, devDependencies, exported types)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n --ignore-internal Ignore exports with tag @internal (JSDoc/TSDoc)\n -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --no-gitignore Don't use .gitignore\n --include Report only provided issue type(s), can be comma-separated or repeated (1)\n --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)\n --dependencies Shortcut for --include dependencies,unlisted,unresolved\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --include-entry-exports Include entry files when reporting unused exports\n -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)\n --preprocessor Preprocess the results before providing it to the reporter(s), can be repeated\n --reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --no-config-hints Suppress configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of issues before non-zero exit code (default: 0)\n -d, --debug Show debug output\n --debug-file-filter Filter for files in debug output (regex as string)\n --performance Measure count and running time of expensive functions and display stats table\n -h, --help Print this help text\n -V, --version Print version\n\n(1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, classMembers, types, nsTypes, enumMembers, duplicates\n\nExamples:\n\n$ knip\n$ knip --production\n$ knip --workspace packages/client --include files,dependencies\n$ knip -c ./config/knip.json --reporter compact\n$ knip --reporter codeowners --reporter-options '{\"path\":\".github/CODEOWNERS\"}'\n$ knip --debug --debug-file-filter '(specific|particular)-module'\n\nMore documentation and bug reports: https://github.com/webpro/knip";
2
2
  declare const _default: {
3
3
  config: string | undefined;
4
4
  debug: boolean | undefined;
@@ -9,7 +9,7 @@ Options:
9
9
  --production Analyze only production source files (e.g. no tests, devDependencies, exported types)
10
10
  --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)
11
11
  --ignore-internal Ignore exports with tag @internal (JSDoc/TSDoc)
12
- --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)
12
+ -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)
13
13
  --no-gitignore Don't use .gitignore
14
14
  --include Report only provided issue type(s), can be comma-separated or repeated (1)
15
15
  --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)
@@ -68,7 +68,7 @@ try {
68
68
  strict: { type: 'boolean' },
69
69
  tsConfig: { type: 'string', short: 't' },
70
70
  version: { type: 'boolean', short: 'V' },
71
- workspace: { type: 'string' },
71
+ workspace: { type: 'string', short: 'W' },
72
72
  },
73
73
  });
74
74
  }
@@ -1,7 +1,14 @@
1
1
  import { ISSUE_TYPES } from '../constants.js';
2
2
  export const getIncludedIssueTypes = (cliArgs, { include = [], exclude = [], isProduction = false } = {}) => {
3
3
  if (cliArgs.dependencies) {
4
- cliArgs.include = [...cliArgs.include, 'dependencies', 'unlisted', 'binaries', 'unresolved'];
4
+ cliArgs.include = [
5
+ ...cliArgs.include,
6
+ 'dependencies',
7
+ 'optionalPeerDependencies',
8
+ 'unlisted',
9
+ 'binaries',
10
+ 'unresolved',
11
+ ];
5
12
  }
6
13
  if (cliArgs.exports) {
7
14
  const exports = ['exports', 'nsExports', 'classMembers', 'types', 'nsTypes', 'enumMembers', 'duplicates'];
@@ -21,9 +28,9 @@ export const getIncludedIssueTypes = (cliArgs, { include = [], exclude = [], isP
21
28
  }
22
29
  else {
23
30
  if (_include.includes('dependencies'))
24
- _include.push('devDependencies');
31
+ _include.push('devDependencies', 'optionalPeerDependencies');
25
32
  if (_exclude.includes('dependencies'))
26
- _exclude.push('devDependencies');
33
+ _exclude.push('devDependencies', 'optionalPeerDependencies');
27
34
  }
28
35
  const included = (_include.length > 0 ? _include : ISSUE_TYPES).filter(group => !_exclude.includes(group));
29
36
  return ISSUE_TYPES.reduce((types, group) => ((types[group] = included.includes(group)), types), {});
@@ -0,0 +1 @@
1
+ export declare const getGitHookPaths: (defaultPath?: string) => string[];
@@ -0,0 +1,20 @@
1
+ import { execSync } from 'child_process';
2
+ import { join } from './path.js';
3
+ const hookFileNames = [
4
+ `prepare-commit-msg`,
5
+ `commit-msg`,
6
+ `pre-{applypatch,commit,merge-commit,push,rebase,receive}`,
7
+ `post-{checkout,commit,merge,rewrite}`,
8
+ ];
9
+ const getGitHooksPath = (defaultPath = '.git/hooks') => {
10
+ try {
11
+ return execSync('git config --get core.hooksPath', { encoding: 'utf8' }).trim();
12
+ }
13
+ catch (error) {
14
+ return defaultPath;
15
+ }
16
+ };
17
+ export const getGitHookPaths = (defaultPath = '.git/hooks') => {
18
+ const gitHooksPath = getGitHooksPath(defaultPath);
19
+ return hookFileNames.map(fileName => join(gitHooksPath, fileName));
20
+ };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "2.21.1";
1
+ export declare const version = "2.22.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.21.1';
1
+ export const version = '2.22.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "2.21.1",
3
+ "version": "2.22.0",
4
4
  "description": "Find unused files, dependencies and exports in your TypeScript and JavaScript projects",
5
5
  "homepage": "https://github.com/webpro/knip",
6
6
  "repository": "github:webpro/knip",
@@ -25,7 +25,7 @@
25
25
  "pretest": "node rmdir.js tmp && swc src -d tmp/src && swc tests -d tmp/tests",
26
26
  "test": "node --no-warnings --test tmp",
27
27
  "coverage": "c8 --reporter html node --no-warnings --loader tsx --test tests/*.test.ts tests/*/*.test.ts",
28
- "watch": "tsc --watch",
28
+ "watch": "rm $(which knip); npm link && tsc --watch",
29
29
  "prebuild": "node rmdir.js dist",
30
30
  "build": "tsc",
31
31
  "docs": "npm run docs:cli && npm run docs:plugins && npm run docs:format",
@@ -65,16 +65,16 @@
65
65
  "@npmcli/package-json": "5.0.0",
66
66
  "@release-it/bumper": "5.1.0",
67
67
  "@swc/cli": "0.1.62",
68
- "@swc/core": "1.3.80",
68
+ "@swc/core": "1.3.82",
69
69
  "@types/eslint": "8.44.2",
70
70
  "@types/js-yaml": "4.0.5",
71
71
  "@types/micromatch": "4.0.2",
72
72
  "@types/minimist": "1.2.2",
73
- "@types/node": "20.5.7",
73
+ "@types/node": "20.5.9",
74
74
  "@types/npmcli__map-workspaces": "3.0.1",
75
75
  "@types/webpack": "5.28.2",
76
- "@typescript-eslint/eslint-plugin": "6.5.0",
77
- "@typescript-eslint/parser": "6.5.0",
76
+ "@typescript-eslint/eslint-plugin": "6.6.0",
77
+ "@typescript-eslint/parser": "6.6.0",
78
78
  "c8": "8.0.1",
79
79
  "eslint": "8.48.0",
80
80
  "eslint-import-resolver-typescript": "3.6.0",
@@ -84,7 +84,7 @@
84
84
  "release-it": "16.1.5",
85
85
  "remark-cli": "11.0.0",
86
86
  "remark-preset-webpro": "0.0.3",
87
- "tsx": "3.12.7",
87
+ "tsx": "3.12.8",
88
88
  "type-fest": "4.3.1"
89
89
  },
90
90
  "engines": {
@@ -1 +0,0 @@
1
- export declare const getGitHooksPath: () => string;
@@ -1,9 +0,0 @@
1
- import { execSync } from 'child_process';
2
- export const getGitHooksPath = () => {
3
- try {
4
- return execSync('git config --get core.hooksPath', { encoding: 'utf8' }).trim();
5
- }
6
- catch (error) {
7
- return '.husky';
8
- }
9
- };