Skip to content

Handling Issues

A long report can be frustrating. The list may contain false positives, but also actual clutter. To get the most value out of Knip, it may require some initial configuration.

This page guides you in dealing with false positives. Especially if you start out using Knip in a large project and have a long report, it makes sense to go over the issue types one by one. For instance, reducing the number of unused files will also reduce the number of unused dependencies.

Unused files

The first thing we’re looking at is unused files, since the positive effect of getting those right trickles down into the other issue types as well. Files are reported as unused if they are in the set of project files, but not in the set of files resolved from the entry files:

unused files = project files - (entry files + resolved files)

Let’s go over common causes for adding entry patterns manually:

Use --files to filter the report and focus on unused files:

Terminal window
knip --files

This works with other issue types as well. For instance, use --dependencies to focus only on dependencies and exclude issues related to unused files and exports.

Dynamic import specifiers

Dynamic import specifiers aren’t resolved, such as:

const entry = await import(path.join(baseDir, 'entry.ts'));

Solution: add entry.ts to entry patterns.

Unsupported arguments in scripts

Some tooling command arguments aren’t recognized:

{
"name": "my-lib",
"version": "1.0.0",
"scripts": {
"build": "unknown-build-cli --entryPath entry.ts"
}
}

The same goes for other scripts such as those in GitHub Actions workflows or Git hooks. See script parser for what Knip does support.

Solution: add entry.ts to entry patterns.

Unsupported file formats

Entry files referenced in HTML files (e.g. <script src="entry.js">).

<html>
<body>
<script type="module" src="entry.js"></script>
</body>
</html>

Knip has support for some popular framework formats through compilers, and additional compilers can be added for for any file type.

The recommended solution is usually to add the file as shown in each example as an entry file.

Solution: add entry.js to entry patterns. Or add an .html compiler to extract and resolve the value of <script src> elements.

Missing plugin

