Skip to main content

Features & optimizations

Babel configuration

All packages are parsed and transpiled with Babel (through Rollup). The presets and plugins used are automatically determined on a package-by-package basis, by inspecting the package's root files and respective package.json (and root package.json if using workspaces).

Presets

The environment preset is always enabled and configures the following.

  • @babel/preset-env
    • Defines modules and targets based on the chosen platform and format.
    • Enables spec in development for testing compliance.
    • Enables bugfixes and shippedProposals for smaller file sizes.
    • Disables useBuiltIns as consumers of the package should polyfill accordingly.

The following presets are enabled when one of their conditions are met.

  • @babel/preset-flow
    • Package or root contains a flow-bin dependency.
    • Project contains a .flowconfig.
  • @babel/preset-typescript
    • Package or root contains a typescript dependency.
    • Package contains a tsconfig.json.
  • @babel/preset-react
    • Package contains a react dependency.
    • Enables the new JSX transform if the dependency range captures the minimum requirement.
  • babel-preset-solid
    • Package contains a solid-js dependency.

Plugins

The following plugins are enabled when one of their conditions are met.

  • @babel/plugin-proposal-decorators
    • Enabled when package is TypeScript aware and defines experimentalDecorators in tsconfig.json.
  • babel-plugin-env-constants
    • Always enabled. Will transform __DEV__, __PROD__, and __TEST__ to process.env.NODE_ENV expressions.
  • babel-plugin-conditional-invariant
    • Always enabled. Will wrap invariant() calls with process.env.NODE_ENV conditionals.
  • babel-plugin-cjs-esm-interop
    • Enabled when package platform is configured to node. Will convert ESM code to CJS and vice-versa.

Rollup configuration

While Babel handles the parsing and transformation of source files, Rollup bundles all entry point dependent source files into a single tree-shaken distributable file. This vastly reduces the file size, require/import times, evaluation speed, and more.

However, configuring Rollup can be quite daunting. Because of this, the entire layer is abstracted away behind Packemon, and should just "work" when packages are configured correctly. Our abstraction abides the following:

  • For every input in a package's inputs setting, an output file will be created.
  • For every platform and format in a package, a unique output file will be created.
  • Every Node.js built-in module is configured as external.
  • Every package dependency is configured as external.
  • Always reduces file size as much as possible by utilizing tree-shaking.
  • Allows input files to reference other input files to mitigate "instance of" and "reference" issues.

Plugins

The following plugins are enabled per package.

  • @rollup/plugin-node-resolve
    • Resolves imports using Node.js module resolution.
  • @rollup/plugin-commonjs
    • Converts CommonJS externals to ECMAScript for bundling capabilities.
  • @rollup/plugin-json
    • Allows JSON files to be imported (default export only).
  • @rollup/plugin-babel
    • Parses and transforms source code using Babel.
    • Excludes test related files from transformation.
    • Inlines runtime helpers in the output file.
  • rollup-plugin-node-externals
    • Defines externals based on package.json dependencies.
    • Includes dependencies, devDependencies, peerDependencies, and optionalDependencies.
  • rollup-plugin-polyfill-node
    • Polyfills Node.js built-in modules when platform is browser or native.
  • Custom
    • Prepend a Node.js shebang to bin.* output files.
    • Process imported assets and share between formats.

Development and production targets

Packemon configures Babel internally based on NODE_ENV. When in development (default), code is transpiled for spec compliance and debugging purposes, while in production for performance. We suggest running packemon build for development, and packemon pack for production (before a release).

packemon build # development
packemon pack # production

Automatic package exports

When the --addExports CLI option is enabled, Packemon will automatically generate an exports field in package.json, based on the configured inputs, formats, and platforms for the package. Packemon will do its best to map the configuration to exports conditions, such as browser, node, import, etc. When --declaration is passed, it'll also include the types condition for TypeScript.

As a demonstration, this is the packemon package itself:

{
"exports": {
"./package.json": "./package.json",
"./babel": {
"types": "./mjs/babel.d.ts",
"import": "./mjs/babel.mjs"
},
"./bin": {
"types": "./mjs/bin.d.ts",
"import": "./mjs/bin.mjs"
},
".": {
"types": "./mjs/index.d.ts",
"import": "./mjs/index.mjs"
}
}
}

Tree-shaking optimization

