javascriptwebpackimportcircular-dependencydirectory-structure

Webpack circular dependency while importing from index files


I have the current folder structure

├── files/
│   └── utils.ts
│   └── index.ts
├── api/
│   ├── api.ts
│   ├── utils.ts
│   └── index.ts

Content:

// files/utils.ts
import api from 'api'   // imported directly from api/index.ts
export const fileToBase64 = (...) => ... // does something
export const downloadFile = (...) => api.get(...)  // downloads a file using api

// files/index.ts
export * from './utils'


// api/utils.ts
import { fileToBase64 } from 'files' // imported directly from files/index.ts
export const parseData = (...) => ... fileToBase64(...)... // does something with fileToBase64

// api/api.ts
import { parseData } from './utils'
export const api = { 
   post: (...) => ... const newData = parseData(...) ..., // uses parseData from './utils'
   get... 
}

// api/index.ts
export * from './api'

Both eslint and dpdm package complain about circular dependency. Reason:

The only reason they complain is because I do imports from index files

Actually there is no circular dependency, because:

As a proof, when I move fileToBase64 into separate file files/convertFile.ts and in api/utils.ts import it from files/convertFile (not from files/index), both tools stop complaining about circular dependency

I have 2 questions here:


Solution

  • This pattern of using index files is also known as a barrel export. While the pattern provides for cleaner imports in many cases, there are quite some challenges with using excessive barrels in terms of performance and tree-shaking (especially more so when you use namespaced exports like export * from './utils').

    Now, let's come back to bundlers. Bundlers generally work at file/module level. The tree-shaking optimization comes later in the end after the files have been bundled together into a single file. So, from what you have shared, you do have a cycle in your code. It goes like this:

    api/utils -> files/index -> files/utils -> api/index -> api/api -> api/utils
    

    Even if you are not actually using all the functions, the bundler does not see that at export level. It always looks for circular dependencies at module-level.

    Your approach of moving the common functions into a dedicated file is the correct approach. You have to do exactly that. You can either keep it in multiple files or have one file for such common functions ensuring no circular dependency.

    As a side note, think of your system modules in layers. A layer n can talk to layer n+1 but not the other way round. So, if we consider api and files to be two modules, then think of which one is the higher order module that calls into lower-order module e.g. api -> files.