knip 5.75.1 → 5.76.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.
Files changed (43) hide show
  1. package/README.md +22 -21
  2. package/dist/DependencyDeputy.d.ts +1 -1
  3. package/dist/DependencyDeputy.js +3 -3
  4. package/dist/WorkspaceWorker.d.ts +4 -4
  5. package/dist/WorkspaceWorker.js +5 -9
  6. package/dist/binaries/fallback.js +1 -1
  7. package/dist/cli.js +14 -14
  8. package/dist/graph/analyze.js +3 -17
  9. package/dist/graph/build.js +17 -6
  10. package/dist/graph-explorer/explorer.d.ts +2 -1
  11. package/dist/graph-explorer/explorer.js +2 -0
  12. package/dist/graph-explorer/operations/build-exports-tree.d.ts +3 -3
  13. package/dist/graph-explorer/operations/get-dependency-usage.d.ts +14 -0
  14. package/dist/graph-explorer/operations/get-dependency-usage.js +38 -0
  15. package/dist/plugins/sst/resolveFromAST.js +4 -4
  16. package/dist/reporters/githubActions.js +3 -1
  17. package/dist/reporters/trace.d.ts +11 -0
  18. package/dist/reporters/trace.js +45 -0
  19. package/dist/session/file-descriptor.d.ts +1 -1
  20. package/dist/session/file-descriptor.js +1 -1
  21. package/dist/session/index.d.ts +2 -0
  22. package/dist/session/index.js +1 -0
  23. package/dist/session/package-json-descriptor.d.ts +6 -0
  24. package/dist/session/package-json-descriptor.js +7 -0
  25. package/dist/session/session.d.ts +3 -1
  26. package/dist/session/session.js +2 -0
  27. package/dist/types/config.d.ts +1 -2
  28. package/dist/types/module-graph.d.ts +5 -0
  29. package/dist/typescript/get-imports-and-exports.js +2 -2
  30. package/dist/util/catalog.js +4 -2
  31. package/dist/util/cli-arguments.d.ts +2 -1
  32. package/dist/util/cli-arguments.js +3 -1
  33. package/dist/util/create-input-handler.d.ts +8 -0
  34. package/dist/util/{get-referenced-inputs.js → create-input-handler.js} +19 -5
  35. package/dist/util/create-options.d.ts +2 -1
  36. package/dist/util/create-options.js +41 -40
  37. package/dist/util/module-graph.d.ts +1 -0
  38. package/dist/util/module-graph.js +2 -1
  39. package/dist/util/trace.d.ts +2 -2
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +1 -1
  43. package/dist/util/get-referenced-inputs.d.ts +0 -5
package/README.md CHANGED
@@ -21,28 +21,28 @@ performance, less maintenance and easier refactorings.
21
21
  - GitHub repo: [webpro-nl/knip][4]
22
22
  - npm package: [knip][1]
23
23
  - [Contributing Guide][7]
24
- - Follow [@webpro.nl on Bluesky][3] for updates
25
- - [Sponsor Knip!][8]
24
+ - Follow [@webpro.nl on Bluesky][8] for updates
25
+ - [Sponsor Knip!][9]
26
26
 
27
27
  ## Contributors
28
28
 
29
- Special thanks to [the wonderful people who have contributed to Knip][9]!
29
+ Special thanks to [the wonderful people who have contributed to Knip][10]!
30
30
 
31
31
  ## Knip
32
32
 
33
- /'knɪp/ means "(to) cut" and is [pronounced with a hard "K"][10] 🇳🇱
33
+ /'knɪp/ means "(to) cut" and is [pronounced with a hard "K"][11] 🇳🇱
34
34
 
35
35
  ## License
36
36
 
37
- Knip is free and open-source software licensed under the [ISC License][11].
37
+ Knip is free and open-source software licensed under the [ISC License][12].
38
38
 
39
39
  Parts of Knip have been inspired by and/or partially copy code from the
40
40
  following projects:
41
41
 
42
- - [@npmcli/package-json][12] ([ISC][13])
43
- - [@pnpm/deps.graph-sequencer][14] ([MIT][15])
44
- - [file-entry-cache][16] ([MIT][17])
45
- - [json-parse-even-better-errors][18] ([MIT][19])
42
+ - [@npmcli/package-json][13] ([ISC][14])
43
+ - [@pnpm/deps.graph-sequencer][15] ([MIT][16])
44
+ - [file-entry-cache][17] ([MIT][18])
45
+ - [json-parse-even-better-errors][19] ([MIT][20])
46
46
 
47
47
  [1]: https://www.npmjs.com/package/knip
48
48
  [2]: https://img.shields.io/npm/v/knip?color=f56e0f
@@ -52,16 +52,17 @@ following projects:
52
52
  https://img.shields.io/github/stars/webpro-nl/knip?style=flat-square&color=f56e0f
53
53
  [6]: https://knip.dev
54
54
  [7]: https://github.com/webpro-nl/knip/blob/main/.github/CONTRIBUTING.md
55
- [8]: https://knip.dev/sponsors
56
- [9]: https://knip.dev/#created-by-awesome-contributors
57
- [10]: https://www.youtube.com/watch?v=PE7h7KvQoUI&t=9s
58
- [11]: ./license
59
- [12]: https://github.com/npm/package-json
60
- [13]: https://github.com/npm/package-json/blob/main/LICENSE
61
- [14]: https://github.com/pnpm/pnpm/tree/main/deps/graph-sequencer
62
- [15]: https://github.com/pnpm/pnpm/blob/main/LICENSE
63
- [16]: https://github.com/jaredwray/cacheable/tree/main/packages/file-entry-cache
64
- [17]:
55
+ [8]: https://bsky.app/profile/webpro.nl
56
+ [9]: https://knip.dev/sponsors
57
+ [10]: https://knip.dev/#created-by-awesome-contributors
58
+ [11]: https://www.youtube.com/watch?v=PE7h7KvQoUI&t=9s
59
+ [12]: ./license
60
+ [13]: https://github.com/npm/package-json
61
+ [14]: https://github.com/npm/package-json/blob/main/LICENSE
62
+ [15]: https://github.com/pnpm/pnpm/tree/main/deps/graph-sequencer
63
+ [16]: https://github.com/pnpm/pnpm/blob/main/LICENSE
64
+ [17]: https://github.com/jaredwray/cacheable/tree/main/packages/file-entry-cache
65
+ [18]:
65
66
  https://github.com/jaredwray/cacheable/blob/main/packages/file-entry-cache/LICENSE
66
- [18]: https://github.com/npm/json-parse-even-better-errors
67
- [19]: https://github.com/npm/json-parse-even-better-errors/blob/main/LICENSE.md
67
+ [19]: https://github.com/npm/json-parse-even-better-errors
68
+ [20]: https://github.com/npm/json-parse-even-better-errors/blob/main/LICENSE.md
@@ -56,7 +56,7 @@ export declare class DependencyDeputy {
56
56
  }[];
57
57
  getOptionalPeerDependencies(workspaceName: string): DependencyArray;
58
58
  maybeAddReferencedExternalDependency(workspace: Workspace, packageName: string): boolean;
59
- maybeAddReferencedBinary(workspace: Workspace, binaryName: string): boolean;
59
+ maybeAddReferencedBinary(workspace: Workspace, binaryName: string): Set<string> | undefined;
60
60
  private isInDependencies;
