📦 🍣 Zero-config JS bundler for ESM, CommonJS, and .d.ts outputs

Overview

pkgroll

Write your code in ESM & TypeScript and bundle it to get ESM, CommonJS, and type declaration outputs with a single command!

Features

  • Zero config Configuration automatically inferred from package.json
  • ESM/CJS Detection Uses .mjs/.cjs extension or package.json#type to infer module type
  • Export & Import maps Uses export and import maps to configure endpoints and aliases
  • TypeScript friendly Transform TS and emit .d.ts files (Supports .cts/.mts too!)
  • Node.js ESM ⇄ CJS friendly Preserves named exports from ESM to CJS
  • Fast Transformations powered by esbuild

Install

npm install --save-dev pkgroll

Quick setup

  1. Setup your project with source files in src and output in dist (configurable).

  2. Specify output files in package.json:

    {
        "name": "my-package",
    
        // Set "module" or "commonjs" (https://nodejs.org/api/packages.html#type)
        // "type": "module",
    
        // Define the output files
        "main": "./dist/file.js",
        "module": "./dist/file.mjs",
        "types": "./dist/file.d.ts",
    
        // Define output files for Node.js export maps (https://nodejs.org/api/packages.html#exports)
        "exports": {
            "require": "./dist/file.js",
            "import": "./dist/file.mjs",
            "types": "./dist/file.d.ts"
        },
    
        // 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

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 --src and --dist flags) to determine bundle entry-points.

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
Note: This unofficial property is 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-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

Aliases can be configured in the import map, defined in package.json#imports.

For native Node.js import mapping, all entries must be prefixed with # to indicate an internal subpath import. Pkgroll takes advantage of this behavior to define entries that are not prefixed with # as an alias.

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

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

