@jdb8/unbarrel v0.3.1
unbarrel
Barrel files are often a source of slowness in large codebases. While it's probably better to get rid of them entirely for new code, in some situations it may be unavoidable, undesirable, or costly to banish them wholesale. It would be great if there was a way to support barrel files without the heavy performance drawbacks!
Install
npm install -D @jdb8/unbarrel
Usage
Node
import { unbarrel } from "@jdb8/unbarrel";
const result = await unbarrel(options); // TODO: document options
Webpack
const config: webpack.Configuration = {
module: {
rules: [
{
test: /\.(t|j)sx?$/, // whichever file extensions you expect to unbarrel
use: require.resolve("@jdb8/unbarrel"),
// This will exclude node_modules files from being unbarreled,
// but will still be traversed internally
exclude: /node_modules/,
},
],
},
};
How does it work?
If you are working on a monorepo containing many interlinked packages, and each of those packages exports its code from a central index.ts
entrypoint file, you are likely experiencing build tooling inefficiencies as a result of these barrel files.
unbarrel takes a resolver function and an input file:
import { deepImport } from "some-barrel-package";
where some-barrel-package
's index.js
looks like:
export { foo } from "./Foo.js";
export { bar } from "./Bar.js";
export { deepImport } from "./thing-my-caller-actually-wants.js";
and will output an "unbarreled" output file:
import { deepImport } from "/abs/path/to/some-barrel-package/thing-my-caller-actually-wants.js";
Now, tools that would previously have traversed Foo.js
and Bar.js
will completely ignore their existence. You could imagine that Foo.js
and Bar.js
each have many transitive imports into other packages, so at a high enough number of modules in the graph this can be meaningful.
FAQ
Is this safe?
Not really. Barrel files can have side effects which means that it's not completely safe to run this against all files in an unknown codebase, but if you're in control of the source barrel files you can guarantee code is equivalent.
Why isn't this a babel/eslint/<other tool>
plugin?
In order to traverse module imports, we need access to a resolver function, which fits more naturally at a layer of the build pipeline that can provide us one. Bringing our own resolver function also risks incompatibilities with the bundler's own at a higher level of the pipeline.
While it's possible to teach a babel plugin to traverse the filesystem, it goes against the spirit of processing one file at a time in isolation and starts to make things more complicated.
ESLint has some prior art for linting across import boundaries (eslint-plugin-import
) and so would be viable if the goal is to autofix imports to deep imports (perhaps this package's node api could be used under the hood), but this tool aims to leave the code untouched.
Other tools can implement wrappers around the node API by passing in a compatible resolver function from whichever tool is providing one. Jest is one that would probably be useful.
Doesn't tree-shaking already fix this?
Bundler tree-shaking is designed to reduce the amount of code that ends up in the resulting bundle, but it still needs to construct a module graph for every file being imported, so tree-shaking cannot provide a build-time performance boost. The only way to avoid the cost of loading/parsing/transpiling unnecessary files is to remove them from consideration entirely!
Why does the tool output absolute paths?
No strong reason, but in some cases providing absolute paths to tooling can allow it to skip additional lookup logic.
Development
This package is built with bun
. Install bun
on your machine, and then install and build dependencies with:
bun install
bun run build
To run a benchmark:
bun run benchmark
You'll see something like:
------------------------------ LOADER ENABLED: true ------------------------------
[567.80ms] compile true
# modules: 109
asset main.js 13 KiB [emitted] [minimized] (name: main)
orphan modules 68.7 KiB [orphan] 108 modules
./test-app-code.ts + 108 modules 68.8 KiB [built] [code generated]
webpack 5.95.0 compiled successfully in 568 ms
------------------------------ LOADER ENABLED: false ------------------------------
[686.04ms] compile false
# modules: 641
asset main.js 13 KiB [emitted] [minimized] (name: main)
orphan modules 613 KiB [orphan] 640 modules
./test-app-code.ts + 108 modules 68.8 KiB [built] [code generated]
webpack 5.95.0 compiled successfully in 686 ms
which shows that the unbarrelling logic has taken place.
Acknowledgements
This repo began as a fork of, and wrapper around, the debarrel
vite plugin written by @developit. Make sure to watch his excellent ViteConf talk alongside the corresponding tweet thread!
Additional resources around barrel files: