knip-migration
Skill627 starsChanged 9 days agoMITView on GitHub
Dead-code / unused-dependency detection uses `knip`, which requires each package to expose an explicit, minimal `package.json#exports` (no `./*` wildcard). Read when adding a new package or migrating an existing one to knip.
In this repository70 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- analyticsSkill
- batch-feature-devSkill
- ci-runnersSkill
- ci-workflow-refsSkill
- cleanupSkill
- client-idsSkill
- cloud-sync-moduleSkill
- codeownershipSkill
- coin-families-contractSkill
- coin-modulesSkill
- commentsSkill
- configurationSkill
- console-logSkill
- create-changesetSkill
- create-prSkill
- data-layer-advancedSkill
- debug-rn-native-crashSkill
- dependency-patchesSkill
- detect-data-leaksSkill
- device-intent-executorSkill
- devtools-import-boundarySkill
- dialogs-sliceSkill
- docs-migrateSkill
- docs-update-or-createSkill
- domain-dataSkill
- e2e-desktop-add-or-updateSkill
- e2e-desktop-onboardSkill
- e2e-earnSkill
- e2e-mobile-add-or-updateSkill
- e2e-mobile-onboardSkill
- e2e-swapSkill
- errorsSkill
- feature-devSkill
- fix-ciSkill
- flow-packageSkill
- get-pr-commentsSkill
- git-conventionsSkill
- i18n-keysSkill
- impacting-prsSkill
- ldls-nativeSkill
- ldls-webSkill
- ledger-wallet-cliSkill
- legacy-migrationSkill
- mvvm-architectureSkill
- new-packageSkill
- package-public-apiSkill
- pnpm-resolutionSkill
- prefer-set-hasSkill
- pre-reviewSkill
- react-generalSkill
- rebuild-to-fix-lib-import-errorsSkill
- remove-feature-flagSkill
- run-buildsSkill
- run-e2e-ciSkill
- runner-imagesSkill
- run-testsSkill
- slack-pr-messageSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 132
Only typecheck catches that, which is why it runs before trusting a clean knip report.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Dead-code detection: explicit exports + knip
Dead-code / unused-dependency detection runs on knip. The legacy unimported tool is gone
and the dependency checks deny it. CI (the Monorepo Checks job, pnpm knip:all locally)
checks every workspace except the legacy packages listed in
support/knip/knip.json#ignoreWorkspaces. That list only shrinks: a migrated package
leaves it, and a new package never joins it. A package needs no workspaces entry either, since
knip derives its entry files from package.json (main, bin, exports) and its own defaults
(entry files).
Why this isn't just "swap the tool"
knip treats everything listed in a package's package.json#exports as an entry point.
Most legacy libs/ packages export a ./* wildcard mapped to ./src/*.ts (via the
@ledgerhq/source custom condition declared in support/tsconfig/base.json):
"./*": {
"@ledgerhq/source": "./src/*.ts",
"import": "./lib-es/*.js",
"require": "./lib/*.js",
"default": "./lib/*.js"
}Because of this wildcard, knip considers every top-level src/*.ts file "used" and cannot
detect unused ("zombie") top-level source files. So a package needs to replace the ./* wildcard with explicit, minimal subpath exports that enumerate
the package's true public API.
The few libs already pointed at knip (e.g.
libs/live-wallet,libs/live-dmk-mobile) still keep the./*wildcard, so they are only partially migrated — do not copy them as the template.
New package = born migrated
A new package has no consumers, so it should start in the target state:
- Explicit
exportsonly. Enumerate the real public API; do not add a./*wildcard. Mirror the.root entry per subpath, keeping the conditions:"exports": { ".": { "@ledgerhq/source": "./src/index.ts", "import": "./lib-es/index.js", "require": "./lib/index.js", "default": "./lib/index.js" }, "./logic": { "@ledgerhq/source": "./src/logic.ts", "import": "./lib-es/logic.js", "require": "./lib/logic.js", "default": "./lib/logic.js" }, "./lib-es/*": "./lib-es/*.js", "./lib/*": "./lib/*.js", "./package.json": "./package.json" } - Nothing to register — CI checks it from the first PR. To run knip on the package alone,
use
pnpm knip --directory <relative-hop-to-root> -W <workspace-path>(../..forlibs/<x>,../../..forlibs/coin-modules/<x>). Do not add a package script for it:pnpm knip:allat the root runs what CI runs.
Dual-platform packages run knip once per platform
knip has no notion of moduleSuffixes. A suffix-less ./Tool specifier therefore resolves to
nothing when only Tool.web.tsx and Tool.native.tsx exist, and both twins are reported as
unused files (files is an error rule). Naming the suffix in the barrel fixes it for packages
that may do so, but not under devtools/, where suffix-imports/no-platform-suffix makes a
suffixed specifier a lint error.
So a package with platform twins gets two passes, through the shared helper
@support/knip, re-exported at the root as knip.config.base.mjs while package configs import it
by path. support/knip's web.config.mjs and native.config.mjs merge
every per-package config into one run per platform, and its root.config.mjs skips any package
that has one. A native-only package adds only the
native config.
// <pkg>/knip.web.config.mjs — and the `native` mirror
import { createDualPlatformKnipConfig } from "../../knip.config.base.mjs";
export default createDualPlatformKnipConfig({
packagePath: "devtools/shell",
platform: "web",
entry: [],
});To run one platform for the package alone:
pnpm knip --directory ../.. -c devtools/shell/knip.web.config.mjs -W devtools/shell --tsConfig tsconfig.web.json.
Name the file knip.<platform>.config.mjs. The root ignore list matches **/*.config.*, so that
name keeps knip from reporting its own config as unused. A .ts config does not match and needs a
Sonar exclusion to compensate.
These configs carry no comments. This section is the single source for why they look as they do; the three options below are the only reasons a config should differ from the snippet above.
entry is [] unless package.json cannot yield the entry
knip derives entries from package.json (every exports condition, main, and specifiers in
scripts), and reports an entry it already has as a redundant entry pattern. Name one only
when that derivation misses a real entry point:
- the package declares no
react-nativecondition, so only the web barrel is derived (devtools/env, anddevtools/feature-flags/devtools/pay-card, which have noexportsat all). Namesrc/index.native.tsin the native config. - a platform file is reachable only through resolution.
devtools/bindingspairsisMockSessionSupported.tswith a.native.tsoverride and no.webtwin, and knip resolves the unsuffixed sibling first, so the native config namessrc/isMockSessionSupported.native.ts. Such an entry goes stale when the override is deleted; knip then reports Refine entry pattern (no match), so treat that hint as "the pair is gone, drop the entry".
additionalProjectExcludes is for an unsuffixed barrel
The platform glob the helper applies (!src/**/*.native.* on web, !src/**/*.web.* on native)
only matches suffixed files. A web barrel named src/index.ts is not excluded from the native pass
and becomes an unused file there, so the native config names it. A package whose web barrel is
src/index.web.ts (devtools/transport-panel) needs nothing.
additionalIgnoreDependencies is a last resort
A dependency reached only from the excluded platform's files reads as unused, and dependencies is
an error rule. Confirm the dependency is genuinely unreachable in that platform before reaching
for this.
Two resolution facts that look like bugs
- An unsuffixed sibling beats the compiler extension. With
{"web.ts": passThrough}registered,./foostill resolves tofoo.tswhen bothfoo.tsandfoo.web.tsexist. - Compilers resolve file suffixes, not directory
indexsuffixes.from "../hooks"never reacheshooks/index.web.ts. Import the module itself (../hooks/useDevToolsStorage).
A jest/** helper sits outside the project glob, so knip cannot see it importing a barrel export.
Only typecheck catches that, which is why it runs before trusting a clean knip report.
Reviewing
For a new package, flag this and point here:
- a
./*wildcard inpackage.json#exports— new packages must enumerate explicit exports so knip can detect zombie files.
The workspaces block in support/knip/knip.json is a temporary workaround
Everything must work without touching knip.json. The workspaces entries that remain are
band-aids for packages that don't declare their surface properly yet — deep imports behind a ./*
wildcard, runtime entries invisible to package.json (Electron preloads, web workers), or
dependencies knip can't resolve. Each one hides a package that hasn't finished the migration above.
So: don't grow this file. Fixing the package's exports is the real fix, and it lets the
corresponding workspaces entry be deleted. If you truly cannot avoid an entry, keep it to the
smallest possible delta and remember a configured entry replaces knip's default patterns
rather than extending them.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

