knip 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # ✂️ Knip
2
2
 
3
- Knip scans your TypeScript projects for **unused files and exports**. For comparison, ESLint finds unused variables
4
- inside files in isolation, but this will not be flagged:
3
+ Knip scans your JavaScript and TypeScript projects for **unused files, dependencies and exports**. Things that should be
4
+ eliminated. Less code means better performance and less to maintain, important for both UX and DX!
5
+
6
+ For comparison, ESLint finds unused variables inside files in isolation, but this will not be flagged:
5
7
 
6
8
  ```ts
7
9
  export const myVar = true;
8
10
  ```
9
11
 
10
- Unused files will also not be detected by ESLint. So how do you know which files and exports are no longer used? This
11
- requires an analysis of all the right files in the project.
12
+ Unused files will also not be detected by ESLint. So how do you know which files, dependencies and exports are no longer
13
+ used? This requires an analysis of all the right files in the project.
12
14
 
13
15
  This is where Knip comes in:
14
16
 
@@ -17,7 +19,7 @@ This is where Knip comes in:
17
19
  - [x] Finds dependencies not listed in `package.json`.
18
20
  - [x] Finds duplicate exports of the same symbol.
19
21
  - [x] Supports JavaScript inside TypeScript projects (`"allowJs": true`)
20
- - [ ] Supports JavaScript-only projects with CommonJS and ESM (no `tsconfig.json`) - TODO
22
+ - [x] Supports JavaScript-only projects using ESM (without a `tsconfig.json`)
21
23
 
22
24
  Knip really shines in larger projects where you have non-production files (such as `/docs`, `/tools` and `/scripts`).
23
25
  The `includes` setting in `tsconfig.json` is often too broad, resulting in too many false negatives. Similar projects
@@ -78,7 +80,8 @@ knip [options]
78
80
 
79
81
  Options:
80
82
  -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
81
- --cwd Working directory (default: current working directory)
83
+ -t/--tsConfig [file] TypeScript configuration path (default: ./tsconfig.json)
84
+ --dir Working directory (default: current working directory)
82
85
  --include Report only listed issue group(s) (see below)
83
86
  --exclude Exclude issue group(s) from report (see below)
84
87
  --dev Include `devDependencies` in report(s) (default: false)
@@ -92,7 +95,7 @@ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes,
92
95
  Examples:
93
96
 
94
97
  $ knip
95
- $ knip --cwd packages/client --include files
98
+ $ knip --dir packages/client --include files
96
99
  $ knip -c ./knip.js --reporter compact --jsdoc public
97
100
 
98
101
  More info: https://github.com/webpro/knip
@@ -143,8 +146,10 @@ Excluding non-production files from the `projectFiles` allows Knip to understand
143
146
 
144
147
  Non-production code includes files such as end-to-end tests, tooling, scripts, Storybook stories, etc.
145
148
 
149
+ Think of it the same way as you would split `dependencies` and `devDependencies` in `package.json`.
150
+
146
151
  To include both production and test files to analyze the project as a whole, include both sets of files to `entryFiles`,
147
- and add `dev: true`:
152
+ and add `dev: true` to a file named such as `knip.dev.json`:
148
153
 
149
154
  ```json
150
155
  {
@@ -154,7 +159,29 @@ and add `dev: true`:
154
159
  }
155
160
  ```
156
161
 
