<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wojciech Kot</title>
    <description>The latest articles on DEV Community by Wojciech Kot (@wojciech_kot_b82f5d7cbfc6).</description>
    <link>https://dev.to/wojciech_kot_b82f5d7cbfc6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1937511%2Fec153212-0a22-4f59-ac2b-157f4a5e906b.png</url>
      <title>DEV Community: Wojciech Kot</title>
      <link>https://dev.to/wojciech_kot_b82f5d7cbfc6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wojciech_kot_b82f5d7cbfc6"/>
    <language>en</language>
    <item>
      <title>Dead Code kills silently</title>
      <dc:creator>Wojciech Kot</dc:creator>
      <pubDate>Thu, 02 Jul 2026 21:26:00 +0000</pubDate>
      <link>https://dev.to/wojciech_kot_b82f5d7cbfc6/dead-code-kills-silently-i9l</link>
      <guid>https://dev.to/wojciech_kot_b82f5d7cbfc6/dead-code-kills-silently-i9l</guid>
      <description>&lt;p&gt;Unused JavaScript is like cancer — it kills slowly. It accumulates quietly over the course of years, release by release, and by the time anyone notices something is wrong the damage is already significant. No build error, no crash, everything seems to work — just a bundle that is silently 40% larger than it needs to be, a Time to Interactive that is consistently a second slower than it should be, and nobody knows why.&lt;/p&gt;

&lt;p&gt;This article presents a bunch of ways how to find unused code, remove it, and configure tools and bundler to prevent dead code in the future. Sections for bundler are based on set of &lt;a href="https://vite.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;, which under the hood delegates to &lt;a href="https://rollupjs.org/" rel="noopener noreferrer"&gt;Rollup&lt;/a&gt; in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why unused JavaScript actually hurts
&lt;/h2&gt;

&lt;p&gt;Every byte of JavaScript has a runtime cost. It is not just the download — the browser also has to parse and compile every line of code in your bundle before the page becomes interactive. You and at the end your customers pay that cost even for code that will never run.&lt;/p&gt;

&lt;p&gt;It is not just a technical problem, bad practice that makes your code unreadable and hard to maintain, it is also real product issue affecting its whole lifecycle and causing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slower initial load&lt;/strong&gt; — users download JavaScript they will never execute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher Time to Interactive&lt;/strong&gt; — the browser engine compiles every byte before the page becomes responsive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpredictable bundle sizes&lt;/strong&gt; — without automated enforcement, dead code accumulates quietly across releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, this is not a process that cannot be prevented or reversed although the fix is not a one-time cleanup. It is a set of tools and bundler configuration that catches dead code automatically, at the point it is written and at build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find unused code before the build
&lt;/h2&gt;

&lt;p&gt;The best time to catch unused code is before it ever reaches the bundle. Different tools cover different scopes — from a single file up to the full project graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript compiler flags
&lt;/h3&gt;

&lt;p&gt;The TypeScript configuration has a bunch of flags that turn unused code into compile-time errors — unused locals, unused parameters, unreachable statements. They operate at the file level, meaning the compiler checks each file individually in isolation from the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedLocals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowUnreachableCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isolatedModules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiler will error on unused variables, function parameters, and unreachable code statements causing a build failure. The last two flags go a step further on the bundle side: &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; ensures type only imports are erased at compile time rather than pulled into the bundle as  dependencies, and &lt;code&gt;isolatedModules&lt;/code&gt; guarantees type exports are fully stripped at build time rather than emitted as real TypeScript references.&lt;/p&gt;

&lt;p&gt;There is one miss - an issue the TypeScript compiler cannot detect. It sees individual files as standalone entities, not the project as a whole. A function can be exported and never imported anywhere but TypeScript will not warn about it. That is where &lt;code&gt;knip&lt;/code&gt; comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  knip — whole-project unused export detection
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://knip.dev" rel="noopener noreferrer"&gt;knip&lt;/a&gt; is the tool for analysis the full project and detect exports that are never imported anywhere, files that are never referenced, and &lt;code&gt;package.json&lt;/code&gt; dependencies that are never used in source code. It is the tool that closes the gap TypeScript leaves open — seeing only individual files instead of full project and cannot verify if an export is never consumed anywhere in the project.&lt;/p&gt;

&lt;p&gt;A typical knip report looks like this. We can see that generated errors refer to the full project scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unused exports (2)
src/utils/format.ts
  formatCurrency   line 14   ← exported but never imported anywhere

