knip 0.9.1 → 0.11.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
@@ -83,9 +83,10 @@ Knip works by creating two sets of files:
83
83
  --ignore Ignore files matching this glob pattern (can be set multiple times)
84
84
  --no-gitignore Don't use .gitignore
85
85
  --dev Include `devDependencies` in report(s)
86
+ --include-entry-files Report unused exports and types for entry files
86
87
  --no-progress Don't show dynamic progress updates
87
88
  --max-issues Maximum number of issues before non-zero exit code (default: 0)
88
- --reporter Select reporter: symbols, compact, codeowners (default: symbols)
89
+ --reporter Select reporter: symbols, compact, codeowners, json (default: symbols)
89
90
  --reporter-options Pass extra options to the reporter (as JSON string, see example)
90
91
  --jsdoc Enable JSDoc parsing, with options: public
91
92
  --debug Show debug output
@@ -125,10 +126,12 @@ following types of issues:
125
126
  - `nsTypes` - Unused types in namespaces: did not find direct references to this exported variable (2)
126
127
  - `duplicates` - Duplicate exports: the same thing is exported more than once with different names from the same file
127
128
 
128
- 1. This may also include dependencies that could not be resolved properly (such as non-relative `local/dir/file.ts` not
129
- and `local` not being in `node_modules`).
129
+ 1. This includes dependencies that could not be resolved. For instance, what does `unresolved/dir/module` mean?
130
+ - To target something in the (missing) `node_modules/unresolved` package?
131
+ - Target a local module that should have a relative path?
132
+ - It does not match any `paths` entry in `tsconfig.json#compilerOptions`.
130
133
  2. The variable or type is not referenced directly, and has become a member of a namespace. That's why Knip is not sure
131
- whether this export can be removed, so please look into it:
134
+ whether this export can be removed, so please look into it.
132
135
 
133
136
  You can `--include` or `--exclude` any of the types to slice & dice the report to your needs. Alternatively, they can be
134
137
  added to the configuration (e.g. `"exclude": ["dependencies"]`).
@@ -140,8 +143,8 @@ As always, make sure to backup files or use Git before deleting files or making
140
143
  - Unused files can be removed.
141
144
  - Unused dependencies can be removed from `package.json`.
142
145
  - Unlisted dependencies should be added to `package.json`.
143
- - Unused exports and types: remove the `export` keyword in front of unused exports. Then you (or tools such as the
144
- TypeScript language server in VS Code and/or ESLint) can see whether the variable or type is used within the same
146
+ - Unused exports and types: remove the `export` keyword in front of unused exports. Then you (or tools such as
147
+ TypeScript language services in VS Code and/or ESLint) can see whether the variable or type is used within the same
145
148
  file. If this is not the case, it can be removed.
146
149
 
147
150
  🔁 Repeat the process to reveal new unused files and exports. Sometimes it's so liberating to remove things!
@@ -307,10 +310,23 @@ per file like this:
307
310
 
