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
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
.
- Package or root contains a
@babel/preset-typescript
- Package or root contains a
typescript
dependency. - Package contains a
tsconfig.json
.
- Package or root contains a
@babel/preset-react
- Package contains a
react
dependency. - Enables the new JSX transform if the dependency range captures the minimum requirement.
- Package contains a
babel-preset-solid
- Package contains a
solid-js
dependency.
- Package contains a
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
intsconfig.json
.
- Enabled when package is TypeScript aware and defines
babel-plugin-env-constants
- Always enabled. Will transform
__DEV__
,__PROD__
, and__TEST__
toprocess.env.NODE_ENV
expressions.
- Always enabled. Will transform
babel-plugin-conditional-invariant
- Always enabled. Will wrap
invariant()
calls withprocess.env.NODE_ENV
conditionals.
- Always enabled. Will wrap
babel-plugin-cjs-esm-interop
- Enabled when package platform is configured to
node
. Will convert ESM code to CJS and vice-versa.
- Enabled when package platform is configured to
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 onpackage.json
dependencies. - Includes
dependencies
,devDependencies
,peerDependencies
, andoptionalDependencies
.
- Defines
rollup-plugin-polyfill-node
- Polyfills Node.js built-in modules when platform is
browser
ornative
.
- Polyfills Node.js built-in modules when platform is
- Custom
- Prepend a Node.js shebang to
bin.*
output files. - Process imported assets and share between formats.
- Prepend a Node.js shebang to
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.
exports.name = 'value';
// 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 animport
exports conditional.
Based on this information and the examples above, our imports now work as expected!
exports.name = 'value';
import data from 'package';
export const { name } = data;
// 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;