Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/docs/src/content/docs/features/production-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ when finding unused or unlisted dependencies.
Additionally, the `--strict` flag can be added to:

- Verify isolation: workspaces should use strictly their own `dependencies`
- Include `peerDependencies` when finding unused or unlisted dependencies
- Report type-only imports listed in `dependencies`
- Include required `peerDependencies` when finding unused or unlisted
dependencies
- [Distinguish development-only type imports from types exposed to package
consumers][3]

```sh
knip --production --strict
Expand All @@ -92,3 +94,4 @@ knip --production --exclude types

[1]: ../reference/jsdoc-tsdoc-tags.md#internal
[2]: ../guides/configuring-project-files.md
[3]: ../guides/handling-issues.mdx#production-types
31 changes: 19 additions & 12 deletions packages/docs/src/content/docs/guides/handling-issues.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -546,13 +546,19 @@ obsolete respectively.

#### Production types

Knip is strict in the divide between `dependencies` and `devDependencies`. Some
packages are published with one or more type packages listed in `dependencies`.
In strict production mode, even when re-exported and part of the package's
public API, Knip does not try to figure out what exactly are "production types"
and expects those in `devDependencies`.
In [strict mode][51], a dependency used only in type-only source imports can
live in `devDependencies`. If the dependency is exposed through the package's
built declaration files, it is production usage and must instead be listed in
`dependencies` or required `peerDependencies`.

**Solution**: list exceptions in [ignoreDependencies][24].
Knip finds this public type graph by following existing declaration files
exposed through `main`, `types`, `typings`, `exports`, and `typesVersions`. If
pnpm `publishConfig` overrides those manifest fields, Knip uses their published
values.

Private workspaces and packages using `publishConfig.directory` are not
analyzed. Knip only follows existing declaration output and does not guess from
source imports.

### Unlisted dependencies

Expand Down Expand Up @@ -584,7 +590,7 @@ reported as an unlisted binary as there is no package that contains it.

Binaries that are installed on the OS already and thus likely not meant to be
installed from npm are not reported as unlisted (details: [list of ignored
binaries in source][51]).
binaries in source][52]).

#### Missing binaries

Expand All @@ -594,7 +600,7 @@ 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. If needed, [lint workspaces
individually][52].
individually][53].

Sometimes binaries and how they're reported can be a bit confusing. See this
example:
Expand Down Expand Up @@ -622,7 +628,7 @@ technically works, as `commitlint` is a transitive dependency of
## 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][53] and open an
false positive), feel free to create a [minimal reproduction][54] and open an
issue on GitHub. It'll make Knip better for everyone!

[1]: ../explanations/how-knip-works.md
Expand Down Expand Up @@ -675,6 +681,7 @@ issue on GitHub. It'll make Knip better for everyone!
[48]: #type-definition-packages
[49]: #unlisted-dependencies
[50]: #unlisted-binaries
[51]: https://github.com/webpro-nl/knip/blob/e031018e676c47d84873cc2403251c0111ef318d/packages/knip/src/constants.ts#L39-L148
[52]: ../features/monorepos-and-workspaces.md#filter-workspaces
[53]: ../guides/issue-reproduction.md
[51]: ../features/production-mode.md#strict-mode
[52]: https://github.com/webpro-nl/knip/blob/e031018e676c47d84873cc2403251c0111ef318d/packages/knip/src/constants.ts#L39-L148
[53]: ../features/monorepos-and-workspaces.md#filter-workspaces
[54]: ../guides/issue-reproduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { AmbientShim } from 'ambient-shim';

declare global {
const shim: AmbientShim;
}

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@fixtures/declaration-dependencies-private",
"private": true,
"main": "index.js",
"types": "index.d.ts",
"devDependencies": {
"ambient-shim": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { PrivatePattern } from 'private-pattern';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { PatternPublic } from 'pattern-public';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path="./reference.d.ts" />
/// <reference types="reference-public" />

import type { PublicSignature } from 'public-signature';
import type { MisplacedPublic } from 'misplaced-public';
import type { MissingPublic } from 'missing-public';
import type { Legacy } from 'legacy';
import type { ManifestOnly } from 'manifest-only';

export type { ReexportedPublic } from 'reexported-public';
export type { TransitivePublic } from './nested.js';

export declare const inferred: import('inferred-public').InferredPublic;
export declare const virtualAsset: import('virtual-public:assets').Asset;
export type InlinePublic = import('inline-public').InlinePublic;
export declare function acceptsPublic(value: PublicSignature): void;
export declare function acceptsMisplaced(value: MisplacedPublic): void;
export declare function acceptsMissing(value: MissingPublic): void;
export declare function acceptsLegacy(value: Legacy): void;
export declare function acceptsManifestOnly(value: ManifestOnly): void;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { TransitivePublic } from 'transitive-public';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'path-public';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@fixtures/declaration-dependencies",
"private": false,
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./features/*": {
"types": "./dist/features/*.d.ts",
"default": "./dist/features/*.js"
},
"./features/private": null
},
"dependencies": {
"@types/reference-public": "*",
"inferred-public": "*",
"inline-public": "*",
"path-public": "*",
"pattern-public": "*",
"public-signature": "*",
"reexported-public": "*",
"transitive-public": "*",
"unused-production": "*"
},
"devDependencies": {
"@types/legacy": "*",
"@types/manifest-only": "*",
"misplaced-public": "*",
"private-pattern": "*",
"private-source": "*",
"virtual-public": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { PrivateSource } from 'private-source';

const privateState: PrivateSource | undefined = undefined;

export const hasPrivateState = () => privateState !== undefined;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { BinType } from 'bin-only-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { RuntimeType } from 'runtime-only-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { FallbackType } from 'fallback-only-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@fixtures/declaration-entry-selection",
"private": false,
"main": "./src/index.ts",
"types": "./types/development.d.ts",
"bin": "./bin/cli.js",
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"main": "./dist/index.js",
"types": "./types/public.d.ts",
"exports": {
".": {
"types": "./types/public.d.ts",
"default": "./dist/index.js"
}
}
},
"dependencies": {
"public-dependency": "*"
},
"devDependencies": {
"bin-only-dependency": "*",
"development-only-dependency": "*",
"fallback-only-dependency": "*",
"runtime-only-dependency": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { DevelopmentType } from 'development-only-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { PublicType } from 'public-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@fixtures/declaration-publish-directory",
"version": "1.0.0",
"main": "index.js",
"types": "index.d.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { RootOnly } from 'root-only-dependency';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@fixtures/declaration-publish-directory",
"private": false,
"main": "index.js",
"types": "index.d.ts",
"publishConfig": {
"directory": "dist"
},
"devDependencies": {
"root-only-dependency": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { UntypedLib } from 'untyped-lib';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@fixtures/declaration-tsconfig-paths",
"private": false,
"types": "./dist/index.d.ts",
"devDependencies": {
"@types/untyped-lib": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface UntypedLib {
local: true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"untyped-lib": ["./src/untyped-lib.ts"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface CtsTarget {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface MtsTarget {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface TypeScriptTarget {}
13 changes: 9 additions & 4 deletions packages/knip/src/DependencyDeputy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ import { findMatch, toRegexOrString } from './util/regex.ts';
const filterIsProduction = (id: string | RegExp, isProduction: boolean): string | RegExp | never[] =>
typeof id === 'string' ? (isProduction || !id.endsWith('!') ? id.replace(/!$/, '') : []) : id;

interface DependencyReferenceOptions {
isDevOnly?: boolean;
isTypeOnly?: boolean;
isResolved?: boolean;
isPublishedType?: boolean;
}

/**
* - Stores manifests
* - Stores referenced external dependencies
Expand Down Expand Up @@ -197,9 +204,7 @@ export class DependencyDeputy {
public maybeAddReferencedExternalDependency(
workspace: Workspace,
packageName: string,
isDevOnly?: boolean,
isTypeOnly?: boolean,
isResolved?: boolean
{ isDevOnly, isTypeOnly, isResolved, isPublishedType }: DependencyReferenceOptions = {}
): boolean {
if (!this.isReportDependencies) return true;
if (packageName.startsWith('node:') || isBuiltin(packageName)) return true;
Expand All @@ -209,7 +214,7 @@ export class DependencyDeputy {
if (packageName === workspace.pkgName) return true;

const workspaceNames = this.isStrict ? [workspace.name] : [workspace.name, ...workspace.ancestors.toReversed()];
const isDevOrTypeOnly = isDevOnly || isTypeOnly;
const isDevOrTypeOnly = isDevOnly || (isTypeOnly && !isPublishedType);
const closestWorkspaceName = workspaceNames.find(name => this.isInDependencies(name, packageName, isDevOrTypeOnly));

// Prevent false positives by also marking the `@types/packageName` dependency as referenced
Expand Down
11 changes: 4 additions & 7 deletions packages/knip/src/graph/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,10 @@ export const analyze = async ({
const packageName = getPackageNameFromModuleSpecifier(extImport.specifier);
const isHandled =
packageName &&
deputy.maybeAddReferencedExternalDependency(
ws,
packageName,
undefined,
extImport.isTypeOnly,
extImport.filePath !== undefined
);
deputy.maybeAddReferencedExternalDependency(ws, packageName, {
isTypeOnly: extImport.isTypeOnly,
isResolved: extImport.filePath !== undefined,
});
if (!isHandled)
collector.addIssue({
type: 'unlisted',
Expand Down
Loading
Loading