308
311
  ```json
309
312
  [
313
+ {
314
+ "file": "package.json",
315
+ "owners": ["@org/admin"],
316
+ "files": false,
317
+ "dependencies": ["jquery", "moment"],
318
+ "devDependencies": [],
319
+ "unlisted": [],
320
+ "exports": [],
321
+ "types": [],
322
+ "duplicates": []
323
+ },
310
324
  {
311
325
  "file": "src/Registration.tsx",
312
326
  "owners": ["@org/owner"],
313
327
  "files": true,
328
+ "dependencies": [],
329
+ "devDependencies": [],
314
330
  "unlisted": ["react"],
315
331
  "exports": ["lowercaseFirstLetter", "RegistrationBox"],
316
332
  "types": ["RegistrationServices", "RegistrationAction"],
package/dist/help.js CHANGED
@@ -16,7 +16,7 @@ Options:
16
16
  --include-entry-files Report unused exports and types for entry files
17
17
  --no-progress Don't show dynamic progress updates
18
18
  --max-issues Maximum number of issues before non-zero exit code (default: 0)
19
- --reporter Select reporter: symbols, compact, codeowners (default: symbols)
19
+ --reporter Select reporter: symbols, compact, codeowners, json (default: symbols)
20
20
  --reporter-options Pass extra options to the reporter (as JSON string, see example)
21
21
  --jsdoc Enable JSDoc parsing, with options: public
22
22
  --debug Show debug output
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { UnresolvedConfiguration } from './types';
2
- export declare const main: (options: UnresolvedConfiguration) => Promise<{
2
+ export declare const main: (unresolvedConfiguration: UnresolvedConfiguration) => Promise<{
3
3
  report: import("./types").Report;
4
4
  issues: import("./types").Issues;
5
5
  counters: import("./types").Counters;
package/dist/index.js CHANGED
@@ -8,18 +8,22 @@ const typescript_1 = __importDefault(require("typescript"));
8
8
  const config_1 = require("./util/config");
9
9
  const fs_1 = require("./util/fs");
10
10
  const path_1 = require("./util/path");
11
+ const glob_1 = require("./util/glob");
11
12
  const project_1 = require("./util/project");
12
13
  const runner_1 = require("./runner");
13
14
  const errors_1 = require("./util/errors");
14
15
  const debug_1 = require("./util/debug");
15
16
  const progress_1 = require("./progress");
16
- const main = async (options) => {
17
- const { cwd, workingDir, configFilePath: configFilePathArg, tsConfigFilePath: tsConfigFilePathArg, include, exclude, ignore, gitignore, isIncludeEntryFiles, isDev, isShowProgress, jsDoc, debug, } = options;
18
- const updateMessage = (0, progress_1.getMessageUpdater)(options);
19
- (0, debug_1.debugLogObject)(options, 1, 'Unresolved configuration', options);
17
+ const main = async (unresolvedConfiguration) => {
18
+ const { cwd, workingDir, configFilePath: configFilePathArg, tsConfigFilePath: tsConfigFilePathArg, include, exclude, ignore, gitignore, isIncludeEntryFiles, isDev, isShowProgress, jsDoc, debug, } = unresolvedConfiguration;
19
+ const updateMessage = (0, progress_1.getMessageUpdater)(unresolvedConfiguration);
20
+ (0, debug_1.debugLogObject)(debug, 1, 'Unresolved configuration', unresolvedConfiguration);
20
21
  updateMessage('Reading configuration and manifest files...');
21
22
  const manifestPath = await (0, fs_1.findFile)(cwd, workingDir, 'package.json');
22
23
  const manifest = manifestPath && require(manifestPath);
24
+ if (!manifestPath || !manifest) {
25
+ throw new errors_1.ConfigurationError('Unable to find package.json');
26
+ }
23
27
  const configFilePath = configFilePathArg ?? 'knip.json';
24
28
  const resolvedConfigFilePath = await (0, fs_1.findFile)(cwd, workingDir, configFilePath);
25
29
  const localConfig = resolvedConfigFilePath && require(resolvedConfigFilePath);
@@ -31,10 +35,10 @@ const main = async (options) => {
31
35
  if (tsConfigFilePathArg && !resolvedTsConfigFilePath) {
32
36
  throw new errors_1.ConfigurationError(`Unable to find ${tsConfigFilePathArg}`);
33
37
  }
34
- let tsConfigPaths = [];
38
+ let tsConfigPathGlobs = [];
35
39
  if (resolvedTsConfigFilePath) {
36
40
  const config = typescript_1.default.readConfigFile(resolvedTsConfigFilePath, typescript_1.default.sys.readFile);
37
- tsConfigPaths = config.config.compilerOptions?.paths
41
+ tsConfigPathGlobs = config.config.compilerOptions?.paths
38
42
  ? Object.keys(config.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
39
43
  : [];
40
44
  if (config.error) {
@@ -43,7 +47,7 @@ const main = async (options) => {
43
47
  }
44
48
  const dir = (0, path_1.relative)(workingDir);
45
49
  const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfig, { workingDir: dir, isDev });
46
- (0, debug_1.debugLogObject)(options, 1, 'Resolved configuration', resolvedConfig);
50
+ (0, debug_1.debugLogObject)(debug, 1, 'Resolved configuration', resolvedConfig);
47
51
  if (!resolvedConfigFilePath && !manifest.knip && !resolvedTsConfigFilePath) {
48
52
  throw new errors_1.ConfigurationError(`Unable to find ${configFilePath} or package.json#knip or ${tsConfigFilePath}`);
49
53
  }
