Disclaimer: This article applies to Babel that runs on Node.js, which is the most common way of running Babel. I didn't check how Babel on a web browser works. The reason I wrote this article is because I didn't notice anywhere on Babel docs that explains what exactly is a Babel plugin. Babel docs mention how a Babel plugin works, what's its purpose, etc. but as far as I can tell, they don't mention what a Babel plugin is.
A Babel plugin is a CommonJS module, whose export is expected by Babel to be a function. That's it. Note that I didn't say "default export", because there is no concept of "default export" in CommonJS. In CommonJS, there is only the module.exports
property. This property's value can be any valid JavaScript value. However, Babel expects the value of the module.exports
property of a CommonJS module that is designated as a Babel plugin to be a JavaScript function.
How do we know that a Babel plugin is a "CommonJS module"? Can't it be an "ES6 module" as well? No. A Babel plugin is a CommonJS module. It cannot be an ES6 module or any other type of module, at least as of Babel version 7.12.10. The reason is, Babel is using Node.js' require
function to load modules. In babel-core/src/config/files/plugins.js#loadPlugin, it first resolves the plugin and then makes a call to requireModule. In turn, requireModule
calls Node.js require
function with its argument and returns the results of the require
function call.
So, we understand that a Babel plugin is just a require
d module. How does this make Babel plugins being only CommonJS modules, and not any other types of modules (such as ES6 modules)? The reason is, require
function always assumes that the module it loads is a CommonJS module. It does not try to detect the module type and use another loading mechanism to load other types of modules. The only check it does is the following rudimentary check: If the argument (which is a string) provided to require
ends with .mjs
, require
throws an ERR_REQUIRE_ESM
error, which says that ECMAScript modules cannot be require
d, they must be import
ed. This behavior is also documented. Otherwise, require
proceeds with assuming that the module pointed to by its argument is a CommonJS module. Hence, if the module is not a CommonJS module, errors will occur while require
processes the file and require
will throw these errors. Hence, we understand that a Babel plugin is simply a CommonJS module.
One thing to note is that Babel mangles what you provide before passing them to require
. The purpose of this is to allow the user to specify plugin/preset names as "shorthands". Some examples are as follows:
-
If you pass
asdf
as a plugin name to Babel, Babel will not passasdf
to therequire
call. It will passbabel-plugin-asdf
to therequire
call. -
If you pass
@babel/asdf
as a plugin name to Babel, Babel will pass@babel/plugin-asdf
torequire
. -
If you pass
@someOrgName/asdf
as as plugin name to Babel, Babel will pass@someOrgName/babel-plugin-asdf
torequire
. The difference between a non-@babel
organization name and the@babel
organization name is the additionalbabel-
that comes after the slash for modules in non-@babel
organization names. -
If you pass
@someOrgName
as a plugin name to Babel, Babel will pass@someOrgName/babel-plugin
torequire
. I think this is used to designate a organization-wide Babel plugin. Note that there is no counterpart of this with the@babel
organization. That is, if you pass just@babel
as a plugin name to Babel, Babel will not pass@babel/plugin
or@babel/babel-plugin
torequire
. It will just pass@babel
.@babel
is an invalid npm package name, since it does not have a package name, it only has an organization name. Hence, it will not work. Also, as a side note, there are no npm packages named@babel/plugin
or@babel/babel-plugin
. -
If you don't want any mangling to the modules, prefix them with
module:
. That is, if you passmodule:<anything>
as a plugin name to Babel, Babel will just remove the leadingmodule:
from it and pass the remaining part of the string as is torequire
. Likewise, even if the plugin name that you pass to Babel does not start withmodule:
, but it contains a slash (/
) and does not start with@
, again, Babel will not perform any mangling on the name and pass it as is torequire
.
To rephrase, we can say that what we provide to Babel are regarded as "plugin identifiers". Babel makes some mangling (processing) to these "plugin identifiers" and turns them into "CommonJS module identifiers" and then passes them to the require
function.
Since it is hard to precisely describe all rules of converting a "plugin identifier" to a "CommonJS module identifier" in writing, you can go to babel-core/src/config/files/plugins.js to understand how Babel converts a "plugin identifier" to a "CommonJS module identifier". The following is a non-exhaustive list of examples of how Babel converts a "plugin identifier" to a "CommonJS module identifier":
-
asdf
->babel-plugin-asdf
-
@babel/asdf
->@babel/plugin-asdf
-
@zxcv/asdf
->@zxcv/babel-plugin-asdf
-
@zxcv
->@zxcv/babel-plugin
-
module:asdf/ghjk
->asdf/ghjk
-
asdf/ghjl
->asdf/ghjl
-
babel-plugin-asdf
->babel-plugin-asdf
-
babel-plugin-asdf/ghjk
->babel-plugin-asdf/ghjk
-
babel-preset-asdf
->babel-preset-asdf
-
babel-preset-asdf/ghjk
->babel-preset-asdf/ghjk
-
@babel/asdf/ghjk
->@babel/asdf/ghjk
-
@babel/plugin-asdf
->@babel/plugin-asdf
-
@babel/plugin-asdf/ghjk
->@babel/plugin-asdf/ghjk
-
@babel/preset-asdf
->@babel/preset-asdf
-
@babel/preset-asdf/ghjk
->@babel/preset-asdf/ghjk
-
etc.
After a "CommonJS module identifier" is passed to require
, the rest of the algorithm belongs to Node.js. You can read the linked document to understand which module will be loaded given a module identifier.