{
    // ...

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

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

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

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. This can be seamlessly compiled to CommonJS. But when compiling to ESM, Node.js will error because require doesn't exist in the module scope.

When compiling to ESM, Pkgroll detects require() usages and shims it with createRequire(import.meta.url).

Minification

Pass in the --minify flag to minify assets.

pkgroll --minify

Watch mode

Run the bundler in watch mode during development:

pkgroll --watch

FAQ

Why bundle with Rollup?

Rollup has 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 by esbuild for speed.

Why bundle Node.js packages?

  • ESM and CommonJS outputs

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

  • Dependency bundling yields 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 is one 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 dependencies must be declared in the dependencies object in package.json for it to be resolved by the consumer.

    This can be unintuitive because types are a development enhancement and also adds installation bloat. Bundling filters out unused types and allows type dependencies to be declared in devDependencies.

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

Comments
  • Read from `package.json#engines` for target

    Read from `package.json#engines` for target

    package.json#engines specifies what version of Node.js the package runs on.

    Pkgroll should read from this to infer the target, and deprecate the --target flag.

    opened by privatenumber 0
  • feat!: change default target to node 14

    feat!: change default target to node 14

    Closes #13. It can be interpreted as a breaking change for anyone who still has Node <14 installed on their machine for pkgroll and didn't set a specific target. I don't know if you want to mark this as a major release, however.

    opened by ayuhito 0
  • Possible bug related to --dist

    Possible bug related to --dist

    While writing some tests for #11, I was a little confused about this test for the --dist flag.

    Is it intended for the command to be used as pkgroll --dist ./nested or as written in the test pkgroll --dist .? I'm not sure if I agree with the test in this case as being intended behaviour.

    I was originally intending to use distPath for #11, but that wouldn't work with the current --dist flag setup since we'd be cleaning the . dir instead of ./nested.

    Switching it pkgroll --dist ./nested in the test results in the following error:

    /home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/lib/error.js:59
                    error = new Error(message);
                            ^
    
    Error: Command failed with exit code 1: /home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested
    Error: Could not find mathing source file for export path "./nested/index.d.ts"
        at tD (/home/lotus/pkgroll/dist/cli-0d8a4c1f.js:21:5041)
        at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19127
        at async Promise.all (index 2)
        at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19086
        at null.makeError (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/lib/error.js:59:11)
        at null.handlePromise (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/index.js:119:26)
        at processTicksAndRejections (node:internal/process/task_queues:96:5)
        at null.pkgroll (/home/lotus/pkgroll/tests/utils/pkgroll.ts:18:6)
        at null.<anonymous> (/home/lotus/pkgroll/tests/specs/builds/src-dist.ts:88:27)
        at null.<anonymous> (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/manten/dist/index.js:36:9) {
      shortMessage: 'Command failed with exit code 1: /home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested',
      command: '/home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested',
      escapedCommand: '"/home/lotus/.cache/nve/12.22.9/x64/bin/node" "/home/lotus/pkgroll/dist/cli.js" --dist "./nested"',
      exitCode: 1,
      signal: undefined,
      signalDescription: undefined,
      stdout: '',
      stderr: 'Error: Could not find mathing source file for export path "./nested/index.d.ts"\n' +
        '    at tD (/home/lotus/pkgroll/dist/cli-0d8a4c1f.js:21:5041)\n' +
        '    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19127\n' +
        '    at async Promise.all (index 2)\n' +
        '    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19086',
      failed: true,
      timedOut: false,
      isCanceled: false,
      killed: false
    }
    

    Possibly related to the dts plugin not taking in the dist arg properly? I could dig around a little if you have any clues which file may be related.

    opened by ayuhito 0
  • Change default target to LTS

    Change default target to LTS

    I think having the default target to be reliant on process.versions.node creates a lot of inconsistency especially if you use a multitude of environments such as local and CI.

    To fit closer to the paradigm of zero-config, it might be better for the bundler to default to Node LTS instead for best practices.

    I'm not sure if this should be considered a breaking change considering the existing setup already led to inconsistent bundles.

    enhancement pr welcome 
    opened by ayuhito 2
  • Include rm -rf to dist folder

    Include rm -rf to dist folder

    The dist folder end up becoming very polluted at times, especially when you delete a source file which still leaves its bundled-up counterpart in the dist folder. Either cleaning up the dist folder by default or including a --clean flag would be a nice QOL addition.

    (Btw, I am happy to make some PRs in the near future for some of these issues I'm making - imo this bundler is the most ideal one out of others I've seen and I'm working on integrating it into the Fontsource project).

    enhancement pr welcome 
    opened by ayuhito 3
Releases(v1.8.0)
Owner
hiroki osame
I'm on a mission to open source my solutions 🚀
hiroki osame
An npm package for demonstration purposes using TypeScript to build for both the ECMAScript Module format (i.e. ESM or ES Module) and CommonJS Module format. It can be used in Node.js and browser applications.

An npm package for demonstration purposes using TypeScript to build for both the ECMAScript Module format (i.e. ESM or ES Module) and CommonJS Module format. It can be used in Node.js and browser applications.

Snyk Labs 57 Dec 28, 2022
Universal importer for CommonJS and ESM in Node.js

ModuleImporter by Nicholas C. Zakas If you find this useful, please consider supporting my work with a donation. Description A utility for seamlessly

Human Who Codes 18 Dec 2, 2022
utility library for promise, support both commonjs and ESM

promising-utils A utility library for promise, supports both commonjs and ESM npm install promising-utils --save yarn add promising-utils wait Used wh

Qiang Li 4 Oct 18, 2022
Node.js loader for compiling ESM & TypeScript modules to CommonJS

cjs-loader Node.js require() hook to instantaneously transform ESM & TypeScript to CommonJS on demand using esbuild. Features Transforms ESM & TypeScr

esbuild kit 40 Dec 13, 2022
Recursively publish ESM packages as CommonJS!

Commonify.js For us who are still relying on CommonJS, or using Electron which does not support ESM. ?? See also build-electron I made this tool that

Mikael Finstad 31 Dec 29, 2022
Minimal utility to convert to or from any timezone. Deno/Node/Browser. ESM/CommonJS.

minitz Features Convert dates between any timezone supported by the system. Parses ISO8601 time strings. MIT licensed, use the library any way you wan

Hexagon 14 Oct 10, 2022
Babel plugin and helper functions for interoperation between Node.js native ESM and Babel ESM

babel-plugin-node-cjs-interop and node-cjs-interop: fix the default import interoperability issue in Node.js The problem to solve Consider the followi

Masaki Hara 15 Nov 6, 2022
Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

Svante Jonsson IT-Högskolan 3 May 18, 2022
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
Forked from hayes0724/shopify-packer Modern development tool for Shopify using Webpack 5. Easy to extend and customize, zero build config, compatible with Slate and existing websites.

Shopify Packer Modern development tool for Shopify using Webpack 5. Easy to extend and customize, zero build config, comes with starter themes and com

Web & Mobile | eCommerce | Full-Stack Developer 4 Nov 24, 2022
A new zero-config test runner for the true minimalists

Why User-friendly - zero-config, no API to learn, simple conventions Extremely lighweight - only 40 lines of code and no dependencies Super fast - wit

null 680 Dec 20, 2022
Zero-config PWA Plugin for VitePress

Zero-config PWA Plugin for VitePress ?? Features ?? Documentation & guides ?? Zero-Config: sensible built-in default configs for common use cases ?? E

Vite PWA 10 Dec 1, 2022
Vite plugin to client bundle i18next locales composited from one to many json/yaml files from one to many libraries. Zero config HMR support included.

vite-plugin-i18next-loader yarn add -D vite-plugin-i18next-loader Vite plugin to client bundle i18next locales composited from one to many json/yaml f

AlienFast 4 Nov 30, 2022
Leaderboard - An app that outputs the names and the score of the players and it uses API's to get all the informations

leaderboard is an app that outputs the names and the score of the players and it uses API's to get all the informations.

zieeco 7 Jul 8, 2022
The frontend of a full stack application of a personal wallet made with React, Node and MongoDB that allows you to add inputs, outputs and see all your extract.

The frontend of a full stack application of a personal wallet made with React, Node and MongoDB that allows you to add inputs, outputs and see all your extract.

Bernardo Rodrigues 5 Jun 2, 2022
The backend of a full stack application of a personal wallet made with React, Node and MongoDB that allows you to add inputs, outputs and see all your extract.

My first full stack application with the concept of a personal wallet that allows you to create a personal account to keep track of your entire statement by adding incoming and outgoing transactions, as well as calculating the total balance and being able to edit and delete old transactions.

Bernardo Rodrigues 6 Jun 23, 2022
Github action to parse OWNERS files and outputs random reviewers

Get Owners Github Action Do you want to have all the approvers and reviewers without having strange scripts in your actions? Do you want to have rando

Ugo Palatucci 3 Oct 22, 2022
Input a list of Handshake top-level domains, outputs names sorted into 4 arrays: available, registered, reserved, or invalid.

name-check A simple NodeJS package that, given a flat list of top-level domain names, queries the Handshake (HNS) blockchain in order to classify each

Neel Yadav 2 Jan 8, 2022