Skip to content

📦 Zero-config package bundler for Node.js + TypeScript

License

Notifications You must be signed in to change notification settings

privatenumber/pkgroll

Repository files navigation

pkgroll

pkgrollis a JavaScript package bundler powered by Rollup that automatically builds your package from entry-points defined inpackage.json.No config necessary!

Write your code in TypeScript/ESM and runpkgrollto 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)

Already a sponsor?Join the discussion in theDevelopment repo!

Install

npm install --save-dev pkgroll

Quick setup

  1. Setup your project with source files insrcand output indist(configurable).

  2. Define package entry-files inpackage.json.

    These configurationsare 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.

  3. Package roll!

    npm run build#or npx pkgroll

Usage

Entry-points

Pkgrollparses package entry-points frompackage.jsonby reading propertiesmain,module,types,andexports.

The paths in./distare mapped to paths in./src(configurable with--srcand--distflags) to determine bundle entry-points.

Output formats

Pkgrolldetects the format for each entry-point based on the file extension or thepackage.jsonproperty it's placed in, using thesame lookup logic as Node.js.

package.jsonproperty Output format
main Auto-detect
module ESM
Note: Thisunofficial propertyis not supported by Node.js and is mainly used by bundlers.
types TypeScript declaration
exports Auto-detect
exports.require CommonJS
exports.import Auto-detect
exports.types TypeScript declaration
bin Auto-detect
Also patched to be executable with the Node.js hashbang.

Auto-detectinfers the type by extension orpackage.json#type:

Extension Output format
.cjs CommonJS
.mjs ECMAScript Modules
.js Determined bypackage.json#type,defaulting to CommonJS

Dependency bundling & externalization

Packages to externalize are detected by reading dependency types inpackage.json.Only dependencies listed indevDependenciesare bundled in.

When generating type declarations (.d.tsfiles), this also bundles and tree-shakes type dependencies declared indevDependenciesas well.

// package.json
{
//...

"peerDependencies":{
// Externalized
},
"dependencies":{
// Externalized
},
"optionalDependencies":{
// Externalized
},
"devDependencies":{
// Bundled
},
}

Aliases

Aliases can be configured in theimport map,defined inpackage.json#imports.

For native Node.js import mapping, all entries must be prefixed with#to indicate an internalsubpath import.Pkgrolltakes advantage of this behavior to define entries that arenot prefixedwith#as an alias.

Native Node.js import mapping supports conditional imports (eg. resolving different paths for Node.js and browser), butPkgrolldoes not.

⚠️Aliases are not supported in type declaration generation. If you need type support, do not use aliases.

{
//...

"imports":{
// Mapping '~utils' to './src/utils.js'
"~utils":"./src/utils.js",

// Native Node.js import mapping (can't reference./src)
"#internal-package":"./vendors/package/index.js",
}
}

Target

Pkgrollusesesbuildto 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 theesbuild docs.

By default, the target is set to the version of Node.js used. It can be overwritten with the--targetflag:

pkgroll --target=es2020 --target=node14.18.0

It will also automatically detect and include thetargetspecified intsconfig.json#compilerOptions.

Stripnode:protocol

Node.js builtin modules can be prefixed with thenode:protocolfor explicitness:

importfsfrom'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 usingnode::path-exists).

Pass in a Node.js target that that doesn't support it to strip thenode:protocol from imports:

pkgroll --target=node12.19

Customtsconfig.jsonpath

By default,Pkgrolllooks fortsconfig.jsonconfiguration file in the current working directory. You can pass in a customtsconfig.jsonpath with the--tsconfigflag:

pkgroll --tsconfig=tsconfig.build.json

Export condition

Similarly to the target, the export condition specifies which fields to read from when evaluatingexportandimportmaps.

For example, to simulate import resolutions in Node.js, pass innodeas the export condition:

pkgroll --export-condition=node

ESM ⇄ CJS interoperability

Node.js ESM offersinteroperability with CommonJSviastatic analysis.However, not all bundlers compile ESM to CJS syntax in a way that is statically analyzable.

Becausepkgrolluses 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 userequire()orrequire.resolve()in ESM. ESM code that usesrequire()can be seamlessly compiled to CommonJS, but when compiling to ESM, Node.js will error becauserequiredoesn't exist in the module scope.

When compiling to ESM,Pkgrolldetectsrequire()usages and shims it withcreateRequire(import.meta.url).

Environment variables

Pass in compile-time environment variables with the--envflag.

This will replace all instances ofprocess.env.NODE_ENVwith'production'and remove unused code:

pkgroll --env.NODE_ENV=production

Minification

Pass in the--minifyflag to minify assets.

pkgroll --minify

Watch mode

Run the bundler in watch mode during development:

pkgroll --watch

Clean dist

Clean dist directory before bundling:

pkgroll --clean-dist

Source maps

Pass in the--sourcemapflag to emit a source map file:

pkgroll --sourcemap

Or to inline them in the distribution files:

pkgroll --sourcemap=inline

FAQ

Why bundle with Rollup?

Rolluphas the best tree-shaking performance, outputs simpler code, and produces seamless CommonJS and ESM formats (minimal interop code). Notably, CJS outputs generated by Rollup supports named exports so it can be parsed by Node.js ESM. TypeScript & minification transformations are handled byesbuildfor speed.

Why bundle Node.js packages?

  • ESM and CommonJS outputs

    As the Node.js ecosystem migrates toESM,there will be both ESM and CommonJS users. A bundler helps accommodate both distribution types.

  • Dependency bundlingyields smaller and faster installation.

    Tree-shaking only pulls in used code from dependencies, preventing unused code and unnecessary files (eg.README.md,package.json,etc.) from getting downloaded.

    Removing dependencies also eliminates dependency tree traversal, which isone of the biggest bottlenecks.

  • Inadvertent breaking changes

    Dependencies can introduce breaking changes due to a discrepancy in environment support criteria, by accident, or in rare circumstances,maliciously.

    Compiling dependencies will make sure new syntax & features are downgraded to support the same environments. And also prevent any unexpected changes from sneaking in during installation.

  • Type dependenciesmust be declared in thedependenciesobject inpackage.json,instead ofdevDependencies,to be resolved by the consumer.

    This may seem counterintuitive because types are a development enhancement. By bundling them in with your package, you remove the need for an external type dependency. Additionally, bundling only keeps the types that are actually used which helps minimize unnecessary bloat.

  • Minificationstrips dead-code, comments, white-space, and shortens variable names.

Sponsors