⛔️ DEPRECATED/UNMAINTAINED
[!CAUTION]
This project served its purpose back in the day. JavaScript and Webpack have
evolved mightily over the last decade, and as a result this project does not
make much sense anymore.
Patterns like the following are commonplace throughout the JavaScript ecosystem:
const { name: pkgName } = require('../package.json');
With TypeScript and more modern JavaScript, we'd achieve the same with the
following:
import { name as pkgName } from '../package.json';
However, running this through Webpack will trigger warnings like
WARNING in ./src/index.ts 6:30-37 Should not import the named export 'name' (imported as 'pkgName') default-exporting module (only default export is available soon)
.
This simple Babel plugin makes these warnings and errors go away by transforming
named import declarations of CJS and JSON modules into
default import declarations with constant destructuring assignments.
The goal is to make that delicious const { ... } = require(...)
sugar
forward-compatible by allowing imports of named CJS exports to remain
consistent across CJS and ESM source, and to prevent some versions of Webpack,
Node, browsers, et cetera from
choking when encountering
it.
Installation
npm install --save-dev babel-plugin-transform-default-named-imports
And in your babel.config.js
:
module.exports = {
plugins: ['transform-default-named-imports']
};
Keep in mind
plugin order matters with Babel!
And finally, run Babel through your toolchain (Webpack, Jest, etc) or manually.
For example:
npx babel src --out-dir dist
Usage
By default, this plugin will transform named imports for Node's built-in
packages (e.g. http
, url
, path
), for sources that end in .json
, and for
any CJS package under node_modules
(determined by
webpack-node-module-types
).
All other imports, including local imports, are left untouched.
Importing JSON Modules
As of webpack@5.18
, Webpack does not properly tree-shake constant
destructuring assignments of JSON imports without a little help. Until Webpack's
handling of JSON modules stabilizes,
externalize all JSON imports as commonjs:
module.exports = {
...
externals: [
...
({ request }, cb) =>
/\.json$/.test(request) ? cb(null, `commonjs ${request}`) : cb()
]
};
If you do not externalize your JSON imports, you risk bloating your bundle size!
Custom Configuration
Out of the box with zero configuration, the default settings this plugin uses
look something like the following:
const { determineModuleTypes } = require('webpack-node-module-types/sync');
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
test: [
...determineModuleTypes().cjs.map(strToOpenEndedRegex),
/^(\.(\.)?\/)+(.+)\.json$/
],
include: [],
exclude: [],
transformBuiltins: true,
silent: true,
verbose: false,
monorepo: 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, to exclusively transform any imports (bare or deep) of
apollo-server
and any built-ins like url
from the above example,
babel.config.js
would include:
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
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, you can do something
like:
npm install --save-dev webpack-node-module-types
const { determineModuleTypes } = require('webpack-node-module-types/sync');
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
test: [
...determineModuleTypes().cjs.filter(
(p) => !/^next([/?#].+)?/.test(p)
),
'something-special'
]
}
]
]
};
Monorepo Support
If you're running this babel plugin within a monorepo, consider using the
monorepo
option. This enables either the
upward
or
relative
root mode functionality of the underlying webpack-node-module-types
package.
When monorepo
is set to true
, upward root mode is used. This looks for the
closest node_modules
directory within any ancestor directory and throws if it
doesn't exist; errors are prevented when a "local" node_modules
is not found
in the current working directory.
Example:
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
monorepo: true
}
]
]
};
Inversely, when monorepo
is set to a relative path string, relative root mode
is used. This looks for a node_modules
directory at said path, but no error is
thrown if it does not exist. Instead, the current working directory must contain
a "local" node_modules
directory or an error will be thrown.
Example:
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
monorepo: './packages/pkg-1/node_modules'
}
]
]
};
The leading dot (./
or ../
) in the relative path version is required!
Troubleshooting
Firstly, this package uses the debug
package under the hood, so running babel with the DEBUG='*:*'
environment
variable set will yield all sorts of useful information to your CLI.
Excluding Misclassified Packages
If all you want to do is ignore a misclassified module like next
in the
previous section, it's easier to just exclude it:
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
exclude: [/^next([/?#].+)?/]
}
]
]
};
This is useful when
webpack-node-module-types
misclassifies a package or you want to more easily override the defaults.
A clue that a package is being misclassified is when you encounter errors like
TypeError: Cannot destructure property 'X' of '_X.default' as it is undefined.
For example, in the case of the following deep next
import:
import { apiResolver } from 'next/dist/next-server/server/api-utils.js';
Without adding the exclude
configuration key above, Webpack 5.20
reports the
following error:
TypeError: Cannot destructure property 'apiResolver' of '_apiUtils.default' as it is undefined.
After adding the exclude
key, this error disappears.
Including Special Packages
Similar to exclude
, you can use include
to append a module name or regex to
the final list of CJS modules rather than adding webpack-node-module-types
as
a dependency and necessarily overwriting the entire test
array.
This:
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
include: ['package']
}
]
]
};
Instead of this:
const { determineModuleTypes } = require('webpack-node-module-types/sync');
module.exports = {
plugins: [
[
'transform-default-named-imports',
{
test: [...determineModuleTypes().cjs, 'package']
}
]
]
};
This is especially useful when using the monorepo
option, which passes custom
configuration to determineModuleTypes(...)
that shouldn't be overwritten.
Motivation
As of Node 14, there are at least two "gotcha" rules when writing ESM modules
(files that end in .mjs
):
-
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 as import { Button } from './component/button/index.mjs'
.
-
CJS modules can only be imported using default import syntax. As far as
Webpack is concerned, this includes built-ins too. For example,
import { parse } from 'url'
is illegal because url
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 can cause bundling to fail outright.
For instance, suppose one uses Babel to transpile this TypeScript file into the
ESM entry point my-package.mjs
for a
dual CJS2/ESM
package:
import { ApolloServer, gql } from 'apollo-server';
import { Button } from 'ui-library/es';
import { parse as parseUrl } from 'url';
import lib, * as libNamespace from 'cjs-component-library';
import lib2, { item1, item2 } from 'cjs2-component2-library2';
import lib3 from 'cjs3-component3-library3';
import * as lib4 from 'cjs4-component4-library4';
import { util } from '../lib/module-utils.mjs';
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, after running this file as an entry point
through Webpack (with babel-loader) and emitting 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!
Problem: when Webpack attempts to process this as a tree-shakable ESM package
(using our .mjs
entry point), at worst it'll
choke and die encountering
the "illegal" CJS named imports. This 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)
.
In more recent versions of Webpack, this can lead to similar warnings when
transpiling TypeScript source.
babel-plugin-transform-default-named-imports
remedies this and similar issues
by transforming each named import of a CJS module into a default CJS import with
a constant destructuring assignment of the named imports:
import _apolloServer from 'apollo-server';
const { ApolloServer, gql } = _apolloServer;
import { Button } from 'ui-library/es';
import _url from 'url';
const { parse: parseUrl } = _url;
import lib, * as libNamespace from 'cjs-component-library';
import lib2 from 'cjs2-component2-library2';
const { item1, item2 } = lib2;
import lib3 from 'cjs3-component3-library3';
import * as lib4 from 'cjs4-component4-library4';
import { util } from '../lib/module-utils.mjs';
import util2 from 'some-package/dist/utils.js';
const { util: smUtil, cliUtil } = util2;
Now, having my-package
import CJS modules as if they were ESM causes no
warnings or errors! 🎉
Hence, this transformation is mainly 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. It's a solution to a
different symptom of this problem.
This plugin is somewhat similar to
babel-plugin-transform-default-import
and
babel-plugin-transform-named-imports.
You could say this plugin is the functional intersection of the aforementioned.
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!