Go Back

Vite/Rollup preserveModules for libraries

Posted: 

One of the keys when building a JavaScript library is ensuring that it can accommodate tree shaking.

In my cursory knowledge of tree shaking, it seems it can occur in 2 major ways

  • Based on modules used - all unused modules are trimmed away
  • Based on module exports - this can trim named exports that are not used in a module

The first option, where entire modules are trimmed seems to be an easier process for webpack and other bundlers to accomplish. It gets trickier when having to determine the specific exports in a file that are used or not. This gets even more complicated when dealing with barrel files.

If you want to deep dive deeper into the subject, the idea of side effects is the key concept to look at and how webpack and other bundlers determine if there are side effects. The main idea is that is there is a side effect (eg. a global of some kind) then the bundler can't trim anything out because it doesn't know if it's used or not.

preserveModules

The output.preserveModules option in Rollup (and by extension Vite) allows you to keep the module structure so that bundlers have an easier time determining what can be tree shaken away. But there is one major caveat that the Rollup docs don't cover (and I think they should).

Where you use preserveModules it keeps the exact directory structure of your source files. This includes the node_modules directory. All bundled dependencies from the node_modules folder will end up in the dist folder inside a node_modules folder.

/dist/node_modules/bundled_dep/index.js

At first glance this doesn't seem like an issues. But node treats the node_modules folder in a unique way, which can lead to confusing bugs.

The solution is to rename the node_modules folder when bundling. We can do this with the build.lib.fileName option in Vite.

const config = defineConfig({
build: {
lib: {
entry: './src/index.ts',
formats: ['es'],
fileName: (format, entryName) =>
`${entryName.replace(/node_modules\//g, 'external/')}.${format}.js`,
},
output: {
preserveModules: true,
preserveModulesRoot: 'src',
},
},
},
});

All we're doing is running a find and replace on the filename of the bundled file to replace node_modules with external. That way Node doesn't treat the dependencies different than a normal JavaScript file.

Remember to use the global option in your regex for find and replace because you can actually have multiple node_modules folder nested.

Good luck!