Skip to content

Bundle Packages Plugin

A staticbolt plugin that bundles node_modules packages into separate files for the browser. It scans your codebase, collects every import from node_modules, and gives each package a file that exports only what your pages use. The results go to a dedicated output directory.

Each package is bundled into its own file. Sub-packages such as highlight.js/lib/core get their own file too.

How much that narrows a package down depends on the package. lodash-es ships one module per function, so taking four of them gives you 26KB instead of 277KB. react is a single CommonJS file, so its bundle is the same size whichever hooks you import.

Installation

The plugin ships as part of @staticbolt/core. You do not need to install anything else.

Usage

In your .staticbolt.ts configuration file:

import { defineConfig } from "@staticbolt/core";
import { bundlePackagesPlugin } from "@staticbolt/core/plugins";
export default defineConfig({
plugins: [bundlePackagesPlugin()],
});

You do not need to configure anything. The plugin finds and bundles every node_modules import on its own, React included.

Chunks

Packages that need to share a module scope are already put in one file for you. React and its dependencies end up in one packages/react.js on their own. chunks is for grouping by preference on top of that: fewer requests, one cache entry, a vendor file named the way you want it.

The key is the output file name. Give it a list to group exactly those packages:

bundlePackagesPlugin({
chunks: {
utils: ["lodash-es", "dayjs", "nanoid"],
highlight: ["highlight.js/**"],
},
});

"highlight.js/*" and "highlight.js/**" both match a package and its sub-packages.

A big library is dozens of packages you did not install and should not have to name. Give it include instead, and the chunk starts from what you named and follows what those packages need:

bundlePackagesPlugin({
chunks: {
highlighting: { include: ["expressive-code"] },
},
});

Three kinds of package are left out of that walk: ones your own code imports, which keep their own file so a page that needs only them does not pull the chunk; ones you name in exclude; and ones reached only through import(), since bundling those in is what ends their laziness. Naming a package in include overrides all three.

Some packages cannot be separated at all. A require or an export * between them stops working across files. Those are pulled in whatever the rules say, and the build tells you which, so you hear about a chunk that grew instead of finding it in the network tab.

If a chunks entry splits packages that have to stay together, the build says so and tells you which entry fixes it.

Script tags

A <script src> that points at a package is bundled too. Nothing is imported from it. The plugin treats it as running the package for its side effects, which is what a library that only registers something needs.

<script type="module" src="@github/relative-time-element"></script>

Stylesheets

A stylesheet that points at a package, linked from HTML or @imported from CSS, is moved into the same output directory. It keeps its own extension and its path inside the package, and the reference is rewritten to match.

<link rel="stylesheet" href="highlight.js/styles/default.css" />
<!-- becomes ./packages/highlight_js/styles/default.css -->
@import "highlight.js/styles/default.css";
/* becomes ../packages/highlight_js/styles/default.css, relative to the importing file */

Any other file type that is linked from HTML or CSS stays where it resolves, under node_modules.

Note

A .js in a directory name becomes _js, so a package such as highlight.js does not collide with the file that is bundled from it.

Images and fonts

An image or font that a package imports from JavaScript is copied into assetsDir under a name that carries a hash of its content, and the import evaluates to its URL, resolved from the bundle's own URL. No base path is involved. The site can be served from anywhere.

// inside a package
import icon from "./assets/icon.png";
// icon === new URL("./icon-3f2a9c1d.png", import.meta.url).href

Duplicate versions

If node_modules holds more than one copy of a package, only one of them can be bundled, and the build warns with the paths it found. Dedupe the install, or pin a version with an override. Set warnDuplicates to false to silence the warning.

A copy that sits further up the tree, as when the project lives inside a larger repository with its own node_modules, is not a conflict: the project's own copy is the one bundled.

Minification

esbuild minifies the bundles during production builds.

Plugin Options

packagesDir

  • Type: string
  • Default: "./packages"

The output directory for bundled packages and for the stylesheets they ship, relative to the build output directory.

bundlePackagesPlugin({
packagesDir: "./vendor",
});

assetsDir

The output directory for the images and fonts that packages import from JavaScript, relative to the build output directory. See Images and fonts.

bundlePackagesPlugin({
assetsDir: "./assets",
});

chunks

  • Type: Record<string, string[] | { include: string[]; exclude?: string[] }>
  • Default: {}

Groups multiple packages into a single output file. The key is the chunk name. A name that ends in /* or /** matches a package and its sub-packages.

An array is taken literally: exactly those packages. include starts from the packages you name and pulls in what they need. See Chunks for what the walk leaves out.

bundlePackagesPlugin({
chunks: {
utils: ["lodash-es", "dayjs", "nanoid"],
highlighting: { include: ["expressive-code"], exclude: ["postcss"] },
},
});

resolvePackage

  • Type: Record<string, Record<string, string | { production?: string; development?: string }>>
  • Default: {}

Overrides the default resolution path for specific package exports. Useful when a package's exports field does not resolve correctly, or when you need to point at a particular build variant.

Give it one path, or separate paths for production and development.

bundlePackagesPlugin({
resolvePackage: {
"react-dom": {
"./client": {
production: "./cjs/react-dom.production.js",
development: "./cjs/react-dom.development.js",
},
},
},
});

define

  • Type: Record<string, string>
  • Default: {}

Global identifiers replaced with a constant while bundling, on top of process.env.NODE_ENV, which is always set. Values are code, so a string needs its quotes. Vue's esm-bundler build, for one, expects its feature flags:

bundlePackagesPlugin({
define: {
__VUE_OPTIONS_API__: "true",
__VUE_PROD_DEVTOOLS__: "false",
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
},
});

warnDuplicates

  • Type: boolean
  • Default: true

Warns when node_modules holds more than one copy of a package. See Duplicate versions. Turn it off when the duplicate is known and the install cannot be deduped.

bundlePackagesPlugin({
warnDuplicates: false,
});