Pkgroll
📦 Zero-config package bundler for Node.js + TypeScript
Install / Use
/learn @privatenumber/PkgrollREADME
pkgroll is a JavaScript package bundler powered by Rollup that automatically builds your package from entry-points defined in package.json. No config necessary!
Write your code in TypeScript/ESM and run pkgroll to get ESM/CommonJS/.d.ts outputs!
Features
- ✅
package.json#exportsto define entry-points - ✅ Dependency externalization
- ✅ Minification
- ✅ TypeScript support +
.d.tsbundling - ✅ Watch mode
- ✅ CLI outputs (auto hashbang insertion)
Install
npm install --save-dev pkgroll
Quick setup
-
Setup your project with source files in
srcand output indist(configurable). -
Define package entry-files in
package.json.These configurations are for Node.js to determine how to import the package.
Pkgroll leverages the same configuration to determine how to build the package.
{ "name": "my-package", // Set "module" or "commonjs" (https://nodejs.org/api/packages.html#type) // "type": "module", // Define the output files "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.cts", // Define output files for Node.js export maps (https://nodejs.org/api/packages.html#exports) "exports": { "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs", }, "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs", }, }, // bin files will be compiled to be executable with the Node.js hashbang "bin": "./dist/cli.js", // (Optional) Add a build script referencing `pkgroll` "scripts": { "build": "pkgroll", }, // ... }Paths that start with
./dist/are automatically mapped to files in the./src/directory.
[!TIP] In addition to
package.json, pkgroll also supports pnpm'spackage.yaml.
- Package roll!
npm run build # or npx pkgroll
Usage
Entry-points
Pkgroll parses package entry-points from package.json by reading properties main, module, types, and exports.
The paths in ./dist are mapped to paths in ./src (configurable with the --srcdist flag) to determine bundle entry-points.
Wildcard exports
Pkgroll supports wildcard patterns in package.json#exports for exporting multiple modules with a single pattern:
{
"exports": {
"./utils/*": "./dist/utils/*.mjs",
"./components/*/index": "./dist/components/*/index.mjs",
},
}
This automatically bundles all matching source files. For example:
src/utils/format.ts→dist/utils/format.mjssrc/components/button/index.ts→dist/components/button/index.mjs
[!IMPORTANT] Wildcard patterns must include a file extension (e.g.,
.mjs,.cjs)
Subpath Imports
Pkgroll supports building entry-points defined in package.json#imports (Node.js subpath imports), including conditional imports:
{
"imports": {
"#my-pkg": "./dist/index.js",
"#env": {
"node": "./dist/env.node.js",
"default": "./dist/env.browser.js"
}
}
}
Output formats
Pkgroll detects the format for each entry-point based on the file extension or the package.json property it's placed in, using the same lookup logic as Node.js.
| package.json property | Output format |
| - | - |
| main | Auto-detect |
| module | ESM<br><sub>Note: This unofficial property is not supported by Node.js and is mainly used by bundlers.</sub> |
| types | TypeScript declaration |
| exports | Auto-detect |
| exports.require | CommonJS |
| exports.import | Auto-detect |
| exports.types | TypeScript declaration |
| bin | Auto-detect<br>Also patched to be executable with the Node.js hashbang. |
Auto-detect infers the type by extension or package.json#type:
| Extension | Output format |
| - | - |
| .cjs | CommonJS |
| .mjs | ECMAScript Modules |
| .js | Determined by package.json#type, defaulting to CommonJS |
Dependency bundling & externalization
Packages to externalize are detected by reading dependency types in package.json. Only dependencies listed in devDependencies are bundled in.
When generating type declarations (.d.ts files), this also bundles and tree-shakes type dependencies declared in devDependencies as well.
// package.json
{
// ...
"peerDependencies": {
// Externalized
},
"dependencies": {
// Externalized
},
"optionalDependencies": {
// Externalized
},
"devDependencies": {
// Bundled
},
}
Aliases
Import map
You can configure aliases using the import map in package.json#imports.
In Node.js, import mappings must start with # to indicate an internal subpath import. However, Pkgroll allows defining aliases without the # prefix.
Example:
{
"imports": {
// Alias '~utils' points to './src/utils.js'
"~utils": "./src/utils.js",
// Native Node.js subpath import (must use '#', can't reference './src')
"#internal-package": "./vendors/package/index.js",
},
}
Tsconfig paths
You can also define aliases in tsconfig.json using compilerOptions.paths:
{
"compilerOptions": {
"paths": {
"@foo/*": [
"./src/foo/*",
],
"~bar": [
"./src/bar/index.ts",
],
},
},
}
[!TIP] The community is shifting towards using import maps (
imports) as the source of truth for aliases because of their wider support across tools like Node.js, TypeScript, Vite, Webpack, and esbuild.
Target
Pkgroll uses esbuild to handle TypeScript and JavaScript transformation and minification.
The target specifies the environments the output should support. Depending on how new the target is, it can generate less code using newer syntax. Read more about it in the esbuild docs.
By default, the target is set to the version of Node.js used. It can be overwritten with the --target flag:
pkgroll --target=es2020 --target=node14.18.0
It will also automatically detect and include the target specified in tsconfig.json#compilerOptions.
Strip node: protocol
Node.js builtin modules can be prefixed with the node: protocol for explicitness:
import fs from 'node:fs/promises'
This is a new feature and may not work in older versions of Node.js. While you can opt out of using it, your dependencies may still be using it (example package using node:: path-exists).
Pass in a Node.js target that that doesn't support it to strip the node: protocol from imports:
pkgroll --target=node12.19
Custom tsconfig.json path
By default, Pkgroll looks for tsconfig.json configuration file in the current working directory. You can pass in a custom tsconfig.json path with the --tsconfig flag:
pkgroll --tsconfig=tsconfig.build.json
Export condition
Similarly to the target, the export condition specifies which fields to read from when evaluating export and import maps.
For example, to simulate import resolutions in Node.js, pass in node as the export condition:
pkgroll --export-condition=node
ESM ⇄ CJS interoperability
Node.js ESM offers interoperability with CommonJS via static analysis. However, not all bundlers compile ESM to CJS syntax in a way that is statically analyzable.
Because pkgroll uses Rollup, it's able to produce CJS modules that are minimal and interoperable with Node.js ESM.
This means you can technically output in CommonJS to get ESM and CommonJS support.
require() in ESM
Sometimes it's useful to use require() or require.resolve() in ESM. ESM code that uses require()