When a package is bundled, tree-shaking and pure annotations are automatically enabled through the Rollup build. This feature also takes multiple inputs (entry points) into account and chunks the bundled code accordingly.

Code-splitting aware

Make use of import() and Packemon will ensure proper code-splitting for consumers. Packemon will persist dynamic imports when the the target platform and supported version can utilize the feature natively, otherwise it is transpiled down.

React JSX transforms

JSX supports 2 patterns for transforming code: the classic pattern where import React from 'react' is required, and the new automatic pattern where the import can be omitted. Packemon will automatically choose a pattern based on the react dependency found in a package's package.json, by verifying the version satisfies the minimin requirement.

The version can be defined as a peerDependencies:

{
"peerDependencies": {
"react": ">=17.0.0"
}
}

Or the version can be defined as a normal dependencies:

{
"dependencies": {
"react": "^17.0.0"
}
}

Asset imports

When a file imports an asset (styles, images, audio, video), the import remains in-tact so any bundlers can handle accordingly. However, assets are moved to a shared assets folder, are hashed for uniqueness, and any imports are modified to this new path.

An example of this as follows:

// Input:
// src/components/Button/index.ts
// src/components/Button/button.css
import './button.css';
// Output:
// esm/components/Button/index.js
// assets/button-as17p2k9.css
import '../../../assets/button-as17p2k9.css';

UMD builds do not support asset imports!

Environment constants

The babel-plugin-env-constants plugin is always enabled, which will transform __DEV__, __PROD__, and __TEST__ constants to process.env.NODE_ENV conditionals.

When this code is ran through a minifier like Terser, any non-production checks will be removed through a process known as dead-code elimination. This will greatly reduce bundle size on consumers!

// Input
if (__DEV__) {
console.log('Some message in development!');
}
// Output
if (process.env.NODE_ENV !== 'production') {
console.log('Some message in development!');
}

If you are using TypeScript, you'll most likely need to declare the globals yourself.

declare global {
var __DEV__: boolean;
var __PROD__: boolean;
var __TEST__: boolean;
}

Invariant checks

The babel-plugin-conditional-invariant plugin is always enabled, which will wrap invariant() function checks in process.env.NODE_ENV conditionals that only run in development.

When this code is ran through a minifier like Terser, all invariant checks will be removed through a process known as dead-code elimination, just like environment constants above!

// Input
invariant(value === false, 'Value must be falsy!');
// Output
if (process.env.NODE_ENV !== 'production') {
invariant(value === false, 'Value must be falsy!');
}

Automatic .mjs wrappers for .cjs inputs

Publishing a package that contains builds for both .cjs and .mjs may result in the dual package hazard problem. Packemon attempts to mitigate this problem by only allowing 1 format. However, when publishing .cjs code, a consumer cannot used named imports within an .mjs file as one would expect (since CommonJS has no concept of default and named exports), as demonstrated below.

package/cjs/index.cjs
exports.name = 'value';
index.mjs
// Invalid
import { name } from 'package';

// Valid
import cjsPackage from 'package';

const { name } = cjsPackage;

This is rather annoying, as it does not align with import/export assumptions, and also makes it harder for consumers to migrate to ESM based code (.mjs). However, there is a solution to this problem, using an ESM wrapper. Packemon supports this as a first-class feature, and when the format is cjs, the following functionality is automatically enabled:

  • A wrapper .mjs file is emitted for each input, that re-exports all values found in the base .cjs file.
  • If --addExports is enabled, will append an import exports conditional.

Based on this information and the examples above, our imports now work as expected!

package/cjs/index.cjs
exports.name = 'value';
package/cjs/index-wrapper.mjs
import data from 'package';

export const { name } = data;
index.mjs
// Works now!
import { name } from 'package';

This is a great solution for packages that want to offer "modules" but are unable to fully migrate to ESM. If you do not want this functionality, use the lib format instead.

CommonJS & ECMAScript interoperability

Packemon by default encourages ECMAScript modules, but not everyone is there yet. To bridge this gap, we enable the babel-plugin-cjs-esm-interop plugin, which transforms CommonJS code (.cjs, .js) into ECMAScript module code (.mjs, .js with module type), and vice versa, based on the official Node.js documentation.

// Input: mjs
const self = import.meta.url;
// Output: cjs
const self = __filename;