babel-plugin-transform-mjs-imports v1.0.4
babel-plugin-transform-mjs-imports
This Babel plugin transforms CJS named import declarations (which are allowed in
TypeScript and modern
Node) into default CJS
import declarations with constant destructuring assignments when
emitting .mjs
files.
The goal of this plugin is to stop Webpack and other bundlers from
choking on .mjs
files
transpiled from TypeScript (.ts
) and other sources that contain
technically-invalid CJS named import syntax.
Installation and Usage
npm install --save-dev babel-plugin-transform-mjs-imports
And in your babel.config.js
:
module.exports = {
plugins: ['babel-plugin-transform-mjs-imports'],
};
And finally, run Babel:
babel src --extensions .ts --out-dir dist --out-file-extension .mjs
By default, this plugin will transform named imports for Node's built-in
packages (e.g. http
, url
, path
) and any CJS package under node_modules
(reported by
webpack-node-module-types
).
Custom Configuration
Out of the box with zero configuration, the default settings look something like this:
const { getModuleTypes } = require('webpack-node-module-types');
module.exports = {
plugins: [
['babel-plugin-transform-mjs-imports', {
test: [ ...getModuleTypes().cjs ], // ◄ match all CJS modules
exclude: [], // ◄ never excludes modules by default
transformBuiltins: true, // ◄ match all built-in modules
silent: true, // ◄ output results to stdout if silent == false
verbose: false, // ◄ output more detailed results if silent == false
}],
],
};
You can manually specify which import sources are CJS using the test
and
exclude
configuration options, which accept an array of strings/RegExp items
to match sources against. If a string begins and ends with a /
(e.g.
/^apollo/
), it will be evaluated as a case-insensitive RegExp item. Named
imports with sources that match any item in test
and fail to match all items
in exclude
will be transformed. You can also skip transforming built-ins by
default (unless they match in test
) using transformBuiltins: false
.
For instance, if we want only to transform any imports (bare or deep) of
apollo-server
and any built-ins like url
from the above example, my
babel.config.js
would include:
module.exports = {
plugins: [
['babel-plugin-transform-mjs-imports', {
// ▼ regex matches any import that starts with 'apollo-server'
test: [ /^apollo-server/ ],
}],
],
};
Replacing the test
array like this also replaces the default list of CJS
modules from node_modules
. To append rather than replace, try something like:
npm install --save-dev webpack-node-module-types
const { getModuleTypes } = require('webpack-node-module-types');
module.exports = {
plugins: [
['babel-plugin-transform-mjs-imports', {
// ▼ extend, rather than override, the default settings
test: test: [
...getModuleTypes().cjs,
'another/source/path.js',
'something-special'
],
}],
],
};
This is also useful when
webpack-node-module-types
misclassifies a package or you want to override the defaults.
Motivation
As of Node 14, there are at least two "gotcha" rules when writing JavaScript
using .mjs
files destined to be published in an NPM package:
All import sources that are not bare and not found in the package's
imports
/exports
key must include a file extension. This includes imports on directories e.g.import { Button} from './component/button'
, which should appear in an.mjs
file asimport { Button } from './component/button/index.mjs'
.CJS modules can only be imported using default import syntax. As far as Webpack 4 and (so far) Webpack 5 is concerned, this includes built-ins too. For example,
import { parse } from 'url'
is illegal becauseurl
is considered a CJS module.
Node 14 is lax with the second rule, going so far as to use static analysis to allow CJS modules to be imported using the "technically illegal" named import syntax. However, Webpack and other bundlers are much stricter about this and using named import syntax on a CJS module will cause bundling to fail outright.
For instance, suppose we use Babel to transpile this TypeScript file into the
ESM entry point my-package.mjs
for a dual
CJS2/ESM
package:
/* my-package.ts */
// ▼ #1: an "illegal" named bare CJS import
import { ApolloServer, gql } from 'apollo-server'
// ▼ #2: a legal named deep ESM import
import { Button } from 'ui-library/es'
// ▼ #3: an "illegal" named built-in import
import { parse as parseUrl } from 'url'
// ▼ #4: a legal default bare CJS import and a legal namespace bare CJS import
import lib, * as libNamespace from 'cjs-component-library'
// ▼ #5: a legal default bare CJS import and an "illegal" named bare CJS import
import lib2, { item1, item2 } from 'cjs2-component2-library2'
// ▼ #6: a legal default bare CJS import
import lib3 from 'cjs3-component3-library3'
// ▼ #7: a legal namespace bare CJS import
import * as lib4 from 'cjs4-component4-library4'
// ▼ #8: a legal named relative ESM import using .mjs (.ts is not allowed here!)
import { util } from '../lib/module-utils.mjs'
// ▼ #9: an "illegal" named deep CJS import
import { default as util2, util as smUtil, cliUtil } from 'some-package/dist/utils.js'
// ...
The above syntax, which is all legal in Node 14 and TypeScript, will survive
transpilation when emitting my-package.mjs
. Running this with node my-package.mjs
works. Further, if we run this file as an entry point through
Webpack (with babel-loader) and emit CJS bundle file my-package.js
,
running node my-package.js
also works. Everything works, and my-package.mjs
+ my-package.js
can be distributed as a dual CJS2/ESM package!
Just one problem: when Webpack and other bundlers attempt to process this as a
tree-shakable ESM package (using our .mjs
entry point), they'll choke and
die when they encounter the
"illegal" CJS named imports. This usually manifests as strange errors like
ERROR in ./my-package.mjs Can't import the named export 'ApolloServer' from non EcmaScript module (only default export is available)
or ERROR in ./node_modules/my-package/dist/my-package.mjs Can't import the named export 'parse' from non EcmaScript module (only default export is available)
.
babel-plugin-transform-mjs-imports
remedies this by transforming each named
import of a CJS module into a default CJS import with a constant destructuring
assignment of the named imports:
/* my-package.mjs (using babel-plugin-transform-mjs-imports) */
// ▼ #1: named CJS import (transformed)
import _$apollo_server from "apollo-server"; // ◄ default import
const { ApolloServer, gql } = _$apollo_server; // ◄ destructuring assignment
// ▼ #2: named ESM import (preserved)
import { Button } from "ui-library/es";
// ▼ #3: named built-in import (transformed)
import _$url from "url"; // ◄ default import
const { parse: parseUrl } = _$url; // ◄ destructuring assignment
// ▼ #4: default and namespace CJS import (preserved)
import lib, * as libNamespace from "cjs-component-library";
// ▼ #5: default CJS import (preserved); named CJS import (transformed)
import lib2 from "cjs2-component2-library2"; // ◄ default import (preserved)
const { item1, item2 } = lib2; // ◄ destructuring assignment
// ▼ #6: default CJS import (preserved)
import lib3 from "cjs3-component3-library3";
// ▼ #7: namespace CJS import (preserved)
import * as lib4 from "cjs4-component4-library4";
// ▼ #8: named ESM import (preserved) (eliminated by Webpack through bundling)
import { util } from "../lib/module-utils.mjs";
// ▼ #9: named CJS import (default alias is preserved, rest is transformed)
import util2 from "some-package/dist/utils.js";// ◄ default import (preserved)
const { util: smUtil, cliUtil } = util2; // ◄ destructuring assignment
Now, having my-package
as a CJS-importing ESM (.mjs
) dependency of a project
we're bundling is no longer problematic! 🎉🎉🎉
Hence, this transformation is useful for library authors shipping packages with
ESM entry points as it prevents various bundlers from choking on delicious sugar
like named imports of CJS modules in .mjs
files. It's a solution to a
different symptom of this problem.
This plugin is similar to (and inspired by) babel-plugin-transform-default-import.
Contributing
New issues and pull requests are always welcome and greatly appreciated! If you submit a pull request, take care to maintain the existing coding style and add unit tests for any new or changed functionality. Please lint and test your code, of course!
NPM Scripts
Run npm run list-tasks
to see which of the following scripts are available for
this project.
Using these scripts requires a linux-like development environment. None of the scripts are likely to work on non-POSIX environments. If you're on Windows, use WSL.
Development
npm run repl
to run a buffered TypeScript-Babel REPLnpm test
to run the unit tests and gather test coverage data- Look for HTML files under
coverage/
- Look for HTML files under
npm run check-build
to run the integration testsnpm run check-types
to run a project-wide type checknpm run test-repeat
to run the entire test suite 100 times- Good for spotting bad async code and heisenbugs
- Uses
__test-repeat
NPM script under the hood
npm run dev
to start a development server or instancenpm run generate
to transpile config files (underconfig/
) from scratchnpm run regenerate
to quickly re-transpile config files (underconfig/
)
Building
npm run clean
to delete all build process artifactsnpm run build
to compilesrc/
intodist/
, which is what makes it into the published packagenpm run build-docs
to re-build the documentationnpm run build-externals
to compileexternal-scripts/
intoexternal-scripts/bin/
npm run build-stats
to gather statistics about Webpack (look forbundle-stats.json
)
Publishing
npm run start
to start a production instancenpm run fixup
to run pre-publication tests, rebuilds (like documentation), and validations- Triggered automatically by publish-please
NPX
npx publish-please
to publish the packagenpx sort-package-json
to consistently sortpackage.json
npx npm-force-resolutions
to forcefully patch security audit problems
Package Details
You don't need to read this section to use this package, everything should "just work"!
This is a simple CJS2 package with a default export.
package.json
includes the exports
and
main
keys, which point to the CJS2 entry point, the
type
key, which is commonjs
, and the
sideEffects
key, which is false
for optimal tree
shaking, and the types
key, which points to a TypeScript
declarations file.
Release History
See CHANGELOG.md.