📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~8 min de lecture

knip sur Angular + Nx : traquer le code mort sans crouler sous les faux positifs

Tu as une dépendance dans ton package.json que plus personne n'importe. Un export public que zéro fichier ne consomme. Une interface de props laissée là après un refacto. Rien de tout ça ne casse le build, rien ne fait planter les tests. La dette de code mort se reconstitue en silence : chaque dépendance fantôme est une ligne de plus à auditer à chaque faille de sécurité, chaque export orphelin une fausse piste pour le prochain qui lira le fichier.

knip est l'outil qui débusque ça. Mais si tu le lances tel quel sur un projet Angular SSR + Nx, il va te noyer sous les faux positifs dès le premier run. Voici comment je l'ai câblé proprement sur ce repo, et le piège exact que j'ai rencontré.

Le problème : trois types de code mort, zéro alarme

Sur un audit du repo, trois choses sont remontées. Je ne les ai pas dénichées à la main : elles sont sorties d'un audit automatisé qu'on fait tourner sur le repo EasyAngularKit, sous forme de findings priorisés. knip n'était qu'un des outils du passage. Aucune des trois ne déclenchait quoi que ce soit en local ou en CI.

Une dépendance fantôme. @angular/forms était listé dans le package.json. Zéro import runtime sous src/. Pas un FormsModule, pas un ReactiveFormsModule, pas un FormControl. La seule occurrence du mot "form" vivait dans le contenu rédactionnel d'articles de blog. La dépendance était installée, téléchargée, versionnée, pour rien.

Des exports jamais consommés. Des helpers comme getModuleBySlug et getModuleByIndex, une interface CtaButtonProps définie puis abandonnée. Du code que TypeScript compile sans broncher parce qu'un export est valide même sans consommateur.

Des exports faussement publics. getDisplayTitle et toModuleCardData étaient exportés, mais utilisés uniquement en interne par la constante MODULE_CARDS dans le même fichier. L'export était mort, pas la fonction.

Le point commun : aucun de ces trois cas n'est détectable par le compilateur ni par les tests. Il faut un outil qui construise le graphe de dépendances complet du projet et cherche les noeuds que personne ne pointe.

knip brut = bruyant

Premier réflexe : pnpm dlx knip. Et là, douche froide. La sortie liste comme "unused files" des fichiers qui sont au contraire critiques :

Unused files (5)
src/main.server.ts
src/server.ts
src/app/app.config.server.ts
src/app/app.routes.server.ts
src/styles.css

Unused dependencies (1)
@angular/forms

Unused devDependencies (12)
@angular/cli
@angular/language-service
@nx/workspace
prettier
...

Le @angular/forms est un vrai positif, bravo knip. Mais le reste ?

src/main.server.ts, src/server.ts, src/app/app.config.server.ts, src/app/app.routes.server.ts sont les entrypoints SSR d'Angular. Personne ne les importe depuis le code applicatif, et c'est normal : ce sont les points d'entrée que le build serveur et le prerender consomment via les targets server et prerender déclarées dans project.json. knip ne lit pas la config Nx. Pour lui, un fichier que rien n'importe est un fichier mort. Il a tort, mais il ne peut pas savoir. (Le cinquième, src/styles.css, est un autre cas : il sortira du radar dès qu'on restreindra le périmètre d'analyse au TypeScript, on y revient.)

Même logique pour les douze devDependencies. @angular/cli, @nx/workspace, prettier, typescript-eslint... aucune n'est importée dans du code source. Elles sont invoquées via la CLI ou référencées par des fichiers de config (eslint.config.js, .prettierrc) que knip ne relie pas toujours à leur binaire. Faux positifs, tous.

Le danger ici est insidieux. Un outil de qualité qui crie au loup à chaque run, c'est un outil qu'on finit par ignorer, ou pire, qu'on désactive. Le bruit tue le signal. Si tu veux que knip serve à quelque chose en CI, il faut d'abord lui apprendre la topologie réelle de ton projet.

La config qui marche

Tout se joue dans un knip.json minimal. Voici celui du repo, commenté :

{
  "$schema": "https://unpkg.com/knip@5/schema.json",

  // Les points d'entree que knip ne peut pas deviner.
  // main.ts est evident ; les *.server.ts + app.config.server.ts
  // + app.routes.server.ts sont consommes par les targets Nx
  // `server` et `prerender`, pas par un import applicatif.
  // tools/*.ts = scripts SSG (generation blog, sitemap, rss) lances en CLI.
  "entry": [
    "src/main.ts",
    "src/main.server.ts",
    "src/server.ts",
    "src/app/app.config.server.ts",
    "src/app/app.routes.server.ts",
    "tools/*.ts"
  ],

  // Le perimetre a analyser. On reste sur le TS source.
  "project": ["src/**/*.ts", "tools/**/*.ts"],

  // Un export utilise UNIQUEMENT dans son propre fichier n'est plus
  // signale comme mort. Sans ca, les types d'API publique references
  // en interne (ConsentPreferences, ModuleVideoData...) ressortent
  // en faux positifs a chaque run.
  "ignoreExportsUsedInFile": true,

  // Le tooling invoque via CLI ou config, que knip ne relie pas
  // a un import. On l'autorise explicitement plutot que de subir
  // 12 fausses alertes a vie.
  "ignoreDependencies": [
    "@angular/cli",
    "@angular/language-service",
    "@eslint/js",
    "@nx/js",
    "@nx/workspace",
    "@semantic-release/github",
    "@swc/helpers",
    "@tailwindcss/typography",
    "angular-eslint",
    "conventional-changelog-conventionalcommits",
    "prettier",
    "typescript-eslint"
  ]
}

