# FAQ

Source: https://knip.dev/reference/faq

## Introduction

Knip finds and fixes unused dependencies, exports and files. As a “kitchen sink”
in the npm ecosystem, it creates comprehensive module and dependency graphs of
your project.

The JavaScript/TypeScript ecosystem has a vast amount of frameworks and tools.
Additionally, file locations, configuration semantics, command-line arguments
and so on vary wildly. Files and dependencies are referenced in many ways. Knip
tries harder than you think to cover it all.

This FAQ is an attempt to provide some perspective on a few design decisions and
why certain things work the way they do. Here and there it’s intentionally a bit
more in-depth than the rest of the docs.

## Comparison

### Why isn’t Knip an ESLint plugin?

Linters like ESLint analyze files separately, while Knip lints projects as a
whole.

Knip requires full module and dependency graphs to find clutter across the
project. Creating these comprehensive graphs is not a trivial task and it seems
no such tool exists today, even more so when it comes to monorepos.

File-oriented linters like ESLint are complementary to Knip.

### Isn’t tree-shaking enough?

In short: no. They share an important goal: improve UX by removing unused code.
The main takeaway here is that tree-shaking and Knip are different and
complementary tools.

Tree-shaking is a build or compile-time activity to reduce production bundle
size. It typically operates on bundled production code, which might include
external/third-party code. An optimization in the build process, “out of your
hands”.

On the other hand, Knip is a project linter that should be part of the QA phase.
It lints, reports and fixes only your own source code. Moreover, in contrast
with other linters, focuses on inter-file dependencies, so dead code within a
file may not be caught by Knip.

Issues reported by the linter are then for you to handle (except for everything
that Knipauto-fixesfor you).

Besides those differences, Knip has a broader scope:

- Improve DX (seeless is more).
- Include non-production code and dependencies in the process by default.
- Handle moretypes of issues(such as unlisted dependencies).

## Synergy

### Why does Knip have plugins?

Plugins are an essential part of Knip. They prevent you from a lot of
configuration out of the box, by adding entry files as accurately as possible
and only for the tools actually installed. Yet the real magic is in their custom
parsers for configuration files and command-line argument definitions.

For instance, Vitest has theenvironmentconfiguration option. The Vitest
plugin knows"node"is the default value forenvironmentwhich does not
require an extra package, but will translate"edge-runtime"to the@edge-runtime/vmpackage. This allows Knip to report it if this package is not
listed inpackage.json, or when it is no longer used after changes in the
Vitest configuration.

Configuration files may also contain references to entry files. For instance,
Jest hassetupFilesAfterEnv: "<rootDir>/jest.setup.js"or a reference may
point to a file in another workspace in the same monorepo, e.g.setupFiles: ['@org/shared/jest-setup.ts']. Those entry files may also contain
imports of internal modules or external dependencies, and so on.

### Why is Knip so heavily engineered?

Even though a modular approach has its merits, for Knip it makes sense to have
all the pieces in a single tool.

Building up the module and dependency graphs requires non-standard module
resolution and not only static but also dynamic analysis (i.e. actually load and
execute modules), such as for parsers of plugins to receive the exported value
of dynamic tooling configuration files. Additionally,exports consumed by
external librariesrequire type information, as supported by the TypeScript
backend. Last but not least, shell script parsing is required to find the right
entry files, configuration files and dependencies accurately.

The rippling effect of plugins and recursively adding entry files and
dependencies to build up the graphs is also exactly what’s meant by“comprehensive” here.

## Building the graphs

### Where does Knip look for entry files?

- In default locations such asindex.jsandsrc/index.ts
- Inmain,binandexportsfields inpackage.json
- In the entry files as configured by enabled plugins
- Inconfigfiles as configured and parsed by enabled plugins
- Theconfigfiles themselves are entry files
- In dynamic imports (i.e.require()andimport()calls)
- Inrequire.resolve('./entry.js')
- Inimport.meta.resolve('./entry.mjs')
- Through scripts inside template strings in source files such as:

```typescript
await$({ stdio:'inherit'})`c8 node hydrate.js`;// execaawait$`node scripts/parse.js`;// bun/zx
```

- Through scripts inpackage.jsonsuch as:

```typescript
{"name":"my-lib","scripts":{"start":"node --import tsx/esm run.ts","start":"vitest -c config/vitest.config.ts"}}
```

- Through plugins handling CI workflow files like.github/workflows/ci.yml:

```typescript
jobs:test:steps:run:playwright test e2e/**/*.spec.ts --config playwright.e2e.config.tsrun:node --import tsx/esm run.ts
```

Scripts like the ones shown here may also contain references to configuration
files (config/vitest.config.tsandplaywright.e2e.config.tsin the examples
above). They’re recognized as configuration files and passed to their respective
plugins, and may contain additional entry files.