157
- Knip will now report unused files and exports for the combined set of files as configured in `entryFiles`.
162
+ Use `-c knip.dev.json` and unused files and exports for the combined set of files as configured in `entryFiles` will be
163
+ reported.
164
+
165
+ An alternative way to store `dev` configuration is in this example `package.json`:
166
+
167
+ ```json
168
+ {
169
+ "name": "my-package",
170
+ "scripts": {
171
+ "knip": "knip"
172
+ },
173
+ "knip": {
174
+ "entryFiles": ["src/index.ts"],
175
+ "projectFiles": ["src/**/*.ts", "!**/*.spec.ts"],
176
+ "dev": {
177
+ "entryFiles": ["src/index.ts", "src/**/*.spec.ts", "src/**/*.e2e.ts"],
178
+ "projectFiles": ["src/**/*.ts"]
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ This way, the `--dev` flag will use the `dev` options (and also add `devDependencies` to the `dependencies` report).
158
185
 
159
186
  ## More configuration examples
160
187
 
@@ -179,8 +206,8 @@ Packages can also be explicitly configured per package directory.
179
206
  To scan the packages separately, using the first match from the configuration file:
180
207
 
181
208
  ```
182
- knip --cwd packages/client --config knip.json
183
- knip --cwd packages/services --config knip.json
209
+ knip --cwd packages/client
210
+ knip --cwd packages/services
184
211
  ```
185
212
 
186
213
  #### Connected projects
@@ -217,10 +244,14 @@ can be tweaked further to the project structure.
217
244
  ### Default reporter
218
245
 
219
246
  ```
220
- $ knip --config ./knip.json
247
+ $ knip
221
248
  --- UNUSED FILES (2)
222
249
  src/chat/helpers.ts
223
250
  src/components/SideBar.tsx
251
+ --- UNUSED DEPENDENCIES (1)
252
+ moment
253
+ --- UNLISTED DEPENDENCIES (1)
254
+ react
224
255
  --- UNUSED EXPORTS (5)
225
256
  lowercaseFirstLetter src/common/src/string/index.ts
226
257
  RegistrationBox src/components/Registration.tsx
@@ -240,10 +271,14 @@ ProductsList, default src/components/Products.tsx
240
271
  ### Compact
241
272
 
242
273
  ```
243
- $ knip --config ./knip.json --reporter compact
274
+ $ knip --reporter compact
244
275
  --- UNUSED FILES (2)
245
276
  src/chat/helpers.ts
246
277
  src/components/SideBar.tsx
278
+ --- UNUSED DEPENDENCIES (1)
279
+ moment
280
+ --- UNLISTED DEPENDENCIES (1)
281
+ react
247
282
  --- UNUSED EXPORTS (4)
248
283
  src/common/src/string/index.ts: lowercaseFirstLetter
249
284
  src/components/Registration.tsx: RegistrationBox
package/dist/cli.js CHANGED
@@ -12,11 +12,12 @@ const config_1 = require("./util/config");
12
12
  const path_1 = require("./util/path");
13
13
  const reporters_1 = __importDefault(require("./reporters"));
14
14
  const _1 = require(".");
15
- const { values: { help, cwd: cwdArg, config: configFilePath = 'knip.json', include = [], exclude = [], dev: isDev = false, 'no-progress': noProgress = false, reporter = 'symbols', jsdoc = [], 'max-issues': maxIssues = '0', }, } = (0, node_util_1.parseArgs)({
15
+ const { values: { help, dir, config: configFilePath = 'knip.json', tsConfig: tsConfigFilePath, include = [], exclude = [], dev: isDev = false, 'no-progress': noProgress = false, reporter = 'symbols', jsdoc = [], 'max-issues': maxIssues = '0', }, } = (0, node_util_1.parseArgs)({
16
16
  options: {
17
17
  help: { type: 'boolean' },
18
18
  config: { type: 'string', short: 'c' },
19
- cwd: { type: 'string' },
19
+ tsConfig: { type: 'string', short: 't' },
20
+ dir: { type: 'string' },
20
21
  include: { type: 'string', multiple: true },
21
22
  exclude: { type: 'string', multiple: true },
22
23
  dev: { type: 'boolean' },
@@ -31,48 +32,51 @@ if (help) {
31
32
  process.exit(0);
32
33
  }
33
34
  const cwd = process.cwd();
34
- const workingDir = cwdArg ? node_path_1.default.resolve(cwdArg) : cwd;
35
+ const workingDir = dir ? node_path_1.default.resolve(dir) : cwd;
35
36
  const isShowProgress = noProgress === false ? process.stdout.isTTY && typeof process.stdout.cursorTo === 'function' : !noProgress;
36
37
  const printReport = reporter in reporters_1.default ? reporters_1.default[reporter] : require(node_path_1.default.join(workingDir, reporter));
37
38
  const main = async () => {
38
39
  const localConfigurationPath = await (0, path_1.findFile)(workingDir, configFilePath);
39
40
  const manifestPath = await (0, path_1.findFile)(workingDir, 'package.json');
40
- if (!manifestPath || !localConfigurationPath) {
41
+ const localConfiguration = localConfigurationPath && require(localConfigurationPath);
42
+ const manifest = manifestPath && require(manifestPath);
43
+ if (!localConfigurationPath && !manifest.knip) {
41
44
  const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
42
- console.error(`Unable to find ${configFilePath} (or package.json#knip) in ${location}\n`);
45
+ console.error(`Unable to find ${configFilePath} or package.json#knip in ${location}\n`);
43
46
  (0, help_1.printHelp)();
44
47
  process.exit(1);
45
48
  }