You might be using a tool or framework for which Knip doesn’t have a plugin (yet). Configuration and entry files (and related dependencies) may be reported as unused because there is no plugin yet that would include those files. For example,

  • Config file tool.config.js contains a reference to the package "@tool/plugin" and both the file and the dependency may be reported as an unused.
  • A framework imports all files matching src/models/*.ts and those might be reported as unused.

Solution: create a new plugin for the tool or framework that’s not in the list yet, or request it. Or work around it and add entry patterns and maybe ignore a dependency or two.

Incomplete plugin

Files may be reported as unused if existing plugins do not include that entry file pattern yet. See the plugins section of entry files for more details.

Solution: override plugin configuration to customize default patterns for existing plugins. Or even better: send a pull request to improve the plugin.

Integrated monorepos

Multiple instances of configuration files like .eslintrc and jest.config.json across the repository may be reported as unused when working in a (mono)repo with a single package.json.

Solution: see integrated monorepos for more details and how to configure plugins to target those configuration files.

Build artifacts and ignored files

In rare cases, build artifacts and .gitignore files may have a surprising effects on files reported as unused. Results may be different in separate runs, depending on the presence of build artifacts. Knip tries to do the right thing, but in some cases you may need to add a file to the entry file patterns manually for better or more consistent results.

Auto-mocking or auto-imports

Some frameworks have features like “auto-mocking” or “auto-imports” enabled, such as Jest and Nuxt.

Solution: include such entry files by extending the entry file patterns. This is recommended in most cases:

{
"entry": ["src/index.ts", "src/models/*.ts"]
}

Alternatively, exceptions and outliers can be excluded from the analysis using negated project patterns:

{
"project": ["src/**/*.ts", "!src/**/__mocks__/**"]
}

Unused dependencies

First off: dependencies imported in unused files are reported as unused dependencies. That’s why it’s strongly recommended to try and remedy unused files first. Better entry and project file coverage will solve many cases of reported unused dependencies.

The most common causes for unused and unlisted dependencies include:

Use --dependencies to filter the report and focus on dependency related issues:

Terminal window
knip --dependencies

Missing or incomplete plugin

If a plugin exists and the dependency is referenced in the configuration file, but its custom dependency finder does not detect it, then that’s a false positive. Please open a pull request or issue to fix it.

Solution: adding the configuration file as an entry file pattern may be a temporary stopgap that fixes your situation, but it’s better to create a new plugin or fix an existing one.

Unreachable code

Sometimes a reference to a dependency is unrecognizable or unreachable to Knip.

Solution: add a new plugin or improve an existing one. If you don’t feel like a plugin could solve it, a last resort is to ignore it:

{
"ignoreDependencies": ["ignore-me", "@problematic/package"]
}

If a binary (or “executable”) is referenced you’ll want to use ignoreBinaries instead. See unlisted binaries.

Types packages

For packages that have types integrated in the same package, the separate types package is reported as unused.

This is also useful for future regressions: if a package had a DT package for its types before and later on integrates those types into its own package, Knip will start reporting the types dependency as unused. Examples include Webpack v5 and React Router v6 rendering the @types/webpack and @types/react-router dependencies obsolete since those versions.

Solution: remove the types dependency (usually @types scoped DT package).

TypeScript, Jest & legacy ESLint

In monorepos, sharing and extending configurations is convenient, but for a project linter like Knip certain scenarios can be a challenge to assign dependencies to the right workspace. Specifically, the root cause is usually a combination of JSON-based configuration files that extend from each other across workspaces in a monorepo. This is an issue since we can’t “assign” dependencies to other workspaces and incorrectly end up having “unlisted dependencies”.

Most notably, tools like TypeScript, Jest and legacy ESLint suffer from this. The new ESLint flat config system does not have this issue, so it’s recommended to migrate if you haven’t already.

Unlisted dependencies

This means that a dependency is used, but not listed in package.json.

An unlisted dependency is usually a transitive dependency that’s imported directly. The dependency is installed (since it’s a dependency of another dependency) and lives in node_modules, but it’s not listed explicitly in package.json.

You should not rely on transitive dependencies for various reasons, including control, security and stability.

Solution: install and list the dependency in dependencies or devDependencies.

Unlisted binaries

Binaries are executable Node.js scripts. Many npm packages, when installed, add an executable file to use from scripts in package.json. Examples include TypeScript with the tsc binary, Next.js with the next binary, and so on.

Knip detects such binaries in scripts and checks whether there’s a package installed that includes that binary. It looks up the bin field in the package.json file of installed packages. If it doesn’t find it, it will be reported as an unlisted binary as there is no package listed that contains it.

Except for binaries that are most likely meant to be installed on the OS already and not installed from npm (details: list in source).

Missing binaries

In case unused (dev) dependencies look like a match against unlisted binaries, then this might be caused by node_modules not containing the packages. And this might have been caused by either the way your package manager installs dependencies and binaries, or by not running Knip from the root of the repository.

Solution: run Knip from the project root. From there you can lint individual workspaces.

Example

Sometimes their usage or the way Knip reports them can be a bit confusing. See this example:

{
"name": "lib",
"scripts": {
"commitlint": "commitlint --edit"
},
"devDependencies": {
"@commitlint/cli": "*"
}
}

This example works fine without anything reported, as the @commitlint/cli package includes the commitlint binary. However, some script may contain npx commitlint and here Knip assumes commitlint is the name of the package. This technically works as commitlint is a transitive dependency of @commitlint/cli, but to avoid confusion it’s recommended to use npx @commitlint/cli.

npx

For usages of the npx command, Knip assumes npx --yes my-package means that my-package is not listed. Knip expects the dependency to be listed with --no or no flag at all. The recommendation here is to be explicit:

  • Use npx --yes if the dependency is not supposed to be listed in package.json.
  • Use npx --no if the dependency is listed in package.json.

Unused exports

By default, Knip does not report unused exports of entry files.

The most common causes for unused exports include:

Use the --exports flag to filter and focus on issues related to exports:

Terminal window
knip --exports

Enumerations

Currently Knip does not consider all exports or enum members referenced when implicitly referenced in an enumeration like the following example:

import * as Fruits from './fruits.js';
import { FruitsEnum } from './fruits.js';
for (const fruit of Object.values(Fruits)); // Exports are not referenced explicitly
Object.entries(FruitsEnum); // Members are not referenced explicitly

See namepace imports to see what patterns Knip does consider to be used exports.

External libraries

Are the exports consumed or imported by an external library, resulting in a non-standard consumption of your exports? Here’s an example:

import loadable from '@loadable/component';
export const DynamicApple = dynamic(() =>
import('./components.js').then(mod => mod.Apple)
);
export const LoadableOrange = loadable(() => import('./components.js'), {
resolveComponent: components => components.Orange,
});

Knip understands Apple is used, since it’s standard usage. But Orange is referenced through a function of an external library. For performance reasons, Knip does not include external type definitions by default so it won’t see the export being referenced.

To include the type definitions of external libraries, use the —include-libs flag:

Terminal window
knip --include-libs

This comes at a performance and memory penalty, but should give better results if you need it. This flag is implied when classMembers are included (that feature comes with roughly the same performance penalty).

Exclude exports from the report

To exclude unused exports from the report, there are a few options:

  • Ignore exports used in file for exports used internally.
  • Individual exports can be tagged using JSDoc syntax.
  • Have the export in an entry file:
    • Add the file to the entry file patterns array in the configuration.
    • Move the export(s) to an entry file.
    • Add the file to the exports field of package.json
  • Re-export the unused export(s) from an entry file.

Missing exports?

Did you expect certain exports in the report, but are they missing? They might be exported from an entry file. In that case, use —include-entry-exports to make Knip also report unused exports in entry files.

The exports of non-standard extensions like .astro, .mdx, .vue or .svelte are not available by default. See compilers for more details on how to include them.

Class members

Unused class members are not reported by default, here’s how to enable them:

Terminal window
knip --include classMembers

This option is also available in the Knip configuration file. Note that this feature comes at a cost: linting will take more time and more memory.

Individual class members can be tagged using JSDoc syntax.

Classes exported from entry files are ignored, and so are their members. Use —include-entry-exports to make Knip also report members of unused exports in entry files.

Enum members

Unused enums and unused members of enums are reported by default. Reporting such members can also be disabled altogether, for example:

Terminal window
knip --exclude enumMembers

Individual enum members can be tagged using JSDoc syntax.

Enums exported from entry files are ignored, and so are their members. Use —include-entry-exports to make Knip also report members of unused exports in entry files.

Feedback or false positives?

If you believe Knip incorrectly reports something as unused (i.e. there’s a false positive), feel free to create a minimal reproduction and open an issue on GitHub. It’ll make Knip better for everyone! Join the Discord channel to discuss any feedback or questions you may have.

ISC License © 2024 Lars Kappert