61
61
  settleDependencyIssues(): {
62
62
  dependencyIssues: Issue[];
@@ -146,7 +146,7 @@ export class DependencyDeputy {
146
146
  }
147
147
  maybeAddReferencedBinary(workspace, binaryName) {
148
148
  if (IGNORED_GLOBAL_BINARIES.has(binaryName))
149
- return true;
149
+ return new Set();
150
150
  this.addReferencedBinary(workspace.name, binaryName);
151
151
  const workspaceNames = this.isStrict ? [workspace.name] : [workspace.name, ...[...workspace.ancestors].reverse()];
152
152
  for (const name of workspaceNames) {
@@ -156,11 +156,11 @@ export class DependencyDeputy {
156
156
  if (dependencies?.size) {
157
157
  for (const dependency of dependencies)
158
158
  this.addReferencedDependency(name, dependency);
159
- return true;
159
+ return dependencies;
160
160
  }
161
161
  }
162
162
  }
163
- return false;
163
+ return;
164
164
  }
165
165
  isInDependencies(workspaceName, packageName) {
166
166
  const manifest = this._manifests.get(workspaceName);
@@ -1,6 +1,6 @@
1
1
  import { CacheConsultant } from './CacheConsultant.js';
2
2
  import { type Workspace } from './ConfigurationChief.js';
3
- import type { GetReferencedInternalFilePath, GetSourceFile, WorkspaceConfiguration } from './types/config.js';
3
+ import type { GetSourceFile, HandleInput, WorkspaceConfiguration } from './types/config.js';
4
4
  import type { ConfigurationHint } from './types/issues.js';
5
5
  import type { PluginName } from './types/PluginNames.js';
6
6
  import type { PackageJson } from './types/package-json.js';
@@ -13,7 +13,7 @@ type WorkspaceManagerOptions = {
13
13
  config: WorkspaceConfiguration;
14
14
  manifest: PackageJson;
15
15
  dependencies: DependencySet;
16
- getReferencedInternalFilePath: GetReferencedInternalFilePath;
16
+ handleInput: HandleInput;
17
17
  findWorkspaceByFilePath: (filePath: string) => Workspace | undefined;
18
18
  getSourceFile: GetSourceFile;
19
19
  negatedWorkspacePatterns: string[];
@@ -33,7 +33,7 @@ export declare class WorkspaceWorker {
33
33
  config: WorkspaceConfiguration;
34
34
  manifest: PackageJson;
35
35
  dependencies: DependencySet;
36
- getReferencedInternalFilePath: GetReferencedInternalFilePath;
36
+ handleInput: HandleInput;
37
37
  findWorkspaceByFilePath: (filePath: string) => Workspace | undefined;
38
38
  getSourceFile: GetSourceFile;
39
39
  negatedWorkspacePatterns: string[];
@@ -44,7 +44,7 @@ export declare class WorkspaceWorker {
44
44
  enabledPluginsInAncestors: string[];
45
45
  cache: CacheConsultant<CacheItem>;
46
46
  configFilesMap: Map<string, Map<PluginName, Set<string>>>;
47
- constructor({ name, dir, config, manifest, dependencies, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, getReferencedInternalFilePath, findWorkspaceByFilePath, getSourceFile, configFilesMap, options, }: WorkspaceManagerOptions);
47
+ constructor({ name, dir, config, manifest, dependencies, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, handleInput, findWorkspaceByFilePath, getSourceFile, configFilesMap, options, }: WorkspaceManagerOptions);
48
48
  init(): Promise<void>;
49
49
  private determineEnabledPlugins;
50
50
  private getConfigForPlugin;
@@ -22,7 +22,7 @@ export class WorkspaceWorker {
22
22
  config;
23
23
  manifest;
24
24
  dependencies;
25
- getReferencedInternalFilePath;
25
+ handleInput;
26
26
  findWorkspaceByFilePath;
27
27
  getSourceFile;
28
28
  negatedWorkspacePatterns = [];
@@ -33,7 +33,7 @@ export class WorkspaceWorker {
33
33
  enabledPluginsInAncestors;
34
34
  cache;
35
35
  configFilesMap;
36
- constructor({ name, dir, config, manifest, dependencies, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, getReferencedInternalFilePath, findWorkspaceByFilePath, getSourceFile, configFilesMap, options, }) {
36
+ constructor({ name, dir, config, manifest, dependencies, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, handleInput, findWorkspaceByFilePath, getSourceFile, configFilesMap, options, }) {
37
37
  this.name = name;
38
38
  this.dir = dir;
39
39
  this.config = config;
@@ -43,7 +43,7 @@ export class WorkspaceWorker {
43
43
  this.ignoredWorkspacePatterns = ignoredWorkspacePatterns;
44
44
  this.enabledPluginsInAncestors = enabledPluginsInAncestors;
45
45
  this.configFilesMap = configFilesMap;
46
- this.getReferencedInternalFilePath = getReferencedInternalFilePath;
46
+ this.handleInput = handleInput;
47
47
  this.findWorkspaceByFilePath = findWorkspaceByFilePath;
48
48
  this.getSourceFile = getSourceFile;
49
49
  this.options = options;
@@ -171,7 +171,7 @@ export class WorkspaceWorker {
171
171
  const configFiles = this.configFilesMap.get(wsName);
172
172
  const seen = new Map();
173
173
  const storeConfigFilePath = (pluginName, input) => {
174
- const configFilePath = this.getReferencedInternalFilePath(input);
174
+ const configFilePath = this.handleInput(input);
175
175
  if (configFilePath) {
176
176
  const workspace = this.findWorkspaceByFilePath(configFilePath);
177
177
  if (workspace) {
@@ -272,11 +272,7 @@ export class WorkspaceWorker {
272
272
  }
273
273
  if (plugin.resolveFromAST) {
274
274
  const sourceFile = this.getSourceFile(configFilePath);
275
- const resolveASTOpts = {
276
- ...resolveOpts,
277
- getSourceFile: this.getSourceFile,
278
- getReferencedInternalFilePath: this.getReferencedInternalFilePath,
279
- };
275
+ const resolveASTOpts = { ...resolveOpts, getSourceFile: this.getSourceFile };
280
276
  if (sourceFile) {
281
277
  const inputs = plugin.resolveFromAST(sourceFile, resolveASTOpts);
282
278
  for (const input of inputs)
@@ -3,7 +3,7 @@ import { compact } from '../util/array.js';
3
3
  import { toBinary, toDeferResolve, toEntry } from '../util/input.js';
4
4
  import { isValidBinary } from './bash-parser.js';
5
5
  const spawningBinaries = ['cross-env', 'retry-cli'];
6
- const endOfCommandBinaries = ['dotenvx', 'env-cmd'];
6
+ const endOfCommandBinaries = ['dotenvx', 'env-cmd', 'op'];
7
7
  const positionals = new Set(['babel-node', 'esbuild', 'execa', 'jiti', 'oxnode', 'vite-node', 'zx']);
8
8
  const positionalBinaries = new Set(['concurrently']);
9
9
  export const resolve = (binary, args, { fromArgs }) => {
package/dist/cli.js CHANGED
@@ -7,9 +7,9 @@ import { perfObserver } from './util/Performance.js';
7
7
  import { runPreprocessors, runReporters } from './util/reporter.js';
8
8
  import { prettyMilliseconds } from './util/string.js';
9
9
  import { version } from './version.js';
10
- let parsedCLIArgs = {};
10
+ let args = {};
11
11
  try {
12
- parsedCLIArgs = parseArgs();
12
+ args = parseArgs();
13
13
  }
14
14
  catch (error) {
15
15
  if (error instanceof Error) {
@@ -21,12 +21,12 @@ catch (error) {
21
21
  }
22
22
  const run = async () => {
23
23
  try {
24
- const options = await createOptions({ parsedCLIArgs });
25
- if (parsedCLIArgs.help) {
24
+ const options = await createOptions({ args });
25
+ if (args.help) {
26
26
  console.log(helpText);
27
27
  process.exit(0);
28
28
  }
29
- if (parsedCLIArgs.version) {
29
+ if (args.version) {
30
30
  console.log(version);
31
31
  process.exit(0);
32
32
  }
@@ -46,12 +46,12 @@ const run = async () => {
46
46
  isProduction: options.isProduction,
47
47
  isShowProgress: options.isShowProgress,
48
48
  isTreatConfigHintsAsErrors: options.isTreatConfigHintsAsErrors,
49
- maxShowIssues: parsedCLIArgs['max-show-issues'] ? Number(parsedCLIArgs['max-show-issues']) : undefined,
50
- options: parsedCLIArgs['reporter-options'] ?? '',
51
- preprocessorOptions: parsedCLIArgs['preprocessor-options'] ?? '',
49
+ maxShowIssues: args['max-show-issues'] ? Number(args['max-show-issues']) : undefined,
50
+ options: args['reporter-options'] ?? '',
51
+ preprocessorOptions: args['preprocessor-options'] ?? '',
52
52
  };
53
- const finalData = await runPreprocessors(parsedCLIArgs.preprocessor ?? [], initialData);
54
- await runReporters(parsedCLIArgs.reporter ?? ['symbols'], finalData);
53
+ const finalData = await runPreprocessors(args.preprocessor ?? [], initialData);
54
+ await runReporters(args.reporter ?? ['symbols'], finalData);
55
55
  const totalErrorCount = Object.keys(finalData.report)
56
56
  .filter(reportGroup => finalData.report[reportGroup] && options.rules[reportGroup] === 'error')
57
57
  .reduce((errorCount, reportGroup) => errorCount + finalData.counters[reportGroup], 0);
@@ -59,27 +59,27 @@ const run = async () => {
59
59
  await perfObserver.finalize();
60
60
  if (perfObserver.isTimerifyFunctions)
61
61
  console.log(`\n${perfObserver.getTimerifiedFunctionsTable()}`);
62
- if (perfObserver.isMemoryUsageEnabled && !parsedCLIArgs['memory-realtime'])
62
+ if (perfObserver.isMemoryUsageEnabled && !args['memory-realtime'])
63
63
  console.log(`\n${perfObserver.getMemoryUsageTable()}`);
64
64
  if (perfObserver.isEnabled) {
65
65
  const duration = perfObserver.getCurrentDurationInMs();
66
66
  console.log('\nTotal running time:', prettyMilliseconds(duration));
67
67
  perfObserver.reset();
68
68
  }
69
- if (parsedCLIArgs['experimental-tags'] && parsedCLIArgs['experimental-tags'].length > 0) {
69
+ if (args['experimental-tags'] && args['experimental-tags'].length > 0) {
70
70
  logWarning('DEPRECATION WARNING', '--experimental-tags is deprecated, please start using --tags instead');
71
71
  }
72
72
  if (options.isIsolateWorkspaces && options.includedIssueTypes.classMembers) {
73
73
  logWarning('WARNING', 'Class members are not tracked when using the --isolate-workspaces flag');
74
74
  }
75
- if ((!parsedCLIArgs['no-exit-code'] && totalErrorCount > Number(parsedCLIArgs['max-issues'] ?? 0)) ||
75
+ if ((!args['no-exit-code'] && totalErrorCount > Number(args['max-issues'] ?? 0)) ||
76
76
  (!options.isDisableConfigHints && options.isTreatConfigHintsAsErrors && configurationHints.size > 0)) {
77
77
  process.exit(1);
78
78
  }
79
79
  }
80
80
  catch (error) {
81
81
  process.exitCode = 2;
82
- if (!parsedCLIArgs.debug && error instanceof Error && isKnownError(error)) {
82
+ if (!args.debug && error instanceof Error && isKnownError(error)) {
83
83
  const knownErrors = getKnownErrors(error);
84
84
  for (const knownError of knownErrors)
85
85
  logError('ERROR', knownError.message);
@@ -1,10 +1,9 @@
1
1
  import { createGraphExplorer } from '../graph-explorer/explorer.js';
2
2
  import { getIssueType, hasStrictlyEnumReferences } from '../graph-explorer/utils.js';
3
+ import traceReporter from '../reporters/trace.js';
3
4
  import { getPackageNameFromModuleSpecifier } from '../util/modules.js';
4
- import { toRelative } from '../util/path.js';
5
5
  import { findMatch } from '../util/regex.js';
6
6
  import { getShouldIgnoreHandler, getShouldIgnoreTagHandler } from '../util/tag.js';
7
- import { formatTrace } from '../util/trace.js';
8
7
  export const analyze = async ({ analyzedFiles, counselor, chief, collector, deputy, entryPaths, factory, graph, streamer, unreferencedFiles, options, }) => {
9
8
  const shouldIgnore = getShouldIgnoreHandler(options.isProduction);
10
9
  const shouldIgnoreTags = getShouldIgnoreTagHandler(options.tags);
@@ -228,20 +227,7 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
228
227
  collector.addConfigurationHint(hint);
229
228
  };
230
229
  await analyzeGraph();
231
- if (options.isTrace) {
232
- const nodes = explorer.buildExportsTree({ filePath: options.traceFile, identifier: options.traceExport });
233
- nodes.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.identifier.localeCompare(b.identifier));
234
- const toRel = (path) => toRelative(path, options.cwd);
235
- const isReferenced = (node) => {
236
- if (explorer.isReferenced(node.filePath, node.identifier, { includeEntryExports: false })[0])
237
- return true;
238
- if (explorer.hasStrictlyNsReferences(node.filePath, node.identifier)[0])
239
- return true;
240
- const exportItem = graph.get(node.filePath)?.exports.get(node.identifier);
241
- return exportItem ? isExportReferencedInFile(exportItem) : false;
242
- };
243
- for (const node of nodes)
244
- console.log(formatTrace(node, toRel, isReferenced(node)));
245
- }
230
+ if (options.isTrace)
231
+ traceReporter({ graph, explorer, options, isExportReferencedInFile });
246
232
  return analyzeGraph;
247
233
  };
@@ -2,12 +2,12 @@ import { _getInputsFromScripts } from '../binaries/index.js';
2
2
  import { getCompilerExtensions, getIncludedCompilers } from '../compilers/index.js';
3
3
  import { DEFAULT_EXTENSIONS, FOREIGN_FILE_EXTENSIONS, IS_DTS } from '../constants.js';
4
4
  import { partition } from '../util/array.js';
5
+ import { createInputHandler } from '../util/create-input-handler.js';
5
6
  import { debugLog, debugLogArray } from '../util/debug.js';
6
- import { getReferencedInputsHandler } from '../util/get-referenced-inputs.js';
7
7
  import { _glob, _syncGlob, negate, prependDirToPattern } from '../util/glob.js';
8
8
  import { isAlias, isConfig, isDeferResolveEntry, isDeferResolveProductionEntry, isEntry, isIgnore, isProductionEntry, isProject, toProductionEntry, } from '../util/input.js';
9
9
  import { loadTSConfig } from '../util/load-tsconfig.js';
10
- import { updateImportMap } from '../util/module-graph.js';
10
+ import { createFileNode, updateImportMap } from '../util/module-graph.js';
11
11
  import { getPackageNameFromModuleSpecifier, isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
12
12
  import { perfObserver } from '../util/Performance.js';
13
13
  import { getEntrySpecifiersFromManifest, getManifestImportDependencies } from '../util/package-json.js';
@@ -20,7 +20,8 @@ export async function build({ chief, collector, counselor, deputy, factory, isGi
20
20
  const toModuleSourceFilePath = getModuleSourcePathHandler(chief);
21
21
  const toSourceFilePaths = getToSourcePathsHandler(chief);
22
22
  const addIssue = (issue) => collector.addIssue(issue) && options.isWatch && collector.retainIssue(issue);
23
- const getReferencedInternalFilePath = getReferencedInputsHandler(deputy, chief, isGitIgnored, addIssue);
23
+ const externalRefsFromInputs = new Map();
24
+ const handleInput = createInputHandler(deputy, chief, isGitIgnored, addIssue, externalRefsFromInputs, options);
24
25
  for (const workspace of workspaces) {
25
26
  const { name, dir, manifestPath, manifestStr } = workspace;
26
27
  const manifest = chief.getManifestForWorkspace(name);
@@ -61,7 +62,7 @@ export async function build({ chief, collector, counselor, deputy, factory, isGi
61
62
  config,
62
63
  manifest,
63
64
  dependencies,
64
- getReferencedInternalFilePath: (input) => getReferencedInternalFilePath(input, workspace),
65
+ handleInput: (input) => handleInput(input, workspace),
65
66
  findWorkspaceByFilePath: chief.findWorkspaceByFilePath.bind(chief),
66
67
  negatedWorkspacePatterns: chief.getNegatedWorkspacePatterns(name),
67
68
  ignoredWorkspacePatterns: chief.getIgnoredWorkspacesFor(name),
@@ -150,7 +151,7 @@ export async function build({ chief, collector, counselor, deputy, factory, isGi
150
151
  }
151
152
  else if (!isConfig(input)) {
152
153
  const ws = (input.containingFilePath && chief.findWorkspaceByFilePath(input.containingFilePath)) || workspace;
153
- const resolvedFilePath = getReferencedInternalFilePath(input, ws);
154
+ const resolvedFilePath = handleInput(input, ws);
154
155
  if (resolvedFilePath) {
155
156
  if (isDeferResolveProductionEntry(input)) {
156
157
  addPattern(productionPatternsSkipExports, input, resolvedFilePath);
@@ -320,12 +321,16 @@ export async function build({ chief, collector, counselor, deputy, factory, isGi
320
321
  for (const input of inputs) {
321
322
  input.containingFilePath ??= filePath;
322
323
  input.dir ??= dir;
323
- const specifierFilePath = getReferencedInternalFilePath(input, workspace);
324
+ const specifierFilePath = handleInput(input, workspace);
324
325
  if (specifierFilePath)
325
326
  principal.addEntryPath(specifierFilePath, { skipExportsAnalysis: true });
326
327
  }
327
328
  }
328
329
  file.imports.unresolved = unresolvedImports;
330
+ const pluginRefs = externalRefsFromInputs.get(filePath);
331
+ if (pluginRefs)
332
+ for (const ref of pluginRefs)
333
+ file.imports.externalRefs.add(ref);
329
334
  const node = graph.get(filePath);
330
335
  if (node) {
331
336
  node.imports = file.imports;
@@ -380,6 +385,12 @@ export async function build({ chief, collector, counselor, deputy, factory, isGi
380
385
  }
381
386
  principals.length = 0;
382
387
  }
388
+ for (const [filePath, refs] of externalRefsFromInputs) {
389
+ if (!graph.has(filePath))
390
+ graph.set(filePath, createFileNode());
391
+ for (const ref of refs)
392
+ graph.get(filePath).imports.externalRefs.add(ref);
393
+ }
383
394
  return {
384
395
  graph,
385
396
  entryPaths,
@@ -7,7 +7,8 @@ export declare const createGraphExplorer: (graph: ModuleGraph, entryPaths: Set<s
7
7
  buildExportsTree: (options: {
8
8
  filePath?: string;
9
9
  identifier?: string;
10
- }) => import("./operations/build-exports-tree.js").TreeNode[];
10
+ }) => import("./operations/build-exports-tree.js").ExportsTreeNode[];
11
+ getDependencyUsage: (pattern?: string | RegExp) => Map<string, import("./operations/get-dependency-usage.js").DependencyNodes>;
11
12
  resolveDefinition: (filePath: string, identifier: string) => import("./operations/resolve-definition.js").DefinitionResult | null;
12
13
  getUsage: (filePath: string, identifier: string) => import("./operations/get-usage.js").UsageResult;
13
14
  findCycles: (filePath: string, maxDepth?: number) => import("../session/types.js").Cycle[];
@@ -2,6 +2,7 @@ import { invalidateCache as invalidateCacheInternal } from './cache.js';
2
2
  import { buildExportsTree } from './operations/build-exports-tree.js';
3
3
  import { findCycles } from './operations/find-cycles.js';
4
4
  import { getContention } from './operations/get-contention.js';
5
+ import { getDependencyUsage } from './operations/get-dependency-usage.js';
5
6
  import { getUsage } from './operations/get-usage.js';
6
7
  import { hasStrictlyNsReferences } from './operations/has-strictly-ns-references.js';
7
8
  import { isReferenced } from './operations/is-referenced.js';
@@ -11,6 +12,7 @@ export const createGraphExplorer = (graph, entryPaths) => {
11
12
  isReferenced: (filePath, identifier, options) => isReferenced(graph, entryPaths, filePath, identifier, options),
12
13
  hasStrictlyNsReferences: (filePath, identifier) => hasStrictlyNsReferences(graph, graph.get(filePath)?.imported, identifier),
13
14
  buildExportsTree: (options) => buildExportsTree(graph, entryPaths, options),
15
+ getDependencyUsage: (pattern) => getDependencyUsage(graph, pattern),
14
16
  resolveDefinition: (filePath, identifier) => resolveDefinition(graph, filePath, identifier),
15
17
  getUsage: (filePath, identifier) => getUsage(graph, entryPaths, filePath, identifier),
16
18
  findCycles: (filePath, maxDepth) => findCycles(graph, filePath, maxDepth),
@@ -1,15 +1,15 @@
1
1
  import type { Identifier, ModuleGraph } from '../../types/module-graph.js';
2
2
  import type { Via } from '../walk-down.js';
3
- export interface TreeNode {
3
+ export interface ExportsTreeNode {
4
4
  filePath: string;
5
5
  identifier: string;
6
6
  originalId: string | undefined;
7
7
  via: Via | undefined;
8
8
  refs: string[];
9
9
  isEntry: boolean;
10
- children: TreeNode[];
10
+ children: ExportsTreeNode[];
11
11
  }
12
12
  export declare const buildExportsTree: (graph: ModuleGraph, entryPaths: Set<string>, options: {
13
13
  filePath?: string;
14
14
  identifier?: Identifier;
15
- }) => TreeNode[];
15
+ }) => ExportsTreeNode[];
@@ -0,0 +1,14 @@
1
+ import type { ModuleGraph } from '../../types/module-graph.js';
2
+ export interface DependencyNode {
3
+ filePath: string;
4
+ specifier: string;
5
+ binaryName: string | undefined;
6
+ pos: number | undefined;
7
+ line: number | undefined;
8
+ col: number | undefined;
9
+ }
10
+ export interface DependencyNodes {
11
+ packageName: string;
12
+ imports: DependencyNode[];
13
+ }
14
+ export declare const getDependencyUsage: (graph: ModuleGraph, pattern?: string | RegExp) => Map<string, DependencyNodes>;
@@ -0,0 +1,38 @@
1
+ import { getPackageNameFromModuleSpecifier } from '../../util/modules.js';
2
+ export const getDependencyUsage = (graph, pattern) => {
3
+ const result = new Map();
4
+ const isMatch = (packageName, binaryName) => {
5
+ if (!pattern)
6
+ return true;
7
+ if (typeof pattern === 'string')
8
+ return packageName === pattern || binaryName === pattern;
9
+ return pattern.test(packageName) || (binaryName !== undefined && pattern.test(binaryName));
10
+ };
11
+ const addEntry = (packageName, filePath, specifier, binaryName, pos, line, col) => {
12
+ let entry = result.get(packageName);
13
+ if (!entry) {
14
+ entry = { packageName, imports: [] };
15
+ result.set(packageName, entry);
16
+ }
17
+ entry.imports.push({ filePath, specifier, binaryName, pos, line, col });
18
+ };
19
+ for (const [filePath, file] of graph) {
20
+ if (file.imports?.external) {
21
+ for (const _import of file.imports.external) {
22
+ const packageName = getPackageNameFromModuleSpecifier(_import.specifier);
23
+ if (packageName && isMatch(packageName)) {
24
+ addEntry(packageName, filePath, _import.specifier, undefined, _import.pos, _import.line, _import.col);
25
+ }
26
+ }
27
+ }
28
+ if (file.imports?.externalRefs) {
29
+ for (const ref of file.imports.externalRefs) {
30
+ const packageName = getPackageNameFromModuleSpecifier(ref.specifier);
31
+ if (packageName && isMatch(packageName, ref.identifier)) {
32
+ addEntry(packageName, filePath, ref.specifier, ref.identifier, undefined, undefined, undefined);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return result;
38
+ };
@@ -1,8 +1,9 @@
1
1
  import ts from 'typescript';
2
2
  import { getImportMap, getPropertyValues } from '../../typescript/ast-helpers.js';
3
3
  import { toDeferResolveProductionEntry } from '../../util/input.js';
4
+ import { dirname } from '../../util/path.js';
5
+ import { _resolveSync } from '../../util/resolve.js';
4
6
  export const getInputsFromHandlers = (sourceFile, options) => {
5
- const { getSourceFile, getReferencedInternalFilePath } = options;
6
7
  const entries = new Set();
7
8
  const importMap = getImportMap(sourceFile);
8
9
  function addHandlerSpecifiers(node) {
@@ -21,10 +22,9 @@ export const getInputsFromHandlers = (sourceFile, options) => {
21
22
  if (ts.isIdentifier(arg)) {
22
23
  const importPath = importMap.get(arg.text);
23
24
  if (importPath) {
24
- const input = toDeferResolveProductionEntry(importPath, { containingFilePath: options.configFilePath });
25
- const resolvedPath = getReferencedInternalFilePath(input);
25
+ const resolvedPath = _resolveSync(importPath, dirname(options.configFilePath));
26
26
  if (resolvedPath) {
27
- const stackFile = getSourceFile(resolvedPath);
27
+ const stackFile = options.getSourceFile(resolvedPath);
28
28
  if (stackFile)
29
29
  ts.forEachChild(stackFile, addHandlerSpecifiers);
30
30
  }
@@ -33,6 +33,7 @@ export default ({ report, issues, cwd, configurationHints, isDisableConfigHints,
33
33
  if (isReportType) {
34
34
  const title = reportMultipleGroups && getIssueTypeTitle(reportType);
35
35
  const issuesForType = Object.values(issues[reportType]).flatMap(Object.values);
36
+ issuesForType.sort((a, b) => a.filePath.localeCompare(b.filePath) || (a.line ?? 0) - (b.line ?? 0));
36
37
  if (issuesForType.length > 0) {
37
38
  title && core.info(`${title} (${issuesForType.length})`);
38
39
  for (const issue of issuesForType) {
@@ -56,7 +57,8 @@ export default ({ report, issues, cwd, configurationHints, isDisableConfigHints,
56
57
  if (!isDisableConfigHints && configurationHints.size > 0) {
57
58
  const CONFIG_HINTS_TITLE = 'Configuration hints';
58
59
  core.info(`${CONFIG_HINTS_TITLE} (${configurationHints.size})`);
59
- for (const hint of configurationHints) {
60
+ const sortedHints = [...configurationHints].sort((a, b) => (a.filePath ?? '').localeCompare(b.filePath ?? ''));
61
+ for (const hint of sortedHints) {
60
62
  const hintPrinter = hintPrinters.get(hint.type);
61
63
  const message = hintPrinter?.print({
62
64
  ...hint,
@@ -0,0 +1,11 @@
1
+ import type { GraphExplorer } from '../graph-explorer/explorer.js';
2
+ import type { Export, ExportMember, ModuleGraph } from '../types/module-graph.js';
3
+ import type { MainOptions } from '../util/create-options.js';
4
+ interface TraceReporterOptions {
5
+ graph: ModuleGraph;
6
+ explorer: GraphExplorer;
7
+ options: MainOptions;
8
+ isExportReferencedInFile: (exportedItem: Export | ExportMember) => boolean;
9
+ }
10
+ declare const _default: ({ graph, explorer, options, isExportReferencedInFile }: TraceReporterOptions) => void;
11
+ export default _default;
@@ -0,0 +1,45 @@
1
+ import pc from 'picocolors';
2
+ import { join, toRelative } from '../util/path.js';
3
+ import { toRegexOrString } from '../util/regex.js';
4
+ import { Table } from '../util/table.js';
5
+ import { formatTrace } from '../util/trace.js';
6
+ export default ({ graph, explorer, options, isExportReferencedInFile }) => {
7
+ if (options.traceDependency) {
8
+ const pattern = toRegexOrString(options.traceDependency);
9
+ const workspaceDir = options.workspace ? join(options.cwd, options.workspace) : undefined;
10
+ const toRel = (path) => toRelative(path, options.cwd);
11
+ const table = new Table({ truncateStart: ['filePath'] });
12
+ const seen = new Set();
13
+ for (const [packageName, { imports }] of explorer.getDependencyUsage(pattern)) {
14
+ const filtered = workspaceDir ? imports.filter(i => i.filePath.startsWith(workspaceDir)) : imports;
15
+ filtered.sort((a, b) => a.filePath.localeCompare(b.filePath) || (a.line ?? 0) - (b.line ?? 0));
16
+ for (const _import of filtered) {
17
+ const pos = _import.line ? `:${_import.line}:${_import.col}` : '';
18
+ const key = `${_import.filePath}${pos}:${packageName}`;
19
+ if (seen.has(key))
20
+ continue;
21
+ seen.add(key);
22
+ table.row();
23
+ table.cell('filePath', pc.whiteBright(`${toRel(_import.filePath)}${pos}`));
24
+ table.cell('package', pc.cyanBright(packageName));
25
+ }
26
+ }
27
+ for (const line of table.toRows())
28
+ console.log(line);
29
+ }
30
+ else {
31
+ const nodes = explorer.buildExportsTree({ filePath: options.traceFile, identifier: options.traceExport });
32
+ nodes.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.identifier.localeCompare(b.identifier));
33
+ const toRel = (path) => toRelative(path, options.cwd);
34
+ const isReferenced = (node) => {
35
+ if (explorer.isReferenced(node.filePath, node.identifier, { includeEntryExports: false })[0])
36
+ return true;
37
+ if (explorer.hasStrictlyNsReferences(node.filePath, node.identifier)[0])
38
+ return true;
39
+ const exportItem = graph.get(node.filePath)?.exports.get(node.identifier);
40
+ return exportItem ? isExportReferencedInFile(exportItem) : false;
41
+ };
42
+ for (const node of nodes)
43
+ console.log(formatTrace(node, toRel, isReferenced(node)));
44
+ }
45
+ };
@@ -3,4 +3,4 @@ import type { File } from './types.js';
3
3
  export interface FileDescriptorOptions {
4
4
  isShowContention?: boolean;
5
5
  }
6
- export declare const buildFileDescriptor: (filePath: string, cwd: string, graph: ModuleGraph, entryPaths: Set<string>, options?: FileDescriptorOptions) => File | null;
6
+ export declare const buildFileDescriptor: (filePath: string, cwd: string, graph: ModuleGraph, entryPaths: Set<string>, options?: FileDescriptorOptions) => File | undefined;
@@ -5,7 +5,7 @@ export const buildFileDescriptor = (filePath, cwd, graph, entryPaths, options =
5
5
  const absolutePath = toAbsolute(filePath, cwd);
6
6
  const node = graph.get(absolutePath);
7
7
  if (!node)
8
- return null;
8
+ return;
9
9
  const explorer = createGraphExplorer(graph, entryPaths);
10
10
  const metrics = { imports: 0, exports: 0, cycles: 0, contention: 0 };
11
11
  let t0 = performance.now();
@@ -1,4 +1,5 @@
1
1
  export { IMPORT_STAR, KNIP_CONFIG_LOCATIONS, SIDE_EFFECTS } from '../constants.js';
2
+ export type { DependencyNode, DependencyNodes } from '../graph-explorer/operations/get-dependency-usage.js';
2
3
  export { finalizeConfigurationHints } from '../reporters/util/configuration-hints.js';
3
4
  export { getIssuePrefix } from '../reporters/util/util.js';
4
5
  export type { Results } from '../run.js';
@@ -6,5 +7,6 @@ export type { Issue, Issues, IssueType, Rules } from '../types/issues.js';
6
7
  export type { PackageJson } from '../types/package-json.js';
7
8
  export { createOptions, type MainOptions } from '../util/create-options.js';
8
9
  export { buildFileDescriptor, type FileDescriptorOptions } from './file-descriptor.js';
10
+ export { buildPackageJsonDescriptor, type PackageJsonFile } from './package-json-descriptor.js';
9
11
  export { createSession, type Session } from './session.js';
10
12
  export type { ContentionDetails, Export, File, SourceLocation } from './types.js';
@@ -3,4 +3,5 @@ export { finalizeConfigurationHints } from '../reporters/util/configuration-hint
3
3
  export { getIssuePrefix } from '../reporters/util/util.js';
4
4
  export { createOptions } from '../util/create-options.js';
5
5
  export { buildFileDescriptor } from './file-descriptor.js';
6
+ export { buildPackageJsonDescriptor } from './package-json-descriptor.js';
6
7
  export { createSession } from './session.js';
@@ -0,0 +1,6 @@
1
+ import type { DependencyNodes } from '../graph-explorer/operations/get-dependency-usage.js';
2
+ import type { ModuleGraph } from '../types/module-graph.js';
3
+ export interface PackageJsonFile {
4
+ dependenciesUsage: Map<string, DependencyNodes>;
5
+ }
6
+ export declare const buildPackageJsonDescriptor: (graph: ModuleGraph, entryPaths: Set<string>) => PackageJsonFile;
@@ -0,0 +1,7 @@
1
+ import { createGraphExplorer } from '../graph-explorer/explorer.js';
2
+ export const buildPackageJsonDescriptor = (graph, entryPaths) => {
3
+ const explorer = createGraphExplorer(graph, entryPaths);
4
+ return {
5
+ dependenciesUsage: explorer.getDependencyUsage(),
6
+ };
7
+ };
@@ -4,6 +4,7 @@ import { type Results } from '../run.js';
4
4
  import type { MainOptions } from '../util/create-options.js';
5
5
  import type { WatchChange } from '../util/watch.js';
6
6
  import { type FileDescriptorOptions } from './file-descriptor.js';
7
+ import { type PackageJsonFile } from './package-json-descriptor.js';
7
8
  import type { File } from './types.js';
8
9
  type WatchUpdate = {
9
10
  duration: number;
@@ -14,7 +15,8 @@ export interface Session {
14
15
  getIssues(): CollectorIssues;
15
16
  getResults(): Results;
16
17
  getConfigurationHints(): ProcessedHint[];
17
- describeFile(filePath: string, options?: FileDescriptorOptions): File | null;
18
+ describeFile(filePath: string, options?: FileDescriptorOptions): File | undefined;
19
+ describePackageJson(): PackageJsonFile;
18
20
  }
19
21
  export declare const createSession: (options: MainOptions) => Promise<Session>;
20
22
  export {};
@@ -1,6 +1,7 @@
1
1
  import { finalizeConfigurationHints } from '../reporters/util/configuration-hints.js';
2
2
  import { run } from '../run.js';
3
3
  import { buildFileDescriptor } from './file-descriptor.js';
4
+ import { buildPackageJsonDescriptor } from './package-json-descriptor.js';
4
5
  export const createSession = async (options) => {
5
6
  const { session, results } = await run(options);
6
7
  if (!session)
@@ -14,5 +15,6 @@ const createSessionAdapter = (session, results, options) => {
14
15
  getResults: () => results,
15
16
  getConfigurationHints: () => finalizeConfigurationHints(results, options),
16
17
  describeFile: (filePath, opts) => buildFileDescriptor(filePath, options.cwd, session.getGraph(), session.getEntryPaths(), opts),
18
+ describePackageJson: () => buildPackageJsonDescriptor(session.getGraph(), session.getEntryPaths()),
17
19
  };
18
20
  };
@@ -96,10 +96,9 @@ export type IsLoadConfig = (options: PluginOptions, dependencies: Set<string>) =
96
96
  export type ResolveConfig<T = any> = (config: T, options: PluginOptions) => Promise<Input[]> | Input[];
97
97
  export type Resolve = (options: PluginOptions) => Promise<Input[]> | Input[];
98
98
  export type GetSourceFile = (filePath: string) => ts.SourceFile | undefined;
99
- export type GetReferencedInternalFilePath = (input: Input) => string | undefined;
99
+ export type HandleInput = (input: Input) => string | undefined;
100
100
  export type ResolveFromAST = (sourceFile: ts.SourceFile, options: PluginOptions & {
101
101
  getSourceFile: GetSourceFile;
102
- getReferencedInternalFilePath: GetReferencedInternalFilePath;
103
102
  }) => Input[];
104
103
  export interface Plugin {
105
104
  title: string;
@@ -30,6 +30,10 @@ export interface Import extends Position {
30
30
  readonly identifier: string | undefined;
31
31
  readonly isTypeOnly: boolean;
32
32
  }
33
+ export interface ExternalRef {
34
+ readonly specifier: string;
35
+ readonly identifier: string | undefined;
36
+ }
33
37
  export interface Export extends Position {
34
38
  readonly identifier: Identifier;
35
39
  readonly type: SymbolType;
@@ -58,6 +62,7 @@ export type FileNode = {
58
62
  imports: {
59
63
  readonly internal: ImportMap;
60
64
  readonly external: Set<Import>;
65
+ readonly externalRefs: Set<ExternalRef>;
61
66
  unresolved: Set<Import>;
62
67
  readonly programFiles: Set<FilePath>;
63
68
  readonly entryFiles: Set<FilePath>;
@@ -164,7 +164,7 @@ const getImportsAndExports = (sourceFile, resolveModule, typeChecker, options, i
164
164
  filePath,
165
165
  specifier: sanitizedSpecifier,
166
166
  identifier: opts.identifier ?? SIDE_EFFECTS,
167
- pos: opts.pos,
167
+ pos,
168
168
  line: line + 1,
169
169
  col: character + 2,
170
170
  isTypeOnly: !!(opts.modifiers & IMPORT_FLAGS.TYPE_ONLY),
@@ -445,7 +445,7 @@ const getImportsAndExports = (sourceFile, resolveModule, typeChecker, options, i
445
445
  item.symbol = undefined;
446
446
  }
447
447
  return {
448
- imports: { internal, external, programFiles, entryFiles, imports, unresolved },
448
+ imports: { internal, external, externalRefs: new Set(), programFiles, entryFiles, imports, unresolved },
449
449
  exports,
450
450
  duplicates: [...aliasedExports.values()],
451
451
  scripts,
@@ -24,8 +24,10 @@ const extractNamedEntries = (catalogs) => {
24
24
  const entries = new Set();
25
25
  if (catalogs && typeof catalogs === 'object') {
26
26
  for (const [catalogName, catalog] of Object.entries(catalogs)) {
27
- for (const name of Object.keys(catalog))
28
- entries.add(`${catalogName}:${name}`);
27
+ if (catalog && typeof catalog === 'object') {
28
+ for (const name of Object.keys(catalog))
29
+ entries.add(`${catalogName}:${name}`);
30
+ }
29
31
  }
30
32
  }
31
33
  return entries;
@@ -1,4 +1,4 @@
1
- export declare const helpText = "\u2702\uFE0F Find unused dependencies, exports and files 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|ts), knip.config.(js|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 test files, devDependencies)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --directory [dir] Run process from a different directory (default: cwd)\n --cache Enable caching\n --cache-location Change cache location (default: node_modules/.cache/knip)\n --watch Watch mode\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,binaries,unresolved,catalog\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --files Shortcut for --include files\n --fix Fix issues\n --fix-type Fix only issues of type, can be comma-separated or repeated (2)\n --format Format modified files after --fix using the local formatter\n --allow-remove-files Allow Knip to remove files (with --fix)\n --include-libs Include type definitions from external dependencies (default: false)\n --include-entry-exports Include entry files when reporting unused exports\n --isolate-workspaces Isolate workspaces into separate programs\n --use-tsconfig-files Use tsconfig.json to define project files (override `project` patterns)\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 --preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)\n --reporter Select reporter: symbols, compact, codeowners, json, codeclimate, markdown, disclosure, github-actions, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --tags Include or exclude tagged exports\n --no-config-hints Suppress configuration hints\n --treat-config-hints-as-errors Exit with non-zero code (1) if there are any configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of total issues before non-zero exit code (default: 0)\n --max-show-issues Maximum number of issues to display per type\n -d, --debug Show debug output\n --trace Show trace output\n --trace-export [name] Show trace output for named export(s)\n --trace-file [file] Show trace output for exports in file\n --performance Measure count and running time of key functions and display stats table\n --performance-fn [name] Measure only function [name]\n --memory Measure memory usage and display data table\n --memory-realtime Log memory usage in realtime\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, catalog\n(2) Fixable issue types: dependencies, exports, types, catalog\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 --tags=-lintignore\n\nWebsite: https://knip.dev";
1
+ export declare const helpText = "\u2702\uFE0F Find unused dependencies, exports and files 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|ts), knip.config.(js|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 test files, devDependencies)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --directory [dir] Run process from a different directory (default: cwd)\n --cache Enable caching\n --cache-location Change cache location (default: node_modules/.cache/knip)\n --watch Watch mode\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,binaries,unresolved,catalog\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --files Shortcut for --include files\n --fix Fix issues\n --fix-type Fix only issues of type, can be comma-separated or repeated (2)\n --format Format modified files after --fix using the local formatter\n --allow-remove-files Allow Knip to remove files (with --fix)\n --include-libs Include type definitions from external dependencies (default: false)\n --include-entry-exports Include entry files when reporting unused exports\n --isolate-workspaces Isolate workspaces into separate programs\n --use-tsconfig-files Use tsconfig.json to define project files (override `project` patterns)\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 --preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)\n --reporter Select reporter: symbols, compact, codeowners, json, codeclimate, markdown, disclosure, github-actions, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --tags Include or exclude tagged exports\n --no-config-hints Suppress configuration hints\n --treat-config-hints-as-errors Exit with non-zero code (1) if there are any configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of total issues before non-zero exit code (default: 0)\n --max-show-issues Maximum number of issues to display per type\n -d, --debug Show debug output\n --trace Show trace output\n --trace-dependency [name] Show files that import the named dependency\n --trace-export [name] Show trace output for named export(s)\n --trace-file [file] Show trace output for exports in file\n --performance Measure count and running time of key functions and display stats table\n --performance-fn [name] Measure only function [name]\n --memory Measure memory usage and display data table\n --memory-realtime Log memory usage in realtime\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, catalog\n(2) Fixable issue types: dependencies, exports, types, catalog\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 --tags=-lintignore\n\nWebsite: https://knip.dev";
2
2
  export type ParsedCLIArgs = ReturnType<typeof parseCLIArgs>;
3
3
  export default function parseCLIArgs(): {
4
4
  cache?: boolean | undefined;
@@ -39,6 +39,7 @@ export default function parseCLIArgs(): {
39
39
  'reporter-options'?: string | undefined;
40
40
  strict?: boolean | undefined;
41
41
  trace?: boolean | undefined;
42
+ 'trace-dependency'?: string | undefined;
42
43
  'trace-export'?: string | undefined;
43
44
  'trace-file'?: string | undefined;
44
45
  'treat-config-hints-as-errors'?: boolean | undefined;
@@ -37,9 +37,10 @@ Options:
37
37
  --treat-config-hints-as-errors Exit with non-zero code (1) if there are any configuration hints
38
38
  --no-exit-code Always exit with code zero (0)
39
39
  --max-issues Maximum number of total issues before non-zero exit code (default: 0)
40
- --max-show-issues Maximum number of issues to display per type
40
+ --max-show-issues Maximum number of issues to display per type
41
41
  -d, --debug Show debug output
42
42
  --trace Show trace output
43
+ --trace-dependency [name] Show files that import the named dependency
43
44
  --trace-export [name] Show trace output for named export(s)
44
45
  --trace-file [file] Show trace output for exports in file
45
46
  --performance Measure count and running time of key functions and display stats table
@@ -103,6 +104,7 @@ export default function parseCLIArgs() {
103
104
  'reporter-options': { type: 'string' },
104
105
  strict: { type: 'boolean' },
105
106
  trace: { type: 'boolean' },
107
+ 'trace-dependency': { type: 'string' },
106
108
  'trace-export': { type: 'string' },
107
109
  'trace-file': { type: 'string' },
108
110
  'treat-config-hints-as-errors': { type: 'boolean' },
@@ -0,0 +1,8 @@
1
+ import type { ConfigurationChief, Workspace } from '../ConfigurationChief.js';
2
+ import type { DependencyDeputy } from '../DependencyDeputy.js';
3
+ import type { Issue } from '../types/issues.js';
4
+ import type { ExternalRef } from '../types/module-graph.js';
5
+ import type { MainOptions } from './create-options.js';
6
+ import { type Input } from './input.js';
7
+ export type ExternalRefsFromInputs = Map<string, Set<ExternalRef>>;
8
+ export declare const createInputHandler: (deputy: DependencyDeputy, chief: ConfigurationChief, isGitIgnored: (filePath: string) => boolean, addIssue: (issue: Issue) => void, externalRefs: ExternalRefsFromInputs, options: MainOptions) => (input: Input, workspace: Workspace) => string | undefined;
@@ -7,15 +7,26 @@ import { _resolveSync } from './resolve.js';
7
7
  const getWorkspaceFor = (input, chief, workspace) => (input.dir && chief.findWorkspaceByFilePath(`${input.dir}/`)) ||
8
8
  (input.containingFilePath && chief.findWorkspaceByFilePath(input.containingFilePath)) ||
9
9
  workspace;
10
- export const getReferencedInputsHandler = (deputy, chief, isGitIgnored, addIssue) => (input, workspace) => {
10
+ const addExternalRef = (map, containingFilePath, ref) => {
11
+ if (!map.has(containingFilePath))
12
+ map.set(containingFilePath, new Set());
13
+ map.get(containingFilePath).add(ref);
14
+ };
15
+ export const createInputHandler = (deputy, chief, isGitIgnored, addIssue, externalRefs, options) => (input, workspace) => {
11
16
  const { specifier, containingFilePath } = input;
12
17
  if (!containingFilePath || IGNORED_RUNTIME_DEPENDENCIES.has(specifier))
13
18
  return;
14
19
  if (isBinary(input)) {
15
20
  const binaryName = fromBinary(input);
16
21
  const inputWorkspace = getWorkspaceFor(input, chief, workspace);
17
- const isHandled = deputy.maybeAddReferencedBinary(inputWorkspace, binaryName);
18
- if (isHandled || input.optional)
22
+ const dependencies = deputy.maybeAddReferencedBinary(inputWorkspace, binaryName);
23
+ if (dependencies) {
24
+ for (const dependency of dependencies) {
25
+ addExternalRef(externalRefs, containingFilePath, { specifier: dependency, identifier: binaryName });
26
+ }
27
+ return;
28
+ }
29
+ if (dependencies || input.optional)
19
30
  return;
20
31
  addIssue({
21
32
  type: 'binaries',
@@ -33,9 +44,12 @@ export const getReferencedInputsHandler = (deputy, chief, isGitIgnored, addIssue
33
44
  const inputWorkspace = getWorkspaceFor(input, chief, workspace);
34
45
  if (inputWorkspace) {
35
46
  const isHandled = deputy.maybeAddReferencedExternalDependency(inputWorkspace, packageName);
47
+ if (!isWorkspace) {
48
+ addExternalRef(externalRefs, containingFilePath, { specifier: packageName, identifier: undefined });
49
+ }
36
50
  if (isWorkspace || isDependency(input)) {
37
51
  if (!isHandled) {
38
- if (!input.optional && ((deputy.isProduction && input.production) || !deputy.isProduction)) {
52
+ if (!input.optional && ((options.isProduction && input.production) || !options.isProduction)) {
39
53
  addIssue({
40
54
  type: 'unlisted',
41
55
  filePath: containingFilePath,
@@ -87,7 +101,7 @@ export const getReferencedInputsHandler = (deputy, chief, isGitIgnored, addIssue
87
101
  });
88
102
  }
89
103
  else {
90
- debugLog(workspace.name, `Unable to resolve ${toDebugString(input, chief.cwd)}`);
104
+ debugLog(workspace.name, `Unable to resolve ${toDebugString(input, options.cwd)}`);
91
105
  }
92
106
  }
93
107
  };
@@ -1,7 +1,7 @@
1
1
  import type { Options } from '../types/options.js';
2
2
  import type { ParsedCLIArgs } from './cli-arguments.js';
3
3
  interface CreateOptions extends Partial<Options> {
4
- parsedCLIArgs?: ParsedCLIArgs;
4
+ args?: ParsedCLIArgs;
5
5
  }
6
6
  export declare const createOptions: (options: CreateOptions) => Promise<{
7
7
  cacheLocation: string;
@@ -1327,6 +1327,7 @@ export declare const createOptions: (options: CreateOptions) => Promise<{
1327
1327
  _files: import("../types/issues.js").IssueSeverity;
1328
1328
  };
1329
1329
  tags: import("../types/options.js").Tags;
1330
+ traceDependency: string | undefined;
1330
1331
  traceExport: string | undefined;
1331
1332
  traceFile: string | undefined;
1332
1333
  tsConfigFile: string | undefined;
@@ -13,25 +13,25 @@ import { isAbsolute, join, normalize, toAbsolute, toPosix } from './path.js';
13
13
  import { splitTags } from './tag.js';
14
14
  const pcwd = process.cwd();
15
15
  export const createOptions = async (options) => {
16
- const { parsedCLIArgs = {} } = options;
17
- const cwd = normalize(toPosix(toAbsolute(options.cwd ?? parsedCLIArgs.directory ?? pcwd, pcwd)));
16
+ const { args = {} } = options;
17
+ const cwd = normalize(toPosix(toAbsolute(options.cwd ?? args.directory ?? pcwd, pcwd)));
18
18
  const manifestPath = findFile(cwd, 'package.json');
19
19
  const manifest = manifestPath && (await loadJSON(manifestPath));
20
20
  if (!(manifestPath && manifest)) {
21
21
  throw new ConfigurationError('Unable to find package.json');
22
22
  }
23
23
  let configFilePath;
24
- for (const configPath of parsedCLIArgs.config ? [parsedCLIArgs.config] : KNIP_CONFIG_LOCATIONS) {
24
+ for (const configPath of args.config ? [args.config] : KNIP_CONFIG_LOCATIONS) {
25
25
  const resolvedConfigFilePath = isAbsolute(configPath) ? configPath : findFile(cwd, configPath);
26
26
  if (resolvedConfigFilePath) {
27
27
  configFilePath = resolvedConfigFilePath;
28
28
  break;
29
29
  }
30
30
  }
31
- if (parsedCLIArgs.config && !configFilePath && !manifest.knip) {
32
- throw new ConfigurationError(`Unable to find ${parsedCLIArgs.config} or package.json#knip`);
31
+ if (args.config && !configFilePath && !manifest.knip) {
32
+ throw new ConfigurationError(`Unable to find ${args.config} or package.json#knip`);
33
33
  }
34
- const loadedConfig = Object.assign({}, manifest.knip, configFilePath ? await loadResolvedConfigFile(configFilePath, parsedCLIArgs) : {});
34
+ const loadedConfig = Object.assign({}, manifest.knip, configFilePath ? await loadResolvedConfigFile(configFilePath, args) : {});
35
35
  const parsedConfig = knipConfigurationSchema.parse(partitionCompilers(loadedConfig));
36
36
  if (!configFilePath && manifest.knip)
37
37
  configFilePath = manifestPath;
@@ -43,58 +43,58 @@ export const createOptions = async (options) => {
43
43
  ? manifest.workspaces
44
44
  : (manifest.workspaces.packages ?? [])
45
45
  : []);
46
- const isStrict = options.isStrict ?? parsedCLIArgs.strict ?? false;
47
- const isProduction = options.isProduction ?? parsedCLIArgs.production ?? isStrict;
48
- const isDebug = parsedCLIArgs.debug ?? false;
49
- const isTrace = Boolean(parsedCLIArgs.trace ?? parsedCLIArgs['trace-file'] ?? parsedCLIArgs['trace-export']);
46
+ const isStrict = options.isStrict ?? args.strict ?? false;
47
+ const isProduction = options.isProduction ?? args.production ?? isStrict;
48
+ const isDebug = args.debug ?? false;
49
+ const isTrace = Boolean(args.trace ?? args['trace-file'] ?? args['trace-export'] ?? args['trace-dependency']);
50
50
  const rules = { ...defaultRules, ...parsedConfig.rules };
51
51
  const excludesFromRules = getKeysByValue(rules, 'off');
52
52
  const includedIssueTypes = getIncludedIssueTypes({
53
53
  isProduction,
54
54
  exclude: [...excludesFromRules, ...(parsedConfig.exclude ?? [])],
55
55
  include: parsedConfig.include ?? [],
56
- excludeOverrides: options.excludedIssueTypes ?? parsedCLIArgs.exclude ?? [],
56
+ excludeOverrides: options.excludedIssueTypes ?? args.exclude ?? [],
57
57
  includeOverrides: [
58
- ...(options.includedIssueTypes ?? parsedCLIArgs.include ?? []),
59
- ...(parsedCLIArgs.dependencies ? shorthandDeps : []),
60
- ...(parsedCLIArgs.exports ? shorthandExports : []),
61
- ...(parsedCLIArgs.files ? shorthandFiles : []),
58
+ ...(options.includedIssueTypes ?? args.include ?? []),
59
+ ...(args.dependencies ? shorthandDeps : []),
60
+ ...(args.exports ? shorthandExports : []),
61
+ ...(args.files ? shorthandFiles : []),
62
62
  ],
63
63
  });
64
64
  for (const [key, value] of Object.entries(includedIssueTypes)) {
65
65
  if (!value)
66
66
  rules[key] = 'off';
67
67
  }
68
- const fixTypes = options.fixTypes ?? parsedCLIArgs['fix-type'] ?? [];
69
- const isFixFiles = parsedCLIArgs['allow-remove-files'] && (fixTypes.length === 0 || fixTypes.includes('files'));
70
- const isIncludeLibs = parsedCLIArgs['include-libs'] ?? options.isIncludeLibs ?? false;
68
+ const fixTypes = options.fixTypes ?? args['fix-type'] ?? [];
69
+ const isFixFiles = args['allow-remove-files'] && (fixTypes.length === 0 || fixTypes.includes('files'));
70
+ const isIncludeLibs = args['include-libs'] ?? options.isIncludeLibs ?? false;
71
71
  const isReportClassMembers = includedIssueTypes.classMembers;
72
- const tags = splitTags(parsedCLIArgs.tags ?? options.tags ?? parsedConfig.tags ?? parsedCLIArgs['experimental-tags'] ?? []);
72
+ const tags = splitTags(args.tags ?? options.tags ?? parsedConfig.tags ?? args['experimental-tags'] ?? []);
73
73
  return {
74
- cacheLocation: parsedCLIArgs['cache-location'] ?? join(cwd, 'node_modules', '.cache', 'knip'),
74
+ cacheLocation: args['cache-location'] ?? join(cwd, 'node_modules', '.cache', 'knip'),
75
75
  catalog: await getCatalogContainer(cwd, manifest, manifestPath, pnpmWorkspacePath, pnpmWorkspace),
76
- config: parsedCLIArgs.config,
76
+ config: args.config,
77
77
  configFilePath,
78
78
  cwd,
79
- dependencies: parsedCLIArgs.dependencies ?? false,
79
+ dependencies: args.dependencies ?? false,
80
80
  experimentalTags: tags,
81
- exports: parsedCLIArgs.exports ?? false,
82
- files: parsedCLIArgs.files ?? false,
81
+ exports: args.exports ?? false,
82
+ files: args.files ?? false,
83
83
  fixTypes,
84
- gitignore: parsedCLIArgs['no-gitignore'] ? false : (options.gitignore ?? true),
84
+ gitignore: args['no-gitignore'] ? false : (options.gitignore ?? true),
85
85
  includedIssueTypes,
86
- isCache: parsedCLIArgs.cache ?? false,
86
+ isCache: args.cache ?? false,
87
87
  isDebug,
88
- isDisableConfigHints: parsedCLIArgs['no-config-hints'] || isProduction || Boolean(parsedCLIArgs.workspace),
89
- isFix: parsedCLIArgs.fix ?? options.isFix ?? isFixFiles ?? fixTypes.length > 0,
88
+ isDisableConfigHints: args['no-config-hints'] || isProduction || Boolean(args.workspace),
89
+ isFix: args.fix ?? options.isFix ?? isFixFiles ?? fixTypes.length > 0,
90
90
  isFixCatalog: fixTypes.length === 0 || fixTypes.includes('catalog'),
91
91
  isFixDependencies: fixTypes.length === 0 || fixTypes.includes('dependencies'),
92
92
  isFixFiles,
93
93
  isFixUnusedExports: fixTypes.length === 0 || fixTypes.includes('exports'),
94
94
  isFixUnusedTypes: fixTypes.length === 0 || fixTypes.includes('types'),
95
- isFormat: parsedCLIArgs.format ?? options.isFormat ?? false,
96
- isIncludeEntryExports: parsedCLIArgs['include-entry-exports'] ?? options.isIncludeEntryExports ?? false,
97
- isIsolateWorkspaces: options.isIsolateWorkspaces ?? parsedCLIArgs['isolate-workspaces'] ?? false,
95
+ isFormat: args.format ?? options.isFormat ?? false,
96
+ isIncludeEntryExports: args['include-entry-exports'] ?? options.isIncludeEntryExports ?? false,
97
+ isIsolateWorkspaces: options.isIsolateWorkspaces ?? args['isolate-workspaces'] ?? false,
98
98
  isProduction,
99
99
  isReportClassMembers,
100
100
  isReportDependencies: includedIssueTypes.dependencies ||
@@ -106,24 +106,25 @@ export const createOptions = async (options) => {
106
106
  isSession: options.isSession ?? false,
107
107
  isShowProgress: !isDebug &&
108
108
  !isTrace &&
109
- parsedCLIArgs['no-progress'] !== true &&
109
+ args['no-progress'] !== true &&
110
110
  options.isShowProgress !== false &&
111
111
  process.stdout.isTTY &&
112
112
  typeof process.stdout.cursorTo === 'function',
113
113
  isSkipLibs: !(isIncludeLibs || includedIssueTypes.classMembers),
114
114
  isStrict,
115
115
  isTrace,
116
- isTreatConfigHintsAsErrors: parsedCLIArgs['treat-config-hints-as-errors'] ?? parsedConfig.treatConfigHintsAsErrors ?? false,
117
- isUseTscFiles: options.isUseTscFiles ?? parsedCLIArgs['use-tsconfig-files'] ?? (options.isSession && !configFilePath),
118
- isWatch: parsedCLIArgs.watch ?? options.isWatch ?? false,
119
- maxShowIssues: parsedCLIArgs['max-show-issues'] ? Number(parsedCLIArgs['max-show-issues']) : undefined,
116
+ isTreatConfigHintsAsErrors: args['treat-config-hints-as-errors'] ?? parsedConfig.treatConfigHintsAsErrors ?? false,
117
+ isUseTscFiles: options.isUseTscFiles ?? args['use-tsconfig-files'] ?? (options.isSession && !configFilePath),
118
+ isWatch: args.watch ?? options.isWatch ?? false,
119
+ maxShowIssues: args['max-show-issues'] ? Number(args['max-show-issues']) : undefined,
120
120
  parsedConfig,
121
121
  rules,
122
122
  tags,
123
- traceExport: parsedCLIArgs['trace-export'],
124
- traceFile: parsedCLIArgs['trace-file'] ? toAbsolute(parsedCLIArgs['trace-file'], cwd) : undefined,
125
- tsConfigFile: parsedCLIArgs.tsConfig,
126
- workspace: options.workspace ?? parsedCLIArgs.workspace,
123
+ traceDependency: args['trace-dependency'],
124
+ traceExport: args['trace-export'],
125
+ traceFile: args['trace-file'] ? toAbsolute(args['trace-file'], cwd) : undefined,
126
+ tsConfigFile: args.tsConfig,
127
+ workspace: options.workspace ?? args.workspace,
127
128
  workspaces,
128
129
  };
129
130
  };
@@ -1,5 +1,6 @@
1
1
  import type { FileNode, IdToFileMap, IdToNsToFileMap, ImportMap, ImportMaps, ModuleGraph } from '../types/module-graph.js';
2
2
  export declare const updateImportMap: (file: FileNode, importMap: ImportMap, graph: ModuleGraph) => void;
3
+ export declare const createFileNode: () => FileNode;
3
4
  export declare const createImports: () => ImportMaps;
4
5
  export declare const addValue: (map: IdToFileMap, id: string, value: string) => void;
5
6
  export declare const addNsValue: (map: IdToNsToFileMap, id: string, ns: string, value: string) => void;
@@ -28,10 +28,11 @@ export const updateImportMap = (file, importMap, graph) => {
28
28
  graph.set(importedFilePath, importedFile);
29
29
  }
30
30
  };
31
- const createFileNode = () => ({
31
+ export const createFileNode = () => ({
32
32
  imports: {
33
33
  internal: new Map(),
34
34
  external: new Set(),
35
+ externalRefs: new Set(),
35
36
  unresolved: new Set(),
36
37
  programFiles: new Set(),
37
38
  entryFiles: new Set(),
@@ -1,2 +1,2 @@
1
- import type { TreeNode } from '../graph-explorer/operations/build-exports-tree.js';
2
- export declare const formatTrace: (node: TreeNode, toRelative: (path: string) => string, isReferenced: boolean) => string;
1
+ import type { ExportsTreeNode } from '../graph-explorer/operations/build-exports-tree.js';
2
+ export declare const formatTrace: (node: ExportsTreeNode, toRelative: (path: string) => string, isReferenced: boolean) => string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "5.75.1";
1
+ export declare const version = "5.76.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '5.75.1';
1
+ export const version = '5.76.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "5.75.1",
3
+ "version": "5.76.0",
4
4
  "description": "Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects",
5
5
  "homepage": "https://knip.dev",
6
6
  "repository": {
@@ -1,5 +0,0 @@
1
- import type { ConfigurationChief, Workspace } from '../ConfigurationChief.js';
2
- import type { DependencyDeputy } from '../DependencyDeputy.js';
3
- import type { Issue } from '../types/issues.js';
4
- import { type Input } from './input.js';
5
- export declare const getReferencedInputsHandler: (deputy: DependencyDeputy, chief: ConfigurationChief, isGitIgnored: (s: string) => boolean, addIssue: (issue: Issue) => void) => (input: Input, workspace: Workspace) => string | undefined;