46
- const localConfiguration = require(localConfigurationPath);
47
- const manifest = require(manifestPath);
48
- const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfiguration, cwdArg);
49
+ const resolvedConfig = (0, config_1.resolveConfig)(manifest.knip ?? localConfiguration, { workingDir: dir, isDev });
49
50
  if (!resolvedConfig) {
50
51
  (0, help_1.printHelp)();
51
52
  process.exit(1);
52
53
  }
53
54
  const report = (0, config_1.resolveIncludedIssueGroups)(include, exclude, resolvedConfig);
54
- const tsConfigFile = await (0, path_1.findFile)(workingDir, 'tsconfig.json');
55
- if (!tsConfigFile) {
56
- const location = workingDir === cwd ? 'current directory' : `${node_path_1.default.relative(cwd, workingDir)} or up.`;
57
- console.error(`Unable to find ${tsConfigFile} or package.json in ${location}\n`);
55
+ let tsConfigPaths = [];
56
+ const tsConfigPath = await (0, path_1.findFile)(workingDir, tsConfigFilePath ?? 'tsconfig.json');
57
+ if (tsConfigFilePath && !tsConfigPath) {
58
+ console.error(`Unable to find ${tsConfigFilePath}\n`);
58
59
  (0, help_1.printHelp)();
59
60
  process.exit(1);
60
61
  }
61
- const tsConfig = typescript_1.default.readConfigFile(tsConfigFile, typescript_1.default.sys.readFile);
62
- const tsConfigPaths = tsConfig.config.compilerOptions?.paths
63
- ? Object.keys(tsConfig.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
64
- : [];
65
- if (tsConfig.error) {
66
- console.error(`An error occured when reading ${node_path_1.default.relative(cwd, tsConfigFile)}.\n`);
67
- (0, help_1.printHelp)();
68
- process.exit(1);
62
+ if (tsConfigPath) {
63
+ const tsConfig = typescript_1.default.readConfigFile(tsConfigPath, typescript_1.default.sys.readFile);
64
+ tsConfigPaths = tsConfig.config.compilerOptions?.paths
65
+ ? Object.keys(tsConfig.config.compilerOptions.paths).map(p => p.replace(/\*/g, '**'))
66
+ : [];
67
+ if (tsConfig.error) {
68
+ console.error(`An error occured when reading ${node_path_1.default.relative(cwd, tsConfigPath)}.\n`);
69
+ (0, help_1.printHelp)();
70
+ process.exit(1);
71
+ }
69
72
  }
70
73
  const config = {
71
74
  workingDir,
72
75
  report,
73
76
  dependencies: Object.keys(manifest.dependencies ?? {}),
74
77
  devDependencies: Object.keys(manifest.devDependencies ?? {}),
75
- isDev: resolvedConfig.dev ?? isDev,
78
+ isDev: typeof resolvedConfig.dev === 'boolean' ? resolvedConfig.dev : isDev,
79
+ tsConfigFilePath,
76
80
  tsConfigPaths,
77
81
  isShowProgress,
78
82
  jsDocOptions: {
package/dist/help.js CHANGED
@@ -6,7 +6,8 @@ const printHelp = () => {
6
6
 
7
7
  Options:
8
8
  -c/--config [file] Configuration file path (default: ./knip.json or package.json#knip)
9
- --cwd Working directory (default: current working directory)
9
+ -t/--tsConfig [file] TypeScript configuration path (default: ./tsconfig.json)
10
+ --dir Working directory (default: current working directory)
10
11
  --include Report only listed issue group(s) (see below)
11
12
  --exclude Exclude issue group(s) from report (see below)
12
13
  --dev Include \`devDependencies\` in report(s) (default: false)
@@ -20,7 +21,7 @@ Issue groups: files, dependencies, unlisted, exports, nsExports, types, nsTypes,
20
21
  Examples:
21
22
 
22
23
  $ knip
23
- $ knip --cwd packages/client --include files
24
+ $ knip --dir packages/client --include files
24
25
  $ knip -c ./knip.js --reporter compact --jsdoc public
25
26
 
26
27
  More info: https://github.com/webpro/knip`);
package/dist/index.js CHANGED
@@ -15,11 +15,11 @@ const lineRewriter = new log_1.LineRewriter();
15
15
  async function run(configuration) {
16
16
  const { workingDir, isShowProgress, report, isDev, jsDocOptions } = configuration;
17
17
  const { getUnresolvedDependencies, getUnusedDependencies, getUnusedDevDependencies } = (0, dependencies_1.getDependencyAnalyzer)(configuration);
18
- const production = await (0, project_1.createProject)(workingDir, configuration.entryFiles);
18
+ const production = await (0, project_1.createProject)(configuration, configuration.entryFiles);
19
19
  const entryFiles = production.getSourceFiles();
20
20
  production.resolveSourceFileDependencies();
21
21
  const productionFiles = production.getSourceFiles();
22
- const project = await (0, project_1.createProject)(workingDir, configuration.projectFiles);
22
+ const project = await (0, project_1.createProject)(configuration, configuration.projectFiles);
23
23
  const projectFiles = project.getSourceFiles();
24
24
  const [usedProductionFiles, unreferencedProductionFiles] = (0, project_1.partitionSourceFiles)(projectFiles, productionFiles);
25
25
  const [usedEntryFiles, usedNonEntryFiles] = (0, project_1.partitionSourceFiles)(usedProductionFiles, entryFiles);
@@ -127,6 +127,8 @@ async function run(configuration) {
127
127
  if (declaration.isKind(ts_morph_1.ts.SyntaxKind.Identifier)) {
128
128
  identifier = declaration;
129
129
  }
130
+ else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.ArrowFunction)) {
131
+ }
130
132
  else if (declaration.isKind(ts_morph_1.ts.SyntaxKind.FunctionDeclaration) ||
131
133
  declaration.isKind(ts_morph_1.ts.SyntaxKind.ClassDeclaration) ||
132
134
  declaration.isKind(ts_morph_1.ts.SyntaxKind.TypeAliasDeclaration) ||
@@ -156,7 +158,7 @@ async function run(configuration) {
156
158
  }
157
159
  else {
158
160
  const refFiles = new Set(refs.map(r => r.compilerObject.references.map(r => r.fileName)).flat());
159
- const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === sourceFile.getFilePath();
161
+ const isReferencedOnlyBySelf = refFiles.size === 1 && [...refFiles][0] === filePath;
160
162
  if (!isReferencedOnlyBySelf)
161
163
  return;
162
164
  if ((0, ts_morph_helpers_1.findReferencingNamespaceNodes)(sourceFile).length > 0) {
@@ -39,7 +39,7 @@ exports.default = ({ issues, config, workingDir }) => {
39
39
  }
40
40
  if (report.dependencies && isDev) {
41
41
  const unreferencedDevDependencies = Array.from(issues.devDependencies);
42
- logIssueGroupResult(unreferencedDevDependencies, workingDir, reportMultipleGroups && 'UNUSED DEV DEPENDENCIES');
42
+ logIssueGroupResult(unreferencedDevDependencies, workingDir, 'UNUSED DEV DEPENDENCIES');
43
43
  }
44
44
  if (report.unlisted) {
45
45
  const unreferencedDependencies = Object.values(issues.unresolved).map(issues => {
@@ -41,7 +41,7 @@ exports.default = ({ issues, config, workingDir }) => {
41
41
  }
42
42
  if (report.dependencies && isDev) {
43
43
  const unreferencedDevDependencies = Array.from(issues.devDependencies);
44
- logIssueGroupResult(unreferencedDevDependencies, workingDir, reportMultipleGroups && 'UNUSED DEV DEPENDENCIES');
44
+ logIssueGroupResult(unreferencedDevDependencies, workingDir, 'UNUSED DEV DEPENDENCIES');
45
45
  }
46
46
  if (report.unlisted) {
47
47
  const unresolvedDependencies = Object.values(issues.unresolved).map(Object.values).flat();
package/dist/types.d.ts CHANGED
@@ -24,8 +24,12 @@ export declare type IssueType = keyof Issues;
24
24
  export declare type ProjectIssueType = Extract<IssueType, 'files' | 'dependencies' | 'devDependencies'>;
25
25
  export declare type SymbolIssueType = Exclude<IssueType, ProjectIssueType>;
26
26
  export declare type IssueGroup = 'files' | 'dependencies' | 'unlisted' | 'exports' | 'nsExports' | 'types' | 'nsTypes' | 'duplicates';
27
- export declare type LocalConfiguration = {
28
- dev?: boolean;
27
+ export declare type BaseLocalConfiguration = {
28
+ entryFiles: string[];
29
+ projectFiles: string[];
30
+ };
31
+ export declare type LocalConfiguration = BaseLocalConfiguration & {
32
+ dev?: boolean | BaseLocalConfiguration;
29
33
  entryFiles: string[];
30
34
  projectFiles: string[];
31
35
  include?: string[];
@@ -40,6 +44,7 @@ export declare type Configuration = LocalConfiguration & {
40
44
  dependencies: string[];
41
45
  devDependencies: string[];
42
46
  isDev: boolean;
47
+ tsConfigFilePath: undefined | string;
43
48
  tsConfigPaths: string[];
44
49
  isShowProgress: boolean;
45
50
  jsDocOptions: {
@@ -1,5 +1,8 @@
1
1
  import type { ImportedConfiguration, LocalConfiguration } from '../types';
2
- export declare const resolveConfig: (importedConfiguration: ImportedConfiguration, cwdArg?: string) => LocalConfiguration | undefined;
2
+ export declare const resolveConfig: (importedConfiguration: ImportedConfiguration, options?: {
3
+ workingDir?: string;
4
+ isDev?: boolean;
5
+ }) => LocalConfiguration | undefined;
3
6
  export declare const resolveIncludedIssueGroups: (includeArg: string[], excludeArg: string[], resolvedConfig?: LocalConfiguration) => {
4
7
  files: boolean;
5
8
  dependencies: boolean;
@@ -5,20 +5,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveIncludedIssueGroups = exports.resolveConfig = void 0;
7
7
  const micromatch_1 = __importDefault(require("micromatch"));
8
- const resolveConfig = (importedConfiguration, cwdArg) => {
8
+ const resolveConfig = (importedConfiguration, options) => {
9
+ let resolvedConfig = importedConfiguration;
10
+ const { workingDir, isDev } = options ?? {};
9
11
  const configKeys = Object.keys(importedConfiguration);
10
- if (cwdArg && !('projectFiles' in importedConfiguration)) {
11
- const importedConfigKey = configKeys.find(pattern => micromatch_1.default.isMatch(cwdArg.replace(/\/$/, ''), pattern));
12
+ if (workingDir && !('projectFiles' in importedConfiguration)) {
13
+ const importedConfigKey = configKeys.find(pattern => micromatch_1.default.isMatch(workingDir.replace(/\/$/, ''), pattern));
12
14
  if (importedConfigKey) {
13
- return importedConfiguration[importedConfigKey];
15
+ resolvedConfig = importedConfiguration[importedConfigKey];
14
16
  }
15
17
  }
16
- if (!cwdArg && (!importedConfiguration.entryFiles || !importedConfiguration.projectFiles)) {
18
+ if (isDev && typeof resolvedConfig.dev === 'object' && 'projectFiles' in resolvedConfig.dev) {
19
+ resolvedConfig = resolvedConfig.dev;
20
+ }
21
+ if (!resolvedConfig.entryFiles || !resolvedConfig.projectFiles) {
17
22
  console.error('Unable to find `entryFiles` and/or `projectFiles` in configuration.');
18
- console.info(`Add it at root level, or use --cwd and match one of: ${configKeys.join(', ')}\n`);
23
+ console.info(`Add these properties at root level, or use --cwd and match one of: ${configKeys.join(', ')}\n`);
19
24
  return;
20
25
  }
21
- return importedConfiguration;
26
+ return resolvedConfig;
22
27
  };
23
28
  exports.resolveConfig = resolveConfig;
24
29
  const resolveIncludedIssueGroups = (includeArg, excludeArg, resolvedConfig) => {
@@ -1,4 +1,5 @@
1
1
  import { Project } from 'ts-morph';
2
2
  import type { SourceFile } from 'ts-morph';
3
- export declare const createProject: (cwd: string, paths?: string | string[]) => Promise<Project>;
3
+ import type { Configuration } from '../types';
4
+ export declare const createProject: (configuration: Configuration, paths?: string | string[]) => Promise<Project>;
4
5
  export declare const partitionSourceFiles: (projectFiles: SourceFile[], productionFiles: SourceFile[]) => SourceFile[][];
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.partitionSourceFiles = exports.createProject = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  const ts_morph_1 = require("ts-morph");
9
- const path_1 = require("./path");
10
9
  const resolvePaths = (cwd, patterns) => {
11
10
  return [patterns].flat().map(pattern => {
12
11
  if (pattern.startsWith('!'))
@@ -14,15 +13,16 @@ const resolvePaths = (cwd, patterns) => {
14
13
  return node_path_1.default.join(cwd, pattern);
15
14
  });
16
15
  };
17
- const createProject = async (cwd, paths) => {
18
- const tsConfigFilePath = await (0, path_1.findFile)(cwd, 'tsconfig.json');
16
+ const createProject = async (configuration, paths) => {
17
+ const { tsConfigFilePath, workingDir } = configuration;
18
+ const tsConfig = tsConfigFilePath ? { tsConfigFilePath } : { compilerOptions: { allowJs: true } };
19
19
  const workspace = new ts_morph_1.Project({
20
- tsConfigFilePath,
20
+ ...tsConfig,
21
21
  skipAddingFilesFromTsConfig: true,
22
22
  skipFileDependencyResolution: true,
23
23
  });
24
24
  if (paths)
25
- workspace.addSourceFilesAtPaths(resolvePaths(cwd, paths));
25
+ workspace.addSourceFilesAtPaths(resolvePaths(workingDir, paths));
26
26
  return workspace;
27
27
  };
28
28
  exports.createProject = createProject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Find unused files and exports in your TypeScript project",
5
5
  "keywords": [
6
6
  "find",