Trois leviers, trois intentions distinctes :

  1. entry dit à knip : « ces fichiers sont des racines du graphe, ne les compte jamais comme morts, et suis ce qu'ils importent ». C'est ce qui éteint les quatre entrypoints SSR .ts. Le cinquième faux positif, src/styles.css, tombe tout seul : en restreignant project à src/**/*.ts et tools/**/*.ts, le CSS n'est plus dans le périmètre analysé, donc plus jamais signalé.
  2. ignoreExportsUsedInFile est le réglage le plus sous-estimé. Par défaut, knip considère qu'un export doit être consommé depuis un autre fichier. Or un type d'API publique (un ConsentPreferences, un ModuleVideoData) est souvent défini et ré-utilisé dans son propre fichier de constantes. Sans ce flag, tu te tapes une liste d'exports "morts" qui ne le sont pas. Avec, knip ne flag que ce qui est vraiment orphelin à l'échelle du projet.
  3. ignoreDependencies est une liste d'exceptions assumée, pas un tapis sous lequel on planque la poussière. Chaque entrée est du tooling qu'on sait invoqué hors import. La règle que je me fixe : si tu ne peux pas expliquer en une phrase pourquoi une dép est dans cette liste, c'est qu'elle n'a rien à y faire.

Le piège à éviter absolument : ne mets pas tes vraies dépendances mortes dans ignoreDependencies pour faire taire knip. C'est exactement l'inverse du but. @angular/forms n'a jamais touché cette liste : il a été retiré du projet.

Le câbler en CI : l'anti-régression

Une config locale, c'est bien. Une gate en CI, c'est ce qui empêche la dette de revenir. Deux étapes.

Le script dans package.json :

{
  "scripts": {
    "lint:deps": "knip"
  }
}

Et l'étape dans le workflow GitHub Actions, après le build/lint/test :

- run: pnpm nx run-many -t build lint test --skip-nx-cache
- name: Dead code & dependencies (knip)
  run: pnpm run lint:deps

knip sort en code non-zéro dès qu'il trouve du code mort. À partir de là, toute PR qui réintroduit un export orphelin, une dépendance jamais importée ou un fichier injoignable fait rougir la CI. Le coût d'un ré-import mort passe de « découvert six mois plus tard par hasard » à « bloqué dans les trente secondes de la PR ». C'est tout l'intérêt : transformer une revue manuelle aléatoire en garde-fou automatique.

Ce que ça a vraiment trouvé

Pas de la théorie. Une fois la config en place et le bruit éteint, voilà ce qui restait, et qui a été nettoyé :

  • @angular/forms retiré du package.json. Une dépendance Angular complète qui ne servait plus à rien.
  • getModuleBySlug et getModuleByIndex supprimés : des helpers sans aucun appelant.
  • L'interface CtaButtonProps supprimée : jamais référencée nulle part.
  • getDisplayTitle et toModuleCardData dé-exportés : conservés parce qu'utilisés en interne par MODULE_CARDS, mais leur export mort a sauté. La fonction reste, sa surface publique fictive disparaît.

Après nettoyage : knip sort en exit 0, aucun finding. Le build pré-rend toujours ses routes sans broncher, les tests restent verts. Le diff net, c'est moins de surface d'API à maintenir et un graphe de dépendances qui dit enfin la vérité sur ce que le projet utilise vraiment.

knip vs depcheck vs ts-prune

La question légitime : pourquoi knip et pas les outils qu'on connaît déjà ?

Avant, il fallait empiler plusieurs outils pour couvrir le même périmètre :

  • depcheck pour les dépendances inutilisées du package.json.
  • ts-prune pour les exports TypeScript orphelins.
  • Un script maison ou un autre outil pour les fichiers carrément injoignables.

Trois outils, trois configs, trois formats de sortie, trois sources de faux positifs à dompter séparément. knip fait les trois dimensions dans un seul passage : fichiers morts, dépendances mortes, exports morts, avec une config unique. Sur un monorepo Nx où le graphe est déjà compliqué, ne maintenir qu'un seul outil de détection change la donne.

Est-ce que knip est parfait ? Non. Le prix d'entrée, c'est précisément le knip.json qu'on vient d'écrire : sans lui, l'outil est inutilisable sur un projet SSR + Nx. Mais ce coût est payé une fois, et il rapporte à chaque PR ensuite.

Ce qu'il faut retenir

Le code mort ne déclenche aucune alarme par défaut. Ni le compilateur, ni les tests, ni le linter standard ne le voient. Il faut un outil qui construise le graphe complet et cherche les orphelins.

Sur Angular SSR + Nx, knip brut est inexploitable. Il flag les entrypoints serveur et le tooling CLI comme morts parce qu'il ne lit pas la config Nx. La valeur n'est pas dans npx knip, elle est dans le knip.json qui lui apprend ta topologie.

ignoreDependencies est une liste d'exceptions, pas une poubelle. Si tu ne sais pas justifier une entrée en une phrase, retire la dépendance au lieu de la masquer.

Mets-le en CI ou ne le mets pas du tout. Un détecteur de code mort qu'on lance à la main une fois par trimestre ne sert à rien. En gate de PR, il transforme la régression silencieuse en échec immédiat. C'est le seul mode où il gagne sa place.


Ce nettoyage n'est pas un one-shot : il fait partie d'un audit systématique du repo EasyAngularKit, qui a remonté 37 findings priorisés, dont celui-ci. La vraie discipline n'est pas de lancer knip une fois et de cocher la case, c'est de transformer chaque finding en gate CI plutôt qu'en TODO qu'on oubliera. Un audit te donne la photo à l'instant T. Une gate, elle, t'empêche d'y retourner.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.