src/api/index.ts
  LegacyApiClient  line 45   ← exported but never imported anywhere

Unused files (1)
src/deprecated/old-connector.ts

Unused dependencies (1)
  lodash   ← in package.json but never imported in source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a &lt;a href="https://marketplace.visualstudio.com/items?itemName=knip.knip-vscode" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; that allows for the same findings inline as you edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  ESLint rules for dead code detection
&lt;/h3&gt;

&lt;p&gt;Most projects already run ESLint. A couple of rules added to config can give instant feedback on dead code directly in the editor.&lt;/p&gt;

&lt;p&gt;Just extending config file &lt;code&gt;.eslintrc&lt;/code&gt; with few lines can help to catch a lot of unused entites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// disabled — the TypeScript-aware version below replaces it to avoid duplicate warnings&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"no-unused-vars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/no-unused-vars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// catches expressions whose result is computed but never used — dead code no-unused-vars misses&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/no-unused-expressions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these two rules, any unused variable, import, parameter, or dead expression gets underlined in the editor the moment it is written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure tree-shaking
&lt;/h2&gt;

&lt;p&gt;Tree-shaking is the bundler's automatic mechanism of dead code elimination at build time. Rollup — which Vite uses in production — traces every &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt; from the entry point, marks everything reachable, and removes the rest - unused before producing the final bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tree-shaking is on by default in every &lt;code&gt;vite build&lt;/code&gt;.&lt;/strong&gt; It is not required to enable it. What you do need to do is just ensure the conditions it depends on are actually met — there are several ways they silently are not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declare &lt;code&gt;"sideEffects"&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A side effect is any code that runs the moment a file is imported, regardless of whether any export is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ side effect — runs on import even if nothing from this file is used&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__appVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ no side effects — nothing runs on import, only exports are defined&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`£&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in many cases these files are not used, can be just some unintended remainders. When Rollup finds such a side effect, it wants to drop it from bundle — but it cannot do that blindly. It assumes the worst and keeps every module, which makes tree-shaking ineffective. The way to enforce Rollup to drop any side effects is to set property&lt;code&gt;"sideEffects"&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; to false. It is the information for Rollup to remove side effects when finding them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sideEffects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field accepts three forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;safe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unused&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sideEffects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;these&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;specific&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;side&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;effects&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sideEffects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src/polyfills.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.css"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;omitted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rollup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keeps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;modules&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;regardless&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;usage&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most projects &lt;code&gt;false&lt;/code&gt; is the correct value. The most common real exception is CSS. A CSS import such as &lt;code&gt;import './styles.css'&lt;/code&gt; has no exports — it works entirely as a side effect by injecting styles into the DOM. Setting &lt;code&gt;"sideEffects": false&lt;/code&gt; globally would cause Rollup to silently discard all CSS imports. List affected files explicitly instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sideEffects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*.css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.scss"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ensure ES module output from shared packages
&lt;/h3&gt;

&lt;p&gt;Tree-shaking strongly depends on static &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt; syntax — it is the only syntax that gives Rollup enough information to determine what is actually used. CommonJS (&lt;code&gt;require&lt;/code&gt;/&lt;code&gt;module.exports&lt;/code&gt;) is resolved at runtime, so Rollup cannot analyse it statically and takes conservative approach for keeping the entire module. It is very important for shared packages - if they only ships a CommonJS build, tree-shaking is silently disabled for it in every consuming app, regardless of how those apps are configured.&lt;/p&gt;

&lt;p&gt;In the package you publish, declare an ES module entry point alongside the CommonJS one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CommonJS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rollup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cannot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tree-shake&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Both&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;outputs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;declared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rollup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;picks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ESM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.esm.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.esm.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The paths must match exactly what your build produces. Run &lt;code&gt;yarn build&lt;/code&gt;, inspect the output in &lt;code&gt;dist/&lt;/code&gt;, and copy those exact paths into &lt;code&gt;package.json&lt;/code&gt;. A path pointing to a non-existent file causes Rollup to fall back to &lt;code&gt;"main"&lt;/code&gt;  and as a result tree-shaking for that package is disabled without any indication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-patterns that silently defeat tree-shaking
&lt;/h2&gt;