@@ -54,30 +58,32 @@ const main = async (options) => {
54
58
  ? { tsConfigFilePath: resolvedTsConfigFilePath }
55
59
  : { compilerOptions: { allowJs: true } };
56
60
  updateMessage('Resolving entry files...');
57
- const entryPaths = await (0, path_1.resolvePaths)({
61
+ const entryPaths = await (0, glob_1.glob)({
62
+ cwd,
58
63
  workingDir,
59
64
  patterns: resolvedConfig.entryFiles,
60
65
  ignore,
61
66
  gitignore,
62
67
  });
63
- (0, debug_1.debugLogFiles)(options, 1, 'Globbed entry paths', entryPaths);
68
+ (0, debug_1.debugLogFiles)(debug, 1, 'Globbed entry paths', entryPaths);
64
69
  const production = (0, project_1.createProject)({ ...projectOptions, ...skipAddFiles }, entryPaths);
65
70
  const entryFiles = production.getSourceFiles();
66
- (0, debug_1.debugLogSourceFiles)(options, 1, 'Included entry source files', entryFiles);
71
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Resolved entry source files', entryFiles);
67
72
  production.resolveSourceFileDependencies();
68
- const productionFiles = production.getSourceFiles();
69
- (0, debug_1.debugLogSourceFiles)(options, 1, 'Included production source files', productionFiles);
73
+ const productionFiles = (0, project_1.removeExternalSourceFiles)(production);
74
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Resolved production source files', productionFiles);
70
75
  updateMessage('Resolving project files...');
71
- const projectPaths = await (0, path_1.resolvePaths)({
76
+ const projectPaths = await (0, glob_1.glob)({
77
+ cwd,
72
78
  workingDir,
73
79
  patterns: resolvedConfig.projectFiles,
74
80
  ignore,
75
81
  gitignore,
76
82
  });
77
- (0, debug_1.debugLogFiles)(options, 1, 'Globbed project paths', projectPaths);
83
+ (0, debug_1.debugLogFiles)(debug, 1, 'Globbed project paths', projectPaths);
78
84
  const project = (0, project_1.createProject)({ ...projectOptions, ...skipAddFiles }, projectPaths);
79
85
  const projectFiles = project.getSourceFiles();
80
- (0, debug_1.debugLogSourceFiles)(options, 1, 'Included project source files', projectFiles);
86
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Resolved project source files', projectFiles);
81
87
  return { entryFiles, productionFiles, projectFiles };
82
88
  }
83
89
  else {
@@ -95,12 +101,13 @@ const main = async (options) => {
95
101
  productionFiles,
96
102
  projectFiles,
97
103
  isIncludeEntryFiles: !resolvedConfig || isIncludeEntryFiles,
104
+ manifestPath,
98
105
  dependencies: Object.keys(manifest.dependencies ?? {}),
99
106
  peerDependencies: Object.keys(manifest.peerDependencies ?? {}),
100
107
  optionalDependencies: Object.keys(manifest.optionalDependencies ?? {}),
101
108
  devDependencies: Object.keys(manifest.devDependencies ?? {}),
102
109
  isDev: Boolean(resolvedConfig?.dev),
103
- tsConfigPaths,
110
+ tsConfigPathGlobs: tsConfigPathGlobs,
104
111
  isShowProgress,
105
112
  jsDocOptions: {
106
113
  isReadPublicTag: jsDoc.includes('public'),
@@ -108,7 +115,7 @@ const main = async (options) => {
108
115
  debug,
109
116
  };
110
117
  const { issues, counters } = await (0, runner_1.findIssues)(config);
111
- (0, debug_1.debugLogObject)(options, 2, 'Issues', issues);
118
+ (0, debug_1.debugLogObject)(debug, 2, 'Issues', issues);
112
119
  return { report, issues, counters };
113
120
  };
114
121
  exports.main = main;
@@ -26,6 +26,8 @@ exports.default = async ({ report, issues, options }) => {
26
26
  file,
27
27
  ...(codeownersEngine && { owners: codeownersEngine.calcFileOwnership(file) }),
28
28
  ...(report.files && { files: false }),
29
+ ...(report.dependencies && { dependencies: [] }),
30
+ ...(report.devDependencies && { devDependencies: [] }),
29
31
  ...(report.unlisted && { unlisted: [] }),
30
32
  ...((report.exports || report.nsExports) && { exports: [] }),
31
33
  ...((report.types || report.nsTypes) && { types: [] }),
package/dist/runner.js CHANGED
@@ -13,20 +13,21 @@ const dependencies_1 = require("./util/dependencies");
13
13
  const debug_1 = require("./util/debug");
14
14
  const progress_1 = require("./progress");
15
15
  async function findIssues(configuration) {
16
- const { workingDir, report, isDev, jsDocOptions } = configuration;
16
+ const { workingDir, report, isDev, jsDocOptions, debug } = configuration;
17
17
  const { entryFiles, productionFiles, projectFiles, isIncludeEntryFiles } = configuration;
18
+ const { manifestPath } = configuration;
18
19
  const updateMessage = (0, progress_1.getMessageUpdater)(configuration);
19
20
  const { getUnresolvedDependencies, getUnusedDependencies, getUnusedDevDependencies } = (0, dependencies_1.getDependencyAnalyzer)(configuration);
20
21
  const [usedProductionFiles, unreferencedProductionFiles] = (0, project_1.partitionSourceFiles)(projectFiles, productionFiles);
21
22
  const [usedEntryFiles, usedNonEntryFiles] = (0, project_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
22
- (0, debug_1.debugLogSourceFiles)(configuration, 1, 'Used production files', usedProductionFiles);
23
- (0, debug_1.debugLogSourceFiles)(configuration, 1, 'Unreferenced production files', unreferencedProductionFiles);
24
- (0, debug_1.debugLogSourceFiles)(configuration, 1, 'Used entry files', usedEntryFiles);
25
- (0, debug_1.debugLogSourceFiles)(configuration, 1, 'Used non-entry files', usedNonEntryFiles);
23
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Used production files', usedProductionFiles);
24
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Unreferenced production files', unreferencedProductionFiles);
25
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Used entry files', usedEntryFiles);
26
+ (0, debug_1.debugLogSourceFiles)(debug, 1, 'Used non-entry files', usedNonEntryFiles);
26
27
  const issues = {
27
28
  files: new Set(unreferencedProductionFiles.map(file => file.getFilePath())),
28
- dependencies: new Set(),
29
- devDependencies: new Set(),
29
+ dependencies: {},
30
+ devDependencies: {},
30
31
  unlisted: {},
31
32
  exports: {},
32
33
  types: {},
@@ -36,8 +37,8 @@ async function findIssues(configuration) {
36
37
  };
37
38
  const counters = {
38
39
  files: issues.files.size,
39
- dependencies: issues.dependencies.size,
40
- devDependencies: issues.dependencies.size,
40
+ dependencies: 0,
41
+ devDependencies: 0,
41
42
  unlisted: 0,
42
43
  exports: 0,
43
44
  types: 0,
@@ -50,19 +51,12 @@ async function findIssues(configuration) {
50
51
  const updateCounters = (0, progress_1.getCountersUpdater)(configuration, counters);
51
52
  const addSymbolIssue = (issueType, issue) => {
52
53
  const { filePath, symbol } = issue;
53
- const key = node_path_1.default.relative(workingDir, filePath);
54
+ const key = node_path_1.default.relative(workingDir, filePath).replace(/\\/g, '/');
54
55
  issues[issueType][key] = issues[issueType][key] ?? {};
55
56
  issues[issueType][key][symbol] = issue;
56
57
  counters[issueType]++;
57
58
  updateCounters(issue);
58
59
  };
59
- const addProjectIssue = (issueType, issue) => {
60
- if (!issues[issueType].has(issue.symbol)) {
61
- issues[issueType].add(issue.symbol);
62
- counters[issueType]++;
63
- }
64
- updateCounters(issue);
65
- };
66
60
  updateMessage('Connecting the dots...');
67
61
  if (report.dependencies ||
68
62
  report.unlisted ||
@@ -174,10 +168,10 @@ async function findIssues(configuration) {
174
168
  }
175
169
  if (report.dependencies) {
176
170
  const unusedDependencies = getUnusedDependencies();
177
- unusedDependencies.forEach(symbol => addProjectIssue('dependencies', { filePath: '', symbol }));
171
+ unusedDependencies.forEach(symbol => addSymbolIssue('dependencies', { filePath: manifestPath, symbol }));
178
172
  if (isDev) {
179
173
  const unusedDevDependencies = getUnusedDevDependencies();
180
- unusedDevDependencies.forEach(symbol => addProjectIssue('devDependencies', { filePath: '', symbol }));
174
+ unusedDevDependencies.forEach(symbol => addSymbolIssue('devDependencies', { filePath: manifestPath, symbol }));
181
175
  }
182
176
  }
183
177
  updateCounters();
package/dist/types.d.ts CHANGED
@@ -10,8 +10,8 @@ export declare type IssueSet = Set<string>;
10
10
  export declare type IssueRecords = Record<string, Record<string, Issue>>;
11
11
  export declare type Issues = {
12
12
  files: IssueSet;
13
- dependencies: IssueSet;
14
- devDependencies: IssueSet;
13
+ dependencies: IssueRecords;
14
+ devDependencies: IssueRecords;
15
15
  unlisted: IssueRecords;
16
16
  exports: IssueRecords;
17
17
  types: IssueRecords;
@@ -20,8 +20,7 @@ export declare type Issues = {
20
20
  duplicates: IssueRecords;
21
21
  };
22
22
  export declare type IssueType = keyof Issues;
23
- export declare type ProjectIssueType = Extract<IssueType, 'files' | 'dependencies' | 'devDependencies'>;
24
- export declare type SymbolIssueType = Exclude<IssueType, ProjectIssueType>;
23
+ export declare type SymbolIssueType = Exclude<IssueType, 'files'>;
25
24
  export declare type Report = {
26
25
  [key in keyof Issues]: boolean;
27
26
  };
@@ -60,12 +59,13 @@ export declare type Configuration = {
60
59
  productionFiles: SourceFile[];
61
60
  entryFiles: SourceFile[];
62
61
  isIncludeEntryFiles: boolean;
62
+ manifestPath: string;
63
63
  dependencies: string[];
64
64
  peerDependencies: string[];
65
65
  optionalDependencies: string[];
66
66
  devDependencies: string[];
67
67
  isDev: boolean;
68
- tsConfigPaths: string[];
68
+ tsConfigPathGlobs: string[];
69
69
  isShowProgress: boolean;
70
70
  jsDocOptions: {
71
71
  isReadPublicTag: boolean;
@@ -1,6 +1,6 @@
1
- import type { ImportedConfiguration, LocalConfiguration } from '../types';
1
+ import type { ImportedConfiguration, LocalConfiguration, Report } from '../types';
2
2
  export declare const resolveConfig: (importedConfiguration: ImportedConfiguration, options?: {
3
3
  workingDir?: string;
4
4
  isDev?: boolean;
5
5
  }) => LocalConfiguration | undefined;
6
- export declare const resolveIncludedIssueTypes: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => import("../types").Report;
6
+ export declare const resolveIncludedIssueTypes: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => Report;
@@ -31,15 +31,13 @@ exports.resolveConfig = resolveConfig;
31
31
  const resolveIncludedIssueTypes = (includeArg, excludeArg, resolvedConfig) => {
32
32
  const deps = resolvedConfig?.dev ? ['dependencies', 'devDependencies'] : ['dependencies'];
33
33
  const groups = ['files', ...deps, 'unlisted', 'exports', 'types', 'nsExports', 'nsTypes', 'duplicates'];
34
- const include = [includeArg, resolvedConfig?.include ?? []]
35
- .flat()
36
- .map(value => value.split(','))
37
- .flat();
38
- const exclude = [excludeArg, resolvedConfig?.exclude ?? []]
39
- .flat()
40
- .map(value => value.split(','))
41
- .flat();
42
- const includes = (include.length > 0 ? include : groups).filter(group => !exclude.includes(group));
43
- return groups.reduce((r, group) => ((r[group] = includes.includes(group)), r), {});
34
+ const normalizedIncludesArg = includeArg.map(value => value.split(',')).flat();
35
+ const normalizedExcludesArg = excludeArg.map(value => value.split(',')).flat();
36
+ const excludes = (resolvedConfig?.exclude ?? []).filter(exclude => !normalizedIncludesArg.includes(exclude));
37
+ const includes = (resolvedConfig?.include ?? []).filter(include => !normalizedExcludesArg.includes(include));
38
+ const include = [normalizedIncludesArg, includes].flat();
39
+ const exclude = [normalizedExcludesArg, excludes].flat();
40
+ const included = (include.length > 0 ? include : groups).filter(group => !exclude.includes(group));
41
+ return groups.reduce((types, group) => ((types[group] = included.includes(group)), types), {});
44
42
  };
45
43
  exports.resolveIncludedIssueTypes = resolveIncludedIssueTypes;
@@ -1,11 +1,9 @@
1
1
  import type { SourceFile } from 'ts-morph';
2
- declare type Config = {
3
- debug: {
4
- isEnabled: boolean;
5
- level: number;
6
- };
2
+ declare type Debug = {
3
+ isEnabled: boolean;
4
+ level: number;
7
5
  };
8
- export declare const debugLogObject: (config: Config, minimumLevel: number, name: string, obj: unknown) => void;
9
- export declare const debugLogFiles: (config: Config, minimumLevel: number, name: string, filePaths: string[]) => void;
10
- export declare const debugLogSourceFiles: (config: Config, minimumLevel: number, name: string, sourceFiles: SourceFile[]) => void;
6
+ export declare const debugLogObject: (debug: Debug, minimumLevel: number, name: string, obj: unknown) => void;
7
+ export declare const debugLogFiles: (debug: Debug, minimumLevel: number, name: string, filePaths: string[]) => void;
8
+ export declare const debugLogSourceFiles: (debug: Debug, minimumLevel: number, name: string, sourceFiles: SourceFile[]) => void;
11
9
  export {};
@@ -6,41 +6,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.debugLogSourceFiles = exports.debugLogFiles = exports.debugLogObject = void 0;
7
7
  const node_util_1 = __importDefault(require("node:util"));
8
8
  const logArray = (collection) => console.log(node_util_1.default.inspect(collection, { maxArrayLength: null }));
9
- const debugLogObject = (config, minimumLevel, name, obj) => {
10
- if (minimumLevel > config.debug.level)
9
+ const debugLogObject = (debug, minimumLevel, name, obj) => {
10
+ if (minimumLevel > debug.level)
11
11
  return;
12
12
  console.log(`[knip] ${name}:`);
13
13
  console.log(node_util_1.default.inspect(obj, { depth: null, colors: true }));
14
14
  };
15
15
  exports.debugLogObject = debugLogObject;
16
- const debugLogFiles = (config, minimumLevel, name, filePaths) => {
17
- if (minimumLevel > config.debug.level)
16
+ const debugLogFiles = (debug, minimumLevel, name, filePaths) => {
17
+ if (minimumLevel > debug.level)
18
18
  return;
19
- const { debug } = config;
19
+ console.debug(`[knip] ${name} (${filePaths.length}):`);
20
20
  if (debug.level > 1) {
21
- console.debug(`[knip] ${name} (${filePaths.length}):`);
22
21
  logArray(filePaths);
23
22
  }
24
- else {
25
- console.debug(`[knip] ${name} (${filePaths.length})`);
26
- }
27
23
  };
28
24
  exports.debugLogFiles = debugLogFiles;
29
- const debugLogSourceFiles = (config, minimumLevel, name, sourceFiles) => {
30
- if (minimumLevel > config.debug.level)
25
+ const debugLogSourceFiles = (debug, minimumLevel, name, sourceFiles) => {
26
+ if (minimumLevel > debug.level)
31
27
  return;
32
- const { debug } = config;
28
+ console.debug(`[knip] ${name} (${sourceFiles.length})`);
33
29
  if (debug.level > 1) {
34
- console.debug(`[knip] ${name} (${sourceFiles.length}):`);
35
- logArray(sourceFiles.map(sourceFile => sourceFile.getFilePath()));
36
- }
37
- else {
38
- console.debug(`[knip] ${name} (${sourceFiles.length})`);
30
+ const files = sourceFiles.map(sourceFile => sourceFile.getFilePath());
31
+ logArray(files);
39
32
  }
40
33
  };
41
34
  exports.debugLogSourceFiles = debugLogSourceFiles;
42
- const debugLogDiff = (config, minimumLevel, name, arrA, arrB) => {
43
- if (minimumLevel > config.debug.level)
35
+ const debugLogDiff = (debug, minimumLevel, name, arrA, arrB) => {
36
+ if (minimumLevel > debug.level)
44
37
  return;
45
38
  const onlyInA = arrA.filter(itemA => !arrB.includes(itemA)).sort();
46
39
  const onlyInB = arrB.filter(itemB => !arrA.includes(itemB)).sort();
@@ -7,29 +7,35 @@ exports.getDependencyAnalyzer = void 0;
7
7
  const ts_morph_1 = require("ts-morph");
8
8
  const is_builtin_module_1 = __importDefault(require("is-builtin-module"));
9
9
  const micromatch_1 = __importDefault(require("micromatch"));
10
+ const ts_morph_helpers_1 = require("ts-morph-helpers");
10
11
  const compact = (collection) => Array.from(new Set(collection)).filter((value) => Boolean(value));
12
+ const findRequireModuleSpecifiers = (sourceFile) => (0, ts_morph_helpers_1.findCallExpressionsByName)(sourceFile, 'require').map(expression => expression.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.StringLiteral));
13
+ const isExternalDependency = (moduleSpecifier, tsConfigPathGlobs) => {
14
+ if (moduleSpecifier.startsWith('.'))
15
+ return false;
16
+ if ((0, is_builtin_module_1.default)(moduleSpecifier))
17
+ return false;
18
+ if (tsConfigPathGlobs.length > 0 && micromatch_1.default.isMatch(moduleSpecifier, tsConfigPathGlobs))
19
+ return false;
20
+ return true;
21
+ };
22
+ const resolvePackageName = (moduleSpecifier) => {
23
+ const parts = moduleSpecifier.split('/').slice(0, 2);
24
+ return moduleSpecifier.startsWith('@') ? parts.join('/') : parts[0];
25
+ };
11
26
  const getDependencyAnalyzer = (configuration) => {
12
- const { dependencies, devDependencies, peerDependencies, optionalDependencies, tsConfigPaths } = configuration;
27
+ const { dependencies, devDependencies, peerDependencies, optionalDependencies, tsConfigPathGlobs } = configuration;
13
28
  const productionDependencies = [...dependencies, ...peerDependencies, ...optionalDependencies];
14
29
  const referencedDependencies = new Set();
15
30
  const getUnresolvedDependencies = (sourceFile) => {
16
31
  const unresolvedDependencies = new Set();
17
32
  const importLiterals = sourceFile.getImportStringLiterals();
18
- const requires = sourceFile
19
- .getDescendantsOfKind(ts_morph_1.ts.SyntaxKind.CallExpression)
20
- .filter(callExpression => callExpression.getExpression().getText() === 'require')
21
- .map(expression => expression.getFirstDescendantByKind(ts_morph_1.ts.SyntaxKind.StringLiteral));
22
- const literals = compact([importLiterals, requires].flat());
23
- literals.forEach(importLiteral => {
24
- const moduleSpecifier = importLiteral.getLiteralText();
25
- if (moduleSpecifier.startsWith('.'))
26
- return;
27
- if ((0, is_builtin_module_1.default)(moduleSpecifier))
28
- return;
29
- if (tsConfigPaths.length > 0 && micromatch_1.default.isMatch(moduleSpecifier, tsConfigPaths))
33
+ const requireCallExpressions = findRequireModuleSpecifiers(sourceFile);
34
+ const moduleSpecifiers = compact([importLiterals, requireCallExpressions].flat()).map(i => i.getLiteralText());
35
+ moduleSpecifiers.forEach(moduleSpecifier => {
36
+ if (!isExternalDependency(moduleSpecifier, tsConfigPathGlobs))
30
37
  return;
31
- const parts = moduleSpecifier.split('/').slice(0, 2);
32
- const packageName = moduleSpecifier.startsWith('@') ? parts.join('/') : parts[0];
38
+ const packageName = resolvePackageName(moduleSpecifier);
33
39
  if (!productionDependencies.includes(packageName) && !devDependencies.includes(packageName)) {
34
40
  unresolvedDependencies.add({ filePath: sourceFile.getFilePath(), symbol: moduleSpecifier });
35
41
  }
@@ -0,0 +1,7 @@
1
+ export declare const glob: ({ cwd, workingDir, patterns, ignore, gitignore, }: {
2
+ cwd: string;
3
+ workingDir: string;
4
+ patterns: string[];
5
+ ignore: string[];
6
+ gitignore: boolean;
7
+ }) => Promise<string[]>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.glob = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ let _globby;
9
+ const globProxy = async function (patterns, options) {
10
+ if (!_globby) {
11
+ const { globby } = await eval('import("globby")');
12
+ _globby = globby;
13
+ }
14
+ return _globby(patterns, options);
15
+ };
16
+ const prependDirToPattern = (workingDir, pattern) => {
17
+ if (pattern.startsWith('!'))
18
+ return '!' + node_path_1.default.posix.join(workingDir, pattern.slice(1));
19
+ return node_path_1.default.posix.join(workingDir, pattern);
20
+ };
21
+ const glob = async ({ cwd, workingDir, patterns, ignore, gitignore, }) => {
22
+ return globProxy(patterns.map(pattern => prependDirToPattern(node_path_1.default.posix.relative(cwd, workingDir), pattern)), {
23
+ cwd,
24
+ ignore: [...ignore, '**/node_modules/**'],
25
+ gitignore,
26
+ absolute: true,
27
+ });
28
+ };
29
+ exports.glob = glob;
@@ -1,7 +1 @@
1
1
  export declare const relative: (to: string) => string;
2
- export declare const resolvePaths: ({ workingDir, patterns, ignore, gitignore, }: {
3
- workingDir: string;
4
- patterns: string[];
5
- ignore: string[];
6
- gitignore: boolean;
7
- }) => Promise<string[]>;
package/dist/util/path.js CHANGED
@@ -3,28 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolvePaths = exports.relative = void 0;
6
+ exports.relative = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const cwd = process.cwd();
9
9
  const relative = (to) => node_path_1.default.relative(cwd, to);
10
10
  exports.relative = relative;
11
- let _globby;
12
- const glob = async function (patterns, options) {
13
- if (!_globby) {
14
- const { globby } = await eval('import("globby")');
15
- _globby = globby;
16
- }
17
- return _globby(patterns, options);
18
- };
19
- const prependDirToPattern = (workingDir, pattern) => {
20
- if (pattern.startsWith('!'))
21
- return '!' + node_path_1.default.join(workingDir, pattern.slice(1));
22
- return node_path_1.default.join(workingDir, pattern);
23
- };
24
- const resolvePaths = async ({ workingDir, patterns, ignore, gitignore, }) => glob(patterns.map(pattern => prependDirToPattern((0, exports.relative)(workingDir), pattern)), {
25
- cwd,
26
- ignore,
27
- gitignore,
28
- absolute: true,
29
- });
30
- exports.resolvePaths = resolvePaths;
@@ -1,4 +1,5 @@
1
1
  import { Project } from 'ts-morph';
2
2
  import type { ProjectOptions, SourceFile } from 'ts-morph';
3
3
  export declare const createProject: (projectOptions: ProjectOptions, paths?: string[]) => Project;
4
+ export declare const removeExternalSourceFiles: (project: Project) => SourceFile[];
4
5
  export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
@@ -1,14 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.partitionSourceFiles = exports.createProject = void 0;
3
+ exports.partitionSourceFiles = exports.removeExternalSourceFiles = exports.createProject = void 0;
4
4
  const ts_morph_1 = require("ts-morph");
5
5
  const createProject = (projectOptions, paths) => {
6
- const workspace = new ts_morph_1.Project(projectOptions);
6
+ const project = new ts_morph_1.Project(projectOptions);
7
7
  if (paths)
8
- workspace.addSourceFilesAtPaths(paths);
9
- return workspace;
8
+ project.addSourceFilesAtPaths(paths);
9
+ return project;
10
10
  };
11
11
  exports.createProject = createProject;
12
+ const removeExternalSourceFiles = (project) => project.getSourceFiles().filter(sourceFile => {
13
+ if (/\/node_modules\//.test(sourceFile.getFilePath())) {
14
+ project.removeSourceFile(sourceFile);
15
+ return false;
16
+ }
17
+ return true;
18
+ });
19
+ exports.removeExternalSourceFiles = removeExternalSourceFiles;
12
20
  const partitionSourceFiles = (projectFiles, productionFiles) => {
13
21
  const productionFilePaths = productionFiles.map(file => file.getFilePath());
14
22
  const usedFiles = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Find unused files, dependencies and exports in your TypeScript and JavaScript project",
5
5
  "keywords": [
6
6
  "find",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "scripts": {
27
27
  "knip": "node ./dist/cli.js --jsdoc public",
28
- "test": "node --loader tsx --test test/*.spec.ts",
28
+ "test": "globstar -- node --loader tsx --test \"test/*.spec.ts\"",
29
29
  "watch": "tsc --watch",
30
30
  "build": "rm -rf dist && tsc",
31
31
  "prepublishOnly": "npm test && npm run build && npm run knip",
@@ -50,6 +50,7 @@
50
50
  "devDependencies": {
51
51
  "@types/micromatch": "4.0.2",
52
52
  "@types/node": "18.11.2",
53
+ "globstar": "1.0.0",
53
54
  "prettier": "2.7.1",
54
55
  "release-it": "15.5.0",
55
56
  "tsx": "3.10.3",