agentleFS
LedgerHQLedgerHQ/ledger-live/.agents/skills/knip-migration/SKILL.md

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.

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
  1. Very specific:

    Only typecheck catches that, which is why it runs before trusting a clean knip report.

    line 132

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. Explicit exports only. 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"
    }
  2. 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> (../.. for libs/<x>, ../../.. for libs/coin-modules/<x>). Do not add a package script for it: pnpm knip:all at 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-native condition, so only the web barrel is derived (devtools/env, and devtools/feature-flags / devtools/pay-card, which have no exports at all). Name src/index.native.ts in the native config.
  • a platform file is reachable only through resolution. devtools/bindings pairs isMockSessionSupported.ts with a .native.ts override and no .web twin, and knip resolves the unsuffixed sibling first, so the native config names src/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, ./foo still resolves to foo.ts when both foo.ts and foo.web.ts exist.
  • Compilers resolve file suffixes, not directory index suffixes. from "../hooks" never reaches hooks/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 in package.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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.