&lt;p&gt;Even with correct configuration, certain code patterns prevent Rollup from eliminating unused modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Barrel files with &lt;code&gt;export *&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shared/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./format-utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 12 utility functions&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./date-utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 8 utility functions&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// large HTTP client&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// configuration constants&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a consumer writes &lt;code&gt;import { formatPrice } from './shared'&lt;/code&gt;, Rollup encounters the wildcard re-export and cannot statically determine which exports from each module will be used in practice.  For safety it pulls in all modules even this never referenced.&lt;/p&gt;

&lt;p&gt;Replace wildcard re-exports with named ones so Rollup can trace the dependency graph precisely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Rollup can trace exactly what is available and what is used&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatCurrency&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./format-utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./date-utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TIMEOUT&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip the barrel entirely and import directly from the source file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ No barrel, no ambiguity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatPrice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@your-scope/utils/format-utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;require()&lt;/code&gt; in shared package source
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ A single require() forces CommonJS treatment for the entire file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;utils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single &lt;code&gt;require()&lt;/code&gt; call anywhere in a file causes Rollup to treat it as CommonJS and skip tree-shaking it entirely. All source files in published shared packages must use &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt; exclusively — mixing module formats in the same file is not supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the production bundle
&lt;/h2&gt;

&lt;p&gt;After making changes, you need to confirm they worked. Build output printed to the terminal gives you chunk sizes, but &lt;code&gt;rollup-plugin-visualizer&lt;/code&gt; gives you the full picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/btd/rollup-plugin-visualizer" rel="noopener noreferrer"&gt;&lt;code&gt;rollup-plugin-visualizer&lt;/code&gt;&lt;/a&gt; is one of the most widely used bundle analysis tools in the Rollup and Vite ecosystem — well-maintained, with a large community. It works as a Vite plugin (Vite delegates to Rollup in production, so the integration is seamless) and adds no overhead to normal builds.&lt;/p&gt;

&lt;p&gt;After an analysis build it opens an interactive &lt;code&gt;html&lt;/code&gt; report in the browser automatically. The report renders the entire bundle as a treemap — every module represented as a rectangle, sized by its byte weight, grouped by chunk and package. The result makes clearly visible which packages dominate the bundle, what actually changed after an optimisation, and what is still there that probably should not be.&lt;/p&gt;

&lt;p&gt;It also supports multiple visualisation modes — treemap, sunburst, network graph, and flamegraph — each suited to a different diagnostic question, from understanding what is taking up space to tracing why a specific module ended up in the bundle at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these steps on their own are enough. TypeScript flags unused locals, but misses unused exports. knip catches unused exports, but only runs when you invoke it. The bundler eliminates dead code at build time, but only if you have set up &lt;code&gt;"sideEffects"&lt;/code&gt; and ES module output correctly.&lt;/p&gt;

&lt;p&gt;Set them all up once. After that, unused code gets caught automatically — at the point it is written, in CI, and at build time. The bundle stays small not because someone remembered to clean it up, but because the tooling makes shipping dead code actively difficult.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>deadcode</category>
      <category>javascript</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Stop circular dependencies before they stop you — dependency-cruiser &amp; the Stable Dependencies Principle</title>
      <dc:creator>Wojciech Kot</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:22:03 +0000</pubDate>
      <link>https://dev.to/wojciech_kot_b82f5d7cbfc6/stop-circular-dependencies-before-they-stop-you-dependency-cruiser-the-stable-dependencies-34ho</link>
      <guid>https://dev.to/wojciech_kot_b82f5d7cbfc6/stop-circular-dependencies-before-they-stop-you-dependency-cruiser-the-stable-dependencies-34ho</guid>
      <description>&lt;p&gt;Circular dependencies are one of those bugs that feel harmless right up until they aren't. No build error. No runtime exception on import. Just a subtle undefined that shows up weeks later in production, in a stack trace that points to the symptom and not the cause.&lt;br&gt;
This article shows how to catch them automatically using dependency-cruiser, and how to go one step further by encoding the Stable Dependencies Principle (SDP) directly into your project rules.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is a circular dependency?
&lt;/h2&gt;

&lt;p&gt;A circular dependency happens when module A imports from B, and B — directly or through a chain — imports back from A.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → A          (direct cycle)
A → B → C → A      (indirect cycle)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice it often looks harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// user-context.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAccessStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./access-status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// access-status.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./user-context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// cycle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why JavaScript makes this dangerous
&lt;/h2&gt;