Entry files are added to the module graph.Module resolutionmight result
in additional entry files recursively until no more entry files are found.

### What does Knip look for in source files?

The TypeScript source file parser is powerful and fault-tolerant. Knip visits
all nodes of the generated AST to find:

- Imports and dynamic imports of internal modules and external dependencies
- Exports
- Accessed properties on namespace imports and re-exports to track individual
export usage
- Calls torequire.resolveandimport.meta.resolve
- Scripts in template strings (passed toscript parser)

### What’s in the graphs?

Once the module and dependency graphs are created, they contain the information
required to create the report including all issue types:

- Unused files
- Unused dependencies
- Unused devDependencies
- Referenced optional peerDependencies
- Unlisted dependencies
- Unlisted binaries
- Unresolved imports
- Unused exports
- Unused exported types
- Unused exported enum members
- Duplicate exports

And optionally more issue types like individual exports and exported types in
namespace imports, and unused class members.

The graphs allows to report more interesting details, such as:

- Circular references
- Usage numbers per export
- Export usage across workspaces in a monorepo
- List of all binaries used
- List of all used (OS) binaries not installed innode_modules

### Why doesn’t Knip just read the lockfile?

Knip reads thepackage.jsonfile of each dependency. Most of the information
required is in the lockfile as well, which would be more efficient. However,
there are a few issues with this approach:

- It requires lockfile parsing for each lockfile format and version of each
package manager.
- The lockfile doesn’t contain whether the packagehas types included.

## Module Resolution

### Why doesn’t Knip use an existing module resolver?

Runtimes like Node.js providerequire.resolveandimport.meta.resolve.
TypeScript comes with module resolution built-in. More module resolvers are out
there and bundlers are known to use or come with module resolvers. None of them
seem to meet all requirements to be usable on its own by Knip:

- Support non-standard extensions like.css,.svelteand.png
- Support path aliases
- Supportexportsmap inpackage.json
- Support self-referencing imports
- Rewirepackage.json#mainbuild artifacts likedist/module.jsto its source
atsrc/module.ts
- Don’t resolve to type definition paths likemodule.d.tsbut source code atmodule.js

A few strategies have been tried and tweaked, and Knip currently uses a
combination ofoxc-resolver, the TypeScript module resolver and a few
customizations. This single custom module resolver function is hooked into the
TypeScript compiler and language service hosts.

Everything else is handled byoxc-resolverfor things likescript parsingand resolving references to files in other workspaces.

### How does Knip handle non-standard import syntax?

Knip tries to be resilient against import syntax like what’s used by e.g.
webpack loaders or Vite asset imports. Knip strips off the prefixes and suffixes
in import specifiers like this:

```typescript
importIconfrom'./icon.svg?raw';importStylesfrom'-!style-loader!css-loader?modules!./styles.css';
```

In this example, thestyle-loaderandcss-loaderdependencies should be
dependencies found in webpack configuration, handled by Knip’s webpack plugin.

## TypeScript

### What’s the difference between workspaces, projects and programs?

A workspace is a directory with apackage.jsonfile. They’re configured inpackage.json#workspaces(orpnpm-workspaces.yml). In case a directory has apackage.jsonfile, but is not a workspace (from a package manager
perspective), it can be added as a workspace to the Knip configuration.

Projects - in the context of TypeScript - are directories with atsconfig.jsonfile. They’re not a concept in Knip.

A TypeScript program has a 1-to-1 relationship with workspaces if they’re
analyzed in isolation. However, by default Knip optimizes for performance and
utilizesworkspace sharing. That’s why debug output contains messages like
“Installed 2 programs for 29 workspaces”.

### Why doesn’t Knip match my TypeScript project structure?

Repositories and workspaces in a monorepo aren’t necessarily structured like
TypeScript projects. Put simply, the location ofpackage.jsonfiles isn’t
always adjacent totsconfig.jsonfiles. Knip follows the structure of
workspaces in a monorepo.

An additional layering of TypeScript projects would complicate things. The
downside is that atsconfig.jsonfile not used by Knip may have conflicting
module resolution settings, potentially resulting in missed files.

In practice, this is rarely an issue. Knip sticks to the workspaces structure
and installs a single “kitchen sink” module resolver function per workspace.
Different strategies might add more complexity and performance penalties, while
the current strategy is simple, fast and good enough.

Note that any directory with apackage.jsonnot listed in the rootpackage.json#workspacescan be added to the Knip configuration manually to
have it handled as a separate workspace.

### Why doesn’t Knip analyze workspaces in isolation by default?

Knip creates TypeScript programs to create a module graph and traverse file
ASTs. In a monorepo, it would make a lot of sense to create one program per
workspace. However, this slows down the whole process considerably. That’s why
Knip shares the files of multiple workspaces in a single program if their
configuration allows it. This optimization is enabled by default, while it also
allows the module resolver (one per program) to do some more caching.

