Externals
Configure external dependencies that should not be bundled
External Dependencies
External dependencies are modules that should not be bundled with your library. Instead, they are expected to be provided by the consuming application. Externalizing dependencies keeps your bundle small and avoids shipping duplicate copies of packages like React or Node.js built-ins.
Packem exposes two related controls:
externals— a top-level array that force-marks specific specifiers (or patterns) as external.rollup.resolveExternals— automatic classification ofpackage.jsondependency groups and Node.js built-ins, plus anexcludelist to force-bundle specific specifiers.
Default Behavior
You usually need no externals configuration at all. By default Packem already externalizes your dependencies, peerDependencies, optionalDependencies, and Node.js built-in modules (fs, path, node:url, …).
The default classification is:
| Group | Externalized by default? |
|---|---|
dependencies | yes (deps: true) |
peerDependencies | yes (peerDeps: true) |
optionalDependencies | yes (optDeps: true) |
| Node.js built-ins | yes (builtins: true) |
devDependencies | no (devDeps: false) |
Because of this, most libraries only need to declare their runtime packages in package.json and let Packem do the rest. Reach for the options below when you need to deviate from the defaults.
The externals Array
Use the top-level externals option to force-externalize additional specifiers that would not otherwise be classified as external. It accepts an array of strings and/or RegExp values — it is not a function.
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
externals: ['react', 'react-dom'],
})- Strings are matched against the import specifier.
RegExpvalues are tested against the import id, which is handy for whole scopes or families of packages.
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
externals: [
'react',
'react-dom',
/^@myscope\//, // every package under the @myscope/ namespace
],
})React library example
A typical React component library declares react and react-dom as peer dependencies. They are then externalized automatically. If you also want to make sure the JSX runtime entry points stay external, list them explicitly:
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
externals: ['react', 'react-dom', 'react/jsx-runtime'],
})Monorepo scope example
To keep every workspace package under a shared scope external (so each one is resolved at runtime rather than inlined), use a RegExp:
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
externals: [/^@mycompany\//],
})Automatic Classification: rollup.resolveExternals
rollup.resolveExternals controls how Packem classifies dependency groups and Node.js built-ins. You only need it when you want to change the defaults shown above.
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
rollup: {
resolveExternals: {
deps: true,
peerDeps: true,
optDeps: true,
builtins: true,
devDeps: false,
},
},
})Options
| Option | Type | Default | Description |
|---|---|---|---|
builtins | boolean | true | Mark Node.js built-ins (path, fs, …) as external. Set to false to use shims/polyfills instead. |
builtinsPrefix | "add" | "ignore" | "strip" | "add" | How to handle the node: prefix on built-in imports. |
deps | boolean | true | Mark dependencies as external. |
devDeps | boolean | false | Mark devDependencies as external. |
optDeps | boolean | true | Mark optionalDependencies as external. |
peerDeps | boolean | true | Mark peerDependencies as external. |
exclude | (RegExp | string)[] | [] | Specifiers forced to be bundled, overriding every other rule. |
Force-bundling with exclude
The exclude list does the opposite of externals: matching specifiers are forced into the bundle even if they would otherwise be classified as external (for example a dependency or peerDependency). This is useful for tiny, ESM-only packages you would rather inline.
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
rollup: {
resolveExternals: {
// Bundle this package even though it is a dependency:
exclude: ['some-tiny-esm-only-pkg', /^@internal\//],
},
},
})- Strings are matched as exact specifiers.
RegExpvalues are tested against the import id.
exclude overrides all other rules — deps, peerDeps, optDeps, and builtins — so a matched specifier is always bundled.
Bundle everything (for IIFE / standalone output)
When producing a self-contained bundle (such as an IIFE for the browser), turn off every classification so nothing is left external:
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
rollup: {
resolveExternals: {
deps: false,
peerDeps: false,
optDeps: false,
builtins: false,
},
},
})If you only need to bundle a few specific packages rather than everything, list them in exclude instead of disabling whole groups.
Node.js Built-ins and the node: Prefix
builtinsPrefix controls how Packem rewrites imports of Node.js built-in modules:
"add"(default) — turns'path'into'node:path'."strip"— turns'node:path'into'path'."ignore"— leaves names exactly as written.
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
rollup: {
resolveExternals: {
builtins: true,
builtinsPrefix: 'add', // 'path' -> 'node:path'
},
},
})To bundle Node.js built-ins with shims/polyfills instead of externalizing them, set builtins: false.
Validating Externals
You can verify your externalization is working by inspecting the output in the build:done hook:
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
externals: ['react', 'react-dom'],
hooks: {
'build:done': async () => {
const { readFile } = await import('node:fs/promises')
const bundle = await readFile('dist/index.mjs', 'utf-8')
if (bundle.includes('React.createElement')) {
throw new Error('react was bundled instead of being left external')
}
},
},
})Troubleshooting
A package is being bundled when it should be external
- Make sure it is listed in your
package.jsonunderdependencies,peerDependencies, oroptionalDependencies— those groups are externalized by default. - If it is not a declared dependency, add it to the top-level
externalsarray. - Check that it is not matched by an
excludepattern inrollup.resolveExternals, which would force it to be bundled.
A package is external when it should be bundled
- Add the specifier (or a matching
RegExp) torollup.resolveExternals.exclude.excludeoverrides all other classification rules. - Alternatively, disable the relevant group (
deps,peerDeps,optDeps,builtins) if you want to change classification wholesale.
Node.js built-ins appear with an unexpected node: prefix
- Adjust
builtinsPrefixto"add","strip", or"ignore"depending on the prefix style you want in the output.
Related Options
- Output Format - Configure output formats
- Package Exports - Package.json exports