babel-plugin-transform-import-source
v2.1.0
Published
Babel plugin for transforming import sources.
Readme
babel-plugin-transform-import-source
Babel plugin for transforming import sources.
Table of Contents
Overview
This plugin allows transforming import sources in the require, import, and
export statements either based on a set of custom rules or by applying a
custom function. It can be useful for modifying import sources to match the
output file extensions, or manipulating query parameters based on certain
conditions at compile-time, etc.
Features
- Transform import sources in
require/importexpressions andimport/exportstatements - Transform import sources outside of the statements with compile-time macros
- Resolve index file when the import source refers to a directory
- Exclude certain import sources to be transformed, with magic comments
- Handle multiple rules to match and replace import sources with different patterns
- Force absolute paths to bypass test conditions (easy cross-platform support)
- Take advantage of the low-level API to implement completely custom behavior
Installation
Install the plugin using npm or another package manager:
npm install -D babel-plugin-transform-import-sourceUsage
Add the plugin to the Babel configuration and specify the transformation rules in the options object.
module.exports = {
plugins: [
[
require.resolve("babel-plugin-transform-import-source"),
{
transform: {
rules: [
{
// Apply this rule to explicit relative paths only.
test: /^[.\\/]+.*$/,
// Bypass the test condition to match absolute paths without
// any restrictions.
includeAbsolute: true,
// Find either the extension part or the end of the string.
//
// `$` character matches the end of the string.
// `?` character makes the pattern optional.
//
// As a result, this will cause the transformation to
// either replace the extension part with `.mjs` or, append
// `.mjs` to the end of the string if there is no extension.
//
// For example, the import source `./file.ts` will be
// transformed to `./file.mjs`, and the import source
// `./file` will be transformed to `./file.mjs` as well.
find: /(?:\.[cm]?[jt]s[x]?)?$/iu,
replace: ".mjs",
// If the import source refers to a directory,
// first append `index` suffix to the import source,
// then apply the transformation.
//
// This will cause the transformation to replace an import
// source like `./dir` with `./dir/index.mjs`, assuming
// `./dir` refers to a directory in this example.
resolveIndex: true,
},
],
},
},
],
],
};JSON Configuration
In case of using JSON configuration, the RegExp patterns can be provided as
objects containing the regexp and optional flags properties.
{
"plugins": [
[
"babel-plugin-transform-import-source",
{
"transform": {
"rules": [
{
"test": {
"regexp": "^[.\\\\/]+.*$"
},
"includeAbsolute": true,
"find": {
"regexp": "(?:\\.[cm]?[jt]s[x]?)?$",
"flags": "iu"
},
"replace": ".mjs",
"resolveIndex": true
}
]
}
}
]
]
}Low-Level API
The plugin can also be used with a custom function to transform import sources.
const { createDefaultTransformer } = require("babel-plugin-transform-import-source");
/**
* @import { type TransformerContext } from "babel-plugin-transform-import-source";
*/
// The default transformer function can be re-used in a custom one.
const superTransform = createDefaultTransformer();
module.exports = {
plugins: [
[
require.resolve("babel-plugin-transform-import-source"),
{
transformer: (
/**
* @type {TransformerContext}
*/
context
) =>
{
// For example, Babel API or the default transformer function
// can be used depending on certain conditions.
if (condition)
{
context.nodePath.replaceWith(context.api.types.nullLiteral());
return undefined;
}
else if (otherCondition)
{
return `${context.importSource}?enterprise=true&oss=false`;
}
return superTransform(context);
},
},
],
],
};Technical Details
The plugin handles the following types of AST nodes:
CallExpressionExportAllDeclarationExportNamedDeclarationImportDeclarationImportExpression
The transformer function is called for each import source in the AST nodes mentioned above. The function receives a context object containing the information about the current import source and the AST node. See the transformer context definition below for more information.
Transformer Context
The transformer context object contains the following properties:
- dirname: The directory path of the current file.
- filename: The file path of the current file.
- importSource: The import source.
- importSourceNode: AST node of the import source.
- nodePath: Current AST node path.
- options: Plugin options.
- state: Plugin state.
Module Methods
The plugin also provides compile-time .transform module-methods to transform
import sources outside of the statements.
When the feature is enabled, the .transform macros can be accessed via the
import.meta object or the require function.
// Transforms `./file.ts` and returns the result.
import.meta.transform("./file.ts");
// Same as above.
require.transform("./file.ts");Type Definitions for Module Methods
The type definitions for the module methods with the default property names are
provided in the env.d.ts file. They can be imported with a reference
directive.
For custom property names, TypeScript's interface merging feature can be used to extend the default type definitions.
declare module "module"
{
declare global
{
interface ImportMeta
{
transform(source: string): string;
}
}
}
declare namespace NodeJS
{
interface Require
{
transform(source: string): string;
}
}Magic Comments
To exclude certain import sources from being transformed,
@babel-plugin-transform-import-source-ignore directive can be used in
a leading comment line.
// The import source will remain unchanged.
// @babel-plugin-transform-import-source-ignore
import "./file.ts";
// Same as above.
require(
// @babel-plugin-transform-import-source-ignore
"./file.ts",
);
// The import source here will remain unchanged as well.
// And the expression will return `./file.ts` as it is.
import.meta.transform(
// @babel-plugin-transform-import-source-ignore
"./file.ts",
);Options
The plugin accepts an options object with the following properties.
moduleMethods
- Type:
object - Description: Configuration for the module methods.
- Properties:
transformImportSource: (Optional) Configuration for the compile-time.transformmethods.- Type:
object - Properties:
importMeta: (Optional) A boolean to enable/disable theimport.meta.transformmacro. Or anobjectwithpropertyNameproperty to specify the custom property name. Default isfalse.require: (Optional) A boolean to enable/disable therequire.transformmacro. Or anobjectwithpropertyNameproperty to specify the custom property name. Default isfalse.
- Type:
transform
- Type:
object - Description: Configuration for transforming import sources.
- Properties:
rules: An array of transformation rules.
Rule
A transformation rule object can have the following properties:
- find: A string or RegExp to match the import source to replace.
- replace: A string to replace the matched import source.
- includeAbsolute: (Optional) A boolean to include absolute paths by bypassing the test.
- resolveIndex: (Optional) A boolean to resolve index file when the import source refers to a directory.
- test: (Optional) A string or RegExp to test the import source.
transformer
- Type:
function - Description: A custom function to transform import sources.
- Arguments:
context: The transformer context object.
- Returns: The transformed import source. If the function returns
nullorundefined, the import source remains unchanged.
Development Notes
The plugin is written in TypeScript and compiled to both CommonJS and ECMAScript modules, and the import sources are transformed to match the output file extensions accordingly. The following notes are not specific to the plugin only, but they can be helpful for understanding the motivation behind the plugin.
Targeting both CommonJS and ECMAScript modules in a single package is a common
use case to provide compatibility with different environments and tools.
Achieving this usually requires workarounds to ensure that the module types are
resolved correctly in different environments. These workarounds are like
defining type fields in package.json files
(e.g., "type": "commonjs" or "type": "module"), or writing wrapper scripts
around the main entry points. Since using wrapper scripts can be helpful in
avoiding
dual-package hazards,
they can also reduce the static evaluation of the package and make it less
optimizable (assuming that the module type is CommonJS at the beginning).
By using .cjs or .mjs file extensions, the module type becomes explicit and
independent of the environment. This allows the module type to be recognized as
a CommonJS or ECMAScript module without relying on the environment or
configuration files like package.json. Instead, it can be determined by the
file extension itself. This can be beneficial in many ways, such as making the
package more portable and less error-prone in different environments.
While CommonJS allows import paths to be written without file extensions, ECMAScript spec requires import paths to be fully specified with explicit file extensions. This mismatch can cause issues in certain scenarios, such as when a package is written in TypeScript with extensionless imports, and then it's compiled and consumed by ECMAScript modules, etc.
To make the module types explicit while compiling with Babel, the
--out-file-extension flag can be used to set the output file extensions
(e.g., --out-file-extension '.cjs' or --out-file-extension '.mjs').
However, -according to my tests- Babel does not use this flag for import
sources in statements to be transformed respectively. So, the import sources
remain unchanged, while extensions of the output files are modified. This can
cause the exact issue mentioned above as well.
The plugin can be used to fill this gap for transforming the import sources in the statements to make them match the output file extensions. This way, the module types are resolved correctly in both CommonJS and ECMAScript modules, and the package becomes more consistent and reliable.
Build Process
The plugin uses itself to transform its own import source strings to target the desired module type explicitly.
To make the plugin able to use itself in its own build process, there is a
bootstrap phase
that compiles the plugin to CommonJS modules first. Since the format of the
import paths is extensionless in the source code, and this is acceptable in
CommonJS modules, they don't need to be changed in the output files. But, at
this step, the output file names have .js extension. Although the default
module type in Node.js is CommonJS, the type field in the package.json file
is set to "type": "commonjs" to ensure that the files are recognized as
CommonJS modules in any case. Finally, these compiled files can be imported
into the Babel configurations successfully.
After the bootstrap phase, the actual build process gets started. At this point, the early version of the plugin that obtained from the bootstrap phase is used as a part of the build process to transform the import sources in the statements to match the output file extensions.
The process can be observed in the following steps:
- The
bootstrap::cjsscript defined in thepackage.jsonfile gets executed to compile the plugin to CommonJS modules using thebabel.config.bootstrap.cjsconfiguration file. babel.config.cjs.cjsandbabel.config.esm.cjsconfiguration files require the CommonJS modules compiled in the bootstrap phase. This is where the plugin uses itself to compile its own source code to target the desired module types explicitly.- The
build::cjsandbuild::esmscripts defined in thepackage.jsonfile get executed to compile the plugin to both CommonJS and ECMAScript modules using the corresponding configuration files mentioned in the previous step.
License
This project is licensed under the MIT License.
See the LICENSE file for more information.
