npm package: @jdb8/unbarrel
Description: Pre-resolve barrel file imports automatically for faster development
Latest version: 0.3.1

# unbarrel

[![main](https://github.com/jdb8/unbarrel/actions/workflows/main.yml/badge.svg)](https://github.com/jdb8/unbarrel/actions/workflows/main.yml) ![version](https://badgen.net/npm/v/@jdb8/unbarrel)

[Barrel files](https://tkdodo.eu/blog/please-stop-using-barrel-files#what-are-barrel-files) are often a [source of slowness](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/) in large codebases. While it's [probably better to get rid of them entirely for new code](https://x.com/_developit/status/1842225029724999761), 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

```bash
npm install -D @jdb8/unbarrel
```

## Usage

### Node

```js
import { unbarrel } from "@jdb8/unbarrel";
const result = await unbarrel(options); // TODO: document options
```

### Webpack

```ts
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](https://tkdodo.eu/blog/please-stop-using-barrel-files#what-are-barrel-files).

unbarrel takes a resolver function and an input file:

```ts index.ts
import { deepImport } from "some-barrel-package";
```

where `some-barrel-package`'s `index.js` looks like:

```ts some-barrel-package/index.js
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:

```ts index.ts
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:

```bash
bun install
bun run build
```

To run a benchmark:

```bash
bun run benchmark
```

You'll see something like:

```bash
------------------------------ 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](https://github.com/jdb8/unbarrel/commits/main/src/vite-plugin-debarrel.ts), and wrapper around, the [`debarrel` vite plugin](https://gist.github.com/developit/d2047ccb47d15b01486484fb79a2f51a) written by [@developit](https://github.com/developit). Make sure to watch [his excellent ViteConf talk](https://viteconf.org/24/replay/scaling_vite) alongside the [corresponding tweet thread](https://x.com/_developit/status/1842225012092104732)!

Additional resources around barrel files:

-   https://tkdodo.eu/blog/please-stop-using-barrel-files
-   https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/