CLI for building a dual ESM and CJS package with Babel. Takes an ES module and produces a compiled ESM and CJS build using the configuration from your babel.config.json
file.
- Node >= 16.19.0.
- Your package uses
"type": "module"
in package.json.
First install babel-dual-package
:
npm install babel-dual-package
Next write your babel.config.json
file and include any plugins or presets your project requires.
{
"presets": [
["@babel/preset-env", {
"modules": false
}]
]
}
Now run babel-dual-package src
to get an ESM and CJS build in a dist
directory that can be used as exports
in a package.json file.
Run babel-dual-package --help
to see a list of more options.
If your project is using typescript then add @babel/preset-typescript
. If it is also using JSX, then add @babel/preset-react
.
babel.config.json
{
"presets": [
["@babel/preset-env", {
"modules": false
}],
"@babel/preset-typescript"
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
Set the declarationDir
for your types to the same value used for --out-dir
, or dist
(the default) if not using --out-dir
.
tsconfig.json
{
"compilerOptions": {
"declaration": true,
"declarationDir": "dist",
"emitDeclarationOnly": true,
"isolatedModules": true,
"strict": true
},
"include": ["src"]
}
In order to support typescript, you must pass the --extensions
used:
babel-dual-package --out-dir dist --extensions .ts,.tsx src
If everything worked you should get an ESM build in dist
and a CJS build in dist/cjs
with extensions in filenames and specifiers updated correctly.
Now you can add some scripts to your package.json file to help automate the build during CI/CD.
package.json
"type": "module",
"scripts": {
"build:types": "tsc --emitDeclarationOnly",
"build:dual": "babel-dual-package --out-dir dist --extensions .ts,.tsx src",
"build": "npm run build:types && npm run build:dual"
}
Given a directory structure like the following,
app/
package.json
src/
one/
file1.js
two/
file2.js
file.js
and by using --out-file-extension
to make sure each build has unique filenames, you can create a flat build while still supporting conditional exports.
For example, running
babel-dual-package --no-cjs-dir --out-file-extension esm:.esm.js,cjs:.cjs.js src/*.js src/one src/two
Will produce the following build output:
dist/
file.esm.js
file.cjs.js
file1.esm.js,
file1.cjs.js,
file2.esm.js,
file2.cjs.js
You can run a build by importing this package and passing an args
query parameter in the import specifier. Boolean options like --no-cjs-dir
should have an empty string as their value (''
), and instead of positional arguments, you pass a files
array (globs supported).
build.js
await import(
`./node_modules/.bin/babel-dual-package?args=${encodeURIComponent(
JSON.stringify({
'--out-dir': 'dist',
'--extensions': '.ts',
'--no-cjs-dir': '',
files: ['src/*.js']
})
)}`
)
There are options that can be passed to provide custom output locations, file extensions, and more.
You can run babel-dual-package --help
to get more info. Below is the output of that:
user@comp ~ $ ./node_modules/.bin/babel-dual-package --help
Usage: babel-dual-package [options] <files ...>
Options:
--out-dir [out] Compile the modules in <files ...> into an output directory.
--root-mode [mode] The project-root resolution mode. One of 'root' (the default), 'upward', or 'upward-optional'.
--cjs-dir-name [string] The name of the --out-dir subdirectory to output the CJS build. [cjs]
--extensions [extensions] List of extensions to compile when a directory is part of the <files ...> input. [.js,.jsx,.mjs,.cjs]
--out-file-extension [extmap] Use a specific extension for esm/cjs files. [esm:.js,cjs:.cjs]
--keep-file-extension Preserve the file extensions of the input files.
--no-cjs-dir Do not create a subdirectory for the CJS build in --out-dir.
--no-comments Remove comments from generated code.
--source-maps Generate an external source map.
--minified Save as many bytes when printing (false by default).
--copy-files When compiling a directory copy over non-compilable files.
--help Output usage information (this information).
Q Can I use "type": "commonjs"
in my package.json file, or just not include the field?
A No. Converting from CJS to ESM is a codemod, not transpiling via Babel.