&lt;p&gt;JavaScript resolves circular imports silently. No build error, no runtime exception — just wrong behaviour that is hard to trace.&lt;br&gt;
When module A and B are in a cycle, whichever loads first will often see the other's exports as undefined (or trigger a ReferenceError in strict ESM environments) at the exact moment it evaluates. Here is exactly what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1 — user-context.ts starts loading, hits the import of access-status.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2 — access-status.ts starts loading, hits the import of user-context.ts&lt;/span&gt;
&lt;span class="c1"&gt;//           user-context.ts is already loading but not finished&lt;/span&gt;
&lt;span class="c1"&gt;//           JavaScript returns what has been exported so far → nothing → UserContext is undefined&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3 — access-status.ts finishes loading with undefined as UserContext&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultPermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPermissions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Step 4 — user-context.ts finishes, but access-status.ts already captured undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug shows up later when getAccessStatus is called, fails silently, and the stack trace points at the call site — not the import structure that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting cycles with &lt;a href="https://www.npmjs.com/package/dependency-cruiser" rel="noopener noreferrer"&gt;dependency-cruiser&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of guessing where these cycles hide, you can use dependency-cruiser. It scans your source files, maps out your entire dependency graph, and validates it against rules you define. It works right out of the box with TypeScript, path aliases, and monorepos like Yarn workspaces.&lt;/p&gt;

&lt;p&gt;Getting it up and running takes few minutes. First, add it into your dev dependencies&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add &lt;span class="nt"&gt;-D&lt;/span&gt; dependency-cruiser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add a shortcut script to your &lt;code&gt;package.json&lt;/code&gt; pointing to your configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"depcruise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"depcruise packages --config .dependency-cruiser.js"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create that &lt;code&gt;.dependency-cruiser.js&lt;/code&gt; file at the root of your repository. We will start with a basic configuration designed purely to catch circular dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('dependency-cruiser').IConfiguration} */&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;forbidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-circular&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tsConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tsconfig.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;doNotFollow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.spec&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.tsx?$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;moduleSystems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the rules in place, you can kick off the check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn depcruise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your codebase is clean, the command runs silently and exits with code 0. However, if a cycle exists in your project, you'll see a violation that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
      &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
      &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
      &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
      &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of each → as a single import statement. The output traces the entire chain of dependency until the final line loops straight back to the first. That loop is exactly what you need to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting cycles is reactive. The SDP is proactive.
&lt;/h2&gt;

&lt;p&gt;Finding a cycle and breaking it is the right fix for the immediate problem. But it does not address why the cycle formed. And if you do not address that, it will form again somewhere else.&lt;br&gt;
Cycles are a symptom. The root cause is a dependency arrow pointing the wrong way - a stable module that has been made to depend on an unstable one. The cycle is just the inevitable byproduct of that structural mistake.&lt;/p&gt;

&lt;p&gt;The Stable Dependencies Principle (SDP) is a design rule from Robert C. Martin's Clean Architecture that formalises this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Depend in the direction of stability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stability here is a structural property, not a code quality score. A module is stable when many other modules depend on it — any change would ripple across all those consumers, and that cost acts as a natural brake. A module that nothing depends on is free to change at any time — structurally unstable, even if the code inside it is excellent.&lt;br&gt;
Martin defines instability (I) as a ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;I &lt;span class="o"&gt;=&lt;/span&gt; Fan-Out / &lt;span class="o"&gt;(&lt;/span&gt;Fan-In + Fan-Out&lt;span class="o"&gt;)&lt;/span&gt;

Fan-In  &lt;span class="o"&gt;=&lt;/span&gt; modules that import this one
Fan-Out &lt;span class="o"&gt;=&lt;/span&gt; modules this one imports from