Also seeworkspace sharing.

### Why doesn’t Knip just usets.findReferences?

TypeScript has a very good “Find references” feature, that you might be using in
your IDE as well. Yet at scale this becomes too slow. That’s why Knip builds up
its own module graph to look up export usages. Additional benefits for this
comprehensive graph include:

- serializable and cacheable
- enables more features
- usable for other tools to build upon as well

Without sacrificing these benefits, Knip does usets.findReferencesto find
references to class members (i.e. when the issue typeclassMembersis
included). In case analysis of exports requires type information of external
dependencies, the--include-libsflagwill trigger the same.

### Why can’t I use path aliases to reference other workspaces?

Projects can usecompilerOptions.pathsto alias paths in other workspaces in
the same monorepo. Knip doesn’t understand those paths might represent internal
workspaces and might report false positives. How we ended up here is a bit
complicated, but a major reason is that Knip does it’s job based on the
workspace graph, whileworkspaces are different from TypeScript programsandthe workspace graph doesn’t match TypeScript project structure.

The recommendation and best practice is to list such workspaces/dependencies inpackage.json, and import them as such. Other tooling should not have any
issues with this standard approach either.

Also see the example inTypeScript path aliases in monorepos.

### What’s up with that configurabletsconfig.jsonlocation?

There’s a difference between--tsConfig [file]as a CLI argument and thetypescript.configoption in Knip configuration.

The--tsConfig [file]optionis used to provide an alternative location
for the default roottsconfig.jsonfile. RelevantcompilerOptionsincludepathsandmoduleResolution. This setting is only available at the root
level.

On the other hand, theconfigoption of the plugincan be set per
workspace. The TypeScript plugin extracts referenced external dependencies such
as those inextends,compilerOptions.typesand JSX settings:

```typescript
{"extends":"@tsconfig/node20/tsconfig.json","compilerOptions":{"jsxImportSource":"hastscript/svg"}}
```

From this example, Knip can determine whether the@tsconfig/node20andhastscriptdependencies are properly listed inpackage.json.

#### Notes

- The TypeScript plugin doesn’t add support for TypeScript to Knip (that’s
already built-in). Like other plugins, it extracts dependencies fromtsconfig.json. With thetypescript.configoption an alternative location
fortsconfig.jsoncan be set per workspace.
- In case path aliases fromcompilerOptions.pathsaren’t picked up by Knip,
either use--tsConfig [file]to target a differenttsconfig.json, or
manually addpathsto the Knip configuration. The latter can be done per
workspace.

## Compilers

### How does Knip handle Svelte or Astro files?

To further increase the coverage of the module graph, non-standard files other
than JavaScript and TypeScript modules should be included as well. For instance,.mdxand.astrofiles can import each other, internal modules and external
dependencies.

Knip includes basic “compilers” for a few common file types (Astro, MDX, Svelte,
Vue). Knip does not include actual compilers for reasons of potential
incompatibility with the existing compiler, and dependency size. Knip allows to
override them with the compilers in your project, and add additional ones for
other file types.

### Why are the exports of my.vuefiles not used?

Knip comes with basic “compilers” for a few common non-standard file types.
They’re not actual compilers, they’re regular expressions only to extract import
statements. Override the built-in Vue “compiler” with the real one in your
project. Also see the answer to the previous question andCompilers.

## Miscellaneous

### Why isn’t production mode the default?

The default mode of Knip includes all source files, tests, dependencies, dev
dependencies and tooling configuration.

On the other hand, production mode considers only source files and production
dependencies. Plugins add only production entry files.

Which mode should’ve been the default? They both have their merits:

- Production mode catches dead production code and dependencies. This mode has
the most impact on UX, since less code tends to be faster and safer.
- Default mode potentially catches more issues, e.g. lots of unused plugins of
tooling, including most issues found in production mode. This mode has the
most impact on DX, for the same reason.

Also seeproduction mode.

### Why doesn’t Knip have…?

Examples of features that have been requested include:

- Expose programmatic API
- Add local/custom plugins
- Expose the module and dependency graphs
- Custom AST visitors, e.g. to find and return:Unused interface/type membersUnused object members (and e.g. React component props)Unused object props in function return values
- Analyze workspaces in parallel
- Support Deno
- Improve internal code structures and accessibility to support contributions
- One-shot dead code removal (more comprehensive removal of unused variables,
duplicate exports, dead code, etc).
- Replace dependencies for better performance and correctness, such as for shell
script parsing, module resolution and globbing with “unignores”.

- Unused interface/type members
- Unused object members (and e.g. React component props)
- Unused object props in function return values

These are all interesting ideas, but most increase the API surface area, and all
require more development efforts and maintenance. Time is limited andsponsorshipscurrently don’t cover - this can change though!

ISC License© 2024Lars Kappert