0.3.1 • Published 6 months ago

@jdb8/unbarrel v0.3.1

Weekly downloads
-
License
MIT
Repository
-
Last release
6 months ago

unbarrel

main version

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:

0.3.1

6 months ago

0.3.0

6 months ago

0.2.0

6 months ago