I &lt;span class="o"&gt;=&lt;/span&gt; 0  →  maximally stable    &lt;span class="o"&gt;(&lt;/span&gt;everything depends on it, it depends on nothing&lt;span class="o"&gt;)&lt;/span&gt;
I &lt;span class="o"&gt;=&lt;/span&gt; 1  →  maximally unstable  &lt;span class="o"&gt;(&lt;/span&gt;it depends on many things, nothing depends on it&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule stated in terms of I: a module's instability score must be higher than the score of every module it imports from. The dependency arrow must always point toward lower I — toward something that changes less often than you do.&lt;/p&gt;

&lt;p&gt;By engineering your project around the SDP, you ensure that your most heavily relied-upon code never points toward volatile, fast-changing modules. You stop treating dependency design as a matter of gut feeling and turn it into a structural rule. When arrows are strictly forced to point toward stability, you don't just find cycles faster—you make hard for them to form in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a healthy dependency graph looks like
&lt;/h2&gt;

&lt;p&gt;The SDP gives you one rule for every arrow in your graph: it must point toward lower I — toward something more stable than the module drawing the arrow.&lt;/p&gt;

&lt;p&gt;In practice that means your most reused, most depended-upon code sits at the bottom of the graph, and your most frequently changing code sits at the top. Nothing at the bottom imports from anything at the top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;✅  Correct — arrows always point toward stability
  ┌──────────────────────────────────────┐
  │  High-level modules  &lt;span class="o"&gt;(&lt;/span&gt;I ≈ 1.0&lt;span class="o"&gt;)&lt;/span&gt;       │  change often
  │  pages, app entry, top-level config  │  nothing &lt;span class="k"&gt;else &lt;/span&gt;imports from them
  └──────────────────┬───────────────────┘
                     │
                     ▼
  ┌──────────────────────────────────────┐
  │  Mid-level modules  &lt;span class="o"&gt;(&lt;/span&gt;I ≈ 0.5&lt;span class="o"&gt;)&lt;/span&gt;        │  change occasionally
  │  domain logic, feature modules       │  some things depend on them
  └──────────────────┬───────────────────┘
                     │
                     ▼
  ┌──────────────────────────────────────┐
  │  Low-level modules  &lt;span class="o"&gt;(&lt;/span&gt;I ≈ 0.0&lt;span class="o"&gt;)&lt;/span&gt;        │  rarely change
  │  utils, types, constants, base hooks │  everything depends on them
  └──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❌  Wrong — an arrow points upward &lt;span class="o"&gt;(&lt;/span&gt;toward instability&lt;span class="o"&gt;)&lt;/span&gt;

  ┌──────────────────────────────────────┐
  │  High-level modules  &lt;span class="o"&gt;(&lt;/span&gt;I ≈ 1.0&lt;span class="o"&gt;)&lt;/span&gt;       │
  └──────────────────────────────────────┘
            ▲
            │  ← this import is the SDP violation
  ┌──────────────────────────────────────┐
  │  Low-level modules  &lt;span class="o"&gt;(&lt;/span&gt;I ≈ 0.0&lt;span class="o"&gt;)&lt;/span&gt;        │
  └──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upward arrow does not have to produce a cycle immediately. But it creates the structural condition in which a cycle will eventually form — because the high-level module will evolve, import more things, and at some point one of those imports will close the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding SDP as dependency-cruiser rules
&lt;/h2&gt;

&lt;p&gt;This is where dependency-cruiser becomes more than a cycle detector — it becomes an architectural rule engine. You declare which import directions are forbidden, and it catches a violation the moment the import is written, before a cycle has a chance to form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;forbidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-circular&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="c1"&gt;// low-level modules must never import from mid- or high-level ones&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-utils-to-features&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stable utils must not depend on less stable feature modules.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^src/utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^src/(features|pages)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="c1"&gt;// mid-level modules must never import from high-level ones&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-features-to-pages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Feature modules must not depend on pages.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^src/features&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^src/pages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;no-utils-to-features is not an arbitrary naming convention. It is the SDP stated as a machine-checkable rule: a module with I≈0 must not import from modules with I≈0.5 or higher. A developer who adds that import sees a CI failure immediately, on that line, with that rule name. The cycle does not need to exist yet.&lt;br&gt;
This is the distinction that matters: cycle detection tells you when a forbidden state has already been created. A direction rule catches the violation before that state can form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Circular dependencies aren't a tooling problem; they are a structural failure. A cycle is simply what happens when dependency arrows point the wrong way for long enough.&lt;/p&gt;

&lt;p&gt;dependency-cruiser gives you two layers of defense: a cycle detector that catches existing fires, and an architectural rule engine that prevents them entirely. While the first is a useful safety net, the second is what actually keeps a codebase clean as it scales.&lt;/p&gt;

&lt;p&gt;The Stable Dependencies Principle provides the "why" behind these rules. Forcing stable code to depend on volatile code isn't just a bad convention. That hidden coupling is exactly what eventually surfaces as a silent undefined in production.&lt;/p&gt;

&lt;p&gt;The configuration file is tiny, and the principle is simple. Together, they transform dependency management from a reactive cleanup task into an automated guardrail that holds the architectural line on every single PR.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>cleancode</category>
    </item>
  </channel>
</rss>
