Gathering detailed insights and metrics for babel-plugin-transform-rewrite-imports
Gathering detailed insights and metrics for babel-plugin-transform-rewrite-imports
Gathering detailed insights and metrics for babel-plugin-transform-rewrite-imports
Gathering detailed insights and metrics for babel-plugin-transform-rewrite-imports
⚡ Add an extension to import specifiers that do not already have one, replace the extensions of those that do, or even rewrite specifiers entirely.
npm install babel-plugin-transform-rewrite-imports
Typescript
Module System
Min. Node Version
Node Version
NPM Version
64.5
Supply Chain
98.6
Quality
80.5
Maintenance
100
Vulnerability
100
License
TypeScript (73.64%)
JavaScript (26.11%)
Shell (0.25%)
Total Downloads
102,536
Last Day
146
Last Week
1,706
Last Month
6,431
Last Year
76,886
MIT License
5 Stars
132 Commits
1 Watchers
7 Branches
1 Contributors
Updated on Mar 10, 2025
Latest Version
1.5.3
Package Id
babel-plugin-transform-rewrite-imports@1.5.3
Unpacked Size
67.51 kB
Size
19.13 kB
File Count
5
NPM Version
9.9.3
Node Version
22.10.0
Published on
Oct 30, 2024
Cumulative downloads
Total Downloads
Last Day
23.7%
146
Compared to previous day
Last Week
11.1%
1,706
Compared to previous week
Last Month
19.6%
6,431
Compared to previous month
Last Year
205.3%
76,886
Compared to previous year
4
1
116
This Babel plugin (1) reliably adds extensions to import and export specifiers that do not already have one, (2) selectively replaces extensions of specifiers that do, and (3) can rewrite whole specifiers in intricate ways using simple yet powerful replacement maps.
All TypeScript and JavaScript flavors are supported depending on how Babel is configured in the project.
For example, something like this:
1import { item1, type item2 } from '==> pretty specifier #1 <=='; 2 3export { here as there } from '==> pretty specifier #2 <=='; 4 5const elsewhere = `==> pretty specifier #${getRandomNumber()} <==`; 6 7jest.mock('==> pretty specifier #3 <=='); 8 9const x = require('==> pretty specifier #4 <=='); 10 11export async function myFunction() { 12 return (await import(elsewhere, { with: { type: 'json' } })).name; 13} 14 15export type MyType = { 16 typeOnlyImport: typeof import('==> pretty specifier #5 <==').Type; 17};
Can be easily transformed into something like this:
1import { item1, type item2 } from '../../../specifier-1.js'; 2 3export { here as there } from '../specifier-2.js'; 4 5const elsewhere = `==> specifier #${getRandomNumber()} <==`; 6 7jest.mock('../../../packages/a-different-monorepo-package/src/specifier-3.js'); 8 9const x = require('@specifier/four'); 10 11const __injected_dynamic_rewrite = function () { 12 /*...*/ 13}; 14 15export async function myFunction() { 16 return ( 17 await import(__injected_dynamic_rewrite(elsewhere), { 18 with: { type: 'json' } 19 }) 20 ).name; 21} 22 23export type MyType = { 24 typeOnlyImport: typeof import('../../../../node_modules/s-5/dist/src/lib.d.ts').Type; 25};
The transform-rewrite-imports plugin comes in handy in situations like
transpiling TypeScript source with extensionless imports to ESM, or changing
alias paths in TypeScript declaration (i.e. .d.ts
) files into relative
paths suitable for publishing. It does this more reliably and efficiently than
prior art.
1npm install --save-dev babel-plugin-transform-rewrite-imports
And integrate the following snippet into your Babel configuration:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 // See below for configuration instructions and examples 7 } 8 ] 9 ] 10};
Finally, run Babel through your toolchain (Webpack, Jest, etc) or manually:
1npx babel src --out-dir dist
The transform-rewrite-imports plugin effectively combines the functionality of the following:
The module-resolver plugin inspired quite a bit of the functionality of
transform-rewrite-imports, such as transforming require
-like functions,
and offers some similar features like RegExp-based aliasing and support for
substitution functions. However, transform-rewrite-imports is capable of more
complex replacements (like handling intricate file extension changes) with
support for a wider variety of specifier types while surfacing a simpler
interface.
babel-plugin-add-import-extension
transform-rewrite-imports started off as a fork of add-import-extension;
transform-rewrite-imports functions more consistently and includes
support for transforming require
, require
-like, static import()
, and
arbitrary dynamic import()
statements, replacing multiple extensions using
complex logic, and reliably ignoring extensions that should not be
replaced.
babel-plugin-replace-import-extension
transform-rewrite-imports is similar in intent to replace-import-extension. However, rewriting extensions is only a small fraction of what transform-rewrite-imports can do. And while both extensions support transforming dynamic imports, transform-rewrite-imports results in more efficient code in production environments due to (1) avoiding injecting dynamic code into the AST except as the very last resort and (2) only injecting dynamic code once and caching it globally (at the file level) rather than filling the file with repeated functions.
babel-plugin-transform-rename-import
With its last update published over 6 years ago, transform-rename-import can also replace import specifiers, though transform-rewrite-imports offers a powerful superset of replacer functionality, including optionally performing replacements of arbitrary dynamic imports at runtime and appending extensions to specifiers that would otherwise not have one.
tsconfig-replace-paths / tsconfig-paths / tscpaths
tsconfig-replace-paths and its predecessors/alternatives tsconfig-paths and
tscpaths are extremely useful for transpiling TypeScript projects, as they
handle alias- and path- resolving use cases without additional configuration;
transform-rewrite-imports, on the other hand, must be fed path/alias
information from tsconfig.json
manually.
Unfortunately, tsconfig-paths is not a Babel plugin and requires patching
your runtime while the others do not support all the latest
TypeScript/Babel AST features (like TsImportType
) and therefore fail
to consistently and correctly transform certain files (especially certain
.d.ts
files).
By mapping a project's tsconfig.json
paths
value to a replacement map
transform-rewrite-imports can understand, it becomes possible to ditch
tsconfig-replace-paths et al and reduce dependency count. Here's an
example using transform-rewrite-imports to replace these plugins (and
babel-plugin-module-resolver) for transforming both sources and type
definitions. Essentially, this Babel configuration file maps the project's
tsconfig.json
paths
into a replaceExtensions
replacement map.
By default this plugin does not affect Babel's output. You must explicitly configure this extension before any specifier will be rewritten.
More information on the available options can be found in the docs:
1{ 2 appendExtension?: string | Callback<string | undefined>; 3 recognizedExtensions?: string[]; 4 replaceExtensions?: Record<string, string | Callback<string>>; 5 requireLikeFunctions?: string[]; 6 injectDynamicRewriter?: 'never' | 'only-if-necessary'; 7 silent?: boolean; 8 verbose?: boolean; 9}
appendExtension
To append an extension to all relative import specifiers that do not already
have a recognized extension, use appendExtension
:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 appendExtension: '.mjs' 7 } 8 ] 9 ] 10};
[!TIP]
Only relative import specifiers (that start with
./
or../
) will be considered forappendExtension
. This means bare specifiers (e.g. built-in packages and packages imported fromnode_modules
) and absolute specifiers will never be affected byappendExtension
.
recognizedExtensions
What is and is not considered a "recognized extension" is determined by
recognizedExtensions
:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 appendExtension: '.mjs', 7 recognizedExtensions: ['.js'] 8 } 9 ] 10 ] 11};
That is: import specifiers that end with an extension included in
recognizedExtensions
will never have appendExtension
appended to
them. All other imports, including those with a .
in the file name (e.g.
component.module.style.ts
), may be rewritten.
recognizedExtensions
is set to ['.js', '.jsx', '.mjs', '.cjs', '.json']
by
default.
If the value of appendExtension
is not included in
recognizedExtensions
, then imports that already end in appendExtension
will have appendExtension
appended to them (e.g. index.ts
is rewritten
as index.ts.ts
when appendExtension: '.ts'
and recognizedExtensions
is its
default value). If this behavior is undesired, ensure appendExtension
is
included in recognizedExtensions
.
[!WARNING]
Note that specifying a custom value for
recognizedExtensions
overwrites the default value entirely. To extend rather than overwrite, you can import the default value from the package itself:1const { 2 defaultRecognizedExtensions 3} = require('babel-plugin-transform-rewrite-imports'); 4 5module.exports = { 6 plugins: [ 7 [ 8 'babel-plugin-transform-rewrite-imports', 9 { 10 appendExtension: '.mjs', 11 recognizedExtensions: [...defaultRecognizedExtensions, '.ts'] 12 } 13 ] 14 ] 15};
replaceExtensions
You can also replace one or more existing extensions in specifiers using a replacement map:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 replaceExtensions: { 7 // Replacements are evaluated **in order**, stopping on the first match. 8 // That means if the following two keys were listed in reverse order, 9 // .node.js would become .node.mjs instead of .cjs 10 '.node.js': '.cjs', 11 '.js': '.mjs' 12 } 13 } 14 ] 15 ] 16};
These configurations can be combined to rewrite many imports at once. For instance, if you wanted to replace certain extensions and append only when no recognized or listed extension is specified:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 appendExtension: '.mjs', 7 replaceExtensions: { 8 '.node.js': '.cjs', 9 // Since .js is in recognizedExtensions by default, file.js would normally 10 // be ignored. However, since .js is mapped to .mjs in the 11 // replaceExtensions map, file.js becomes file.mjs 12 '.js': '.mjs' 13 } 14 } 15 ] 16 ] 17};
appendExtension
and replaceExtensions
accept any suffix, not just
those that begin with .
; additionally, replaceExtensions
accepts regular
expressions. This allows you to partially or entirely rewrite a specifier
rather than just its extension:
1const { 2 defaultRecognizedExtensions 3} = require('babel-plugin-transform-rewrite-imports'); 4 5module.exports = { 6 plugins: [ 7 [ 8 'babel-plugin-transform-rewrite-imports', 9 { 10 appendExtension: '.mjs', 11 // Add .css to recognizedExtensions so .mjs isn't automatically appended 12 recognizedExtensions: [...defaultRecognizedExtensions, '.css'], 13 replaceExtensions: { 14 '.node.js': '.cjs', 15 '.js': '.mjs', 16 17 // The following key replaces the entire specifier when matched 18 '^package$': `${__dirname}/package.json`, 19 // If .css wasn't in recognizedExtensions, my-utils/src/file.less would 20 // become my-utils/src/file.css.mjs instead of my-utils/src/file.css 21 '(.+?)\\.less$': '$1.css' 22 } 23 } 24 ] 25 ] 26};
[!TIP]
If a key of
replaceExtensions
begins with^
or ends with$
, it is considered a regular expression instead of an extension. Regular expression replacements support substitutions of capturing groups as well (e.g.$1
,$2
, etc).
replaceExtensions
is evaluated and replacements made before
appendExtension
is appended to specifiers with unrecognized or missing
extensions. This means an extensionless import specifier could be rewritten by
replaceExtensions
to have a recognized extension, which would then be ignored
instead of having appendExtension
appended to it.
requireLikeFunctions
When it comes to deciding what is and is not a specifier,
transform-rewrite-imports will always scan ImportDeclaration
,
ExportAllDeclaration
, ExportNamedDeclaration
,
TSImportType
, and dynamic import CallExpression
s for specifiers.
For call expressions specifically, requireLikeFunctions
is used to determine
which additional function calls will have their first arguments scanned for
specifiers. By default, requireLikeFunctions
is set to:
1[ 2 'require', 3 'require.resolve', 4 'System.import', 5 'jest.genMockFromModule', 6 'jest.mock', 7 'jest.unmock', 8 'jest.doMock', 9 'jest.dontMock', 10 'jest.requireActual' 11];
[!TIP]
Similar to
defaultRequireLikeFunctions
, these defaults are exported under the namedefaultRequireLikeFunctions
.
This means call expressions like require(...)
, jest.mock(...)
, et al will be
treated the same way as import(...)
, where the first parameter is considered a
specifier. You are free to tweak this functionality to suit your environment.
replaceExtensions
and appendExtension
both accept function
callbacks as values everywhere strings are accepted. This can be used to provide
advanced replacement logic.
These callback functions have the following signatures:
1type AppendExtensionCallback = (context: { 2 specifier: string; 3 capturingGroups: never[]; 4 filepath: string; 5}) => string | undefined; 6 7type ReplaceExtensionsCallback = (context: { 8 specifier: string; 9 capturingGroups: string[]; 10 filepath: string; 11}) => string;
Where specifier
is the import/export specifier being rewritten,
capturingGroups
is a simple string array of capturing groups returned by
String.prototype.match()
, and filepath
is the absolute path to the
original input file being transformed by Babel. capturingGroups
will always be
an empty array except when it appears within a function value of a
replaceExtensions
entry that has a regular expression key.
When provided as the value of appendExtension
, a string containing an
extension should be returned (including leading dot). When provided as the value
of a replaceExtensions
entry, a string containing the full specifier
should be returned. When returning a full specifier, capturing group
substitutions (e.g. $1, $2, etc) within the returned string will be honored.
Further, in the case of appendExtension
, note that specifier
, if its
basename is .
or ..
or if it ends in a directory separator (e.g. /
), will
have "/index" appended to the end before the callback is invoked. However, if
the callback returns undefined
(and the specifier was not matched in
replaceExtensions
), the specifier will not be modified in any way.
By way of example (see the output of this example here):
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 // If the specifier ends with "/no-ext", do not append any extension 7 appendExtension: ({ specifier }) => { 8 return specifier.endsWith('/no-ext') || 9 specifier.endsWith('..') || 10 specifier === './another-thing' 11 ? undefined 12 : '.mjs'; 13 }, 14 replaceExtensions: { 15 // Rewrite imports of packages in a monorepo to use their actual names 16 // v capturing group #1: capturingGroups[1] 17 '^packages/([^/]+)(/.+)?': ({ specifier, capturingGroups }) => { 18 // ^ capturing group #2: capturingGroups[2] 19 if ( 20 specifier === 'packages/root' || 21 specifier.startsWith('packages/root/') 22 ) { 23 return `./monorepo-js${capturingGroups[2] ?? '/'}`; 24 } else if ( 25 !capturingGroups[2] || 26 capturingGroups[2].startsWith('/src/index') 27 ) { 28 return `@monorepo/$1`; 29 } else if (capturingGroups[2].startsWith('/package.json')) { 30 return `@monorepo/$1$2`; 31 } else { 32 return `@monorepo/$1/dist$2`; 33 } 34 } 35 } 36 } 37 ] 38 ] 39};
When transforming dynamic imports and require statements that do not have a
string literal as the first argument, and injectDynamicRewriter
is
not set to 'never'
, the options passed to this plugin are transpiled and
injected into the resulting AST.
[!CAUTION]
This means you take a slight performance hit when you do arbitrary dynamic imports that cannot be statically analyzed (e.g.
require(getMd5Hash() + '.txt')
). These types of dynamic imports are usually code smell, but this library is built to accommodate them regardless.However, if you are NOT doing arbitrary dynamic imports, which are dynamic imports where the first argument is not a string literal, then this section is of no relevance to you since nothing extra will be injected into the AST.
Therefore, to be safe, callback functions must not reference variables outside of their immediate scope.
Good:
1module.exports = { 2 plugins: [ 3 [ 4 'babel-plugin-transform-rewrite-imports', 5 { 6 replaceExtensions: { 7 '^packages/([^/]+)(/.+)?': ({ specifier, capturingGroups }) => { 8 const myPkg = require('my-pkg'); 9 myPkg.doStuff(specifier, capturingGroups); 10 } 11 } 12 } 13 ] 14 ] 15};
Bad:
1const myPkg = require('my-pkg'); 2 3module.exports = { 4 plugins: [ 5 [ 6 'babel-plugin-transform-rewrite-imports', 7 { 8 replaceExtensions: { 9 '^packages/([^/]+)(/.+)?': ({ specifier, capturingGroups }) => { 10 myPkg.doStuff(specifier, capturingGroups); 11 } 12 } 13 } 14 ] 15 ] 16};
Technically, you can get away with violating this rule if you're sure you'll only ever use dynamic imports/require statements with string literal arguments.
Like Babel itself, this plugin leverages debug under the hood for log management. You can take advantage of this to peer into transform-rewrite-imports's innermost workings and deepest decision-making processes by activating the appropriate log level. For example, the following will enable all logging related to this plugin:
1DEBUG='babel-plugin-transform-rewrite-imports:*' npx babel src --out-dir dist
With the following snippet integrated into your Babel configuration:
1const { 2 defaultRecognizedExtensions 3} = require('babel-plugin-transform-rewrite-imports'); 4 5module.exports = { 6 plugins: [ 7 [ 8 'babel-plugin-transform-rewrite-imports', 9 { 10 appendExtension: '.mjs', 11 recognizedExtensions: [...defaultRecognizedExtensions, '.css'], 12 replaceExtensions: { 13 '.ts': '.mjs', 14 '^package$': `${__dirname}/package.json`, 15 '(.+?)\\.less$': '$1.css' 16 } 17 } 18 ] 19 ] 20};
The following source:
1/* file: src/index.ts */ 2import { name as pkgName } from 'package'; 3import { primary } from '.'; 4import { secondary } from '..'; 5import { tertiary } from '../..'; 6import dirImport from './some-dir/'; 7import jsConfig from './jsconfig.json'; 8import projectConfig from './project.config.cjs'; 9import { add, double } from './src/numbers'; 10import { curry } from './src/typed/curry.ts'; 11import styles from './src/less/styles.less'; 12 13// Note that, unless otherwise configured, @babel/preset-typescript deletes 14// type-only imports. If you want to operate on type imports and/or .d.ts files, 15// use @babel/syntax-typescript instead. See ./test/supports-type-only for 16// an example. 17import type * as AllTypes from './lib/something.mjs'; 18 19export { triple, quadruple } from './lib/num-utils'; 20 21// Note that, unless otherwise configured, @babel/preset-typescript deletes 22// type-only exports. If you want to operate on type imports and/or .d.ts files, 23// use @babel/syntax-typescript instead. See ./test/supports-type-only for 24// an example. 25export type { NamedType } from './lib/something'; 26 27const thing = await import('./thing'); 28const anotherThing = require('./another-thing'); 29 30const thing2 = await import(someFn(`./${someVariable}`) + '.json'); 31const anotherThing2 = require(someOtherVariable);
Is, depending on your other plugins/settings, transformed into something like:
1/* file: dist/index.js */ 2const _rewrite = (importPath, options) => { 3 ... 4 }, 5 _rewrite_options = { 6 appendExtension: '.mjs', 7 recognizedExtensions: ['.js', '.jsx', '.mjs', '.cjs', '.json', '.css'], 8 replaceExtensions: { 9 '.ts': '.mjs', 10 '^package$': '/absolute/path/to/project/package.json', 11 '(.+?)\\.less$': '$1.css' 12 } 13 }; 14 15import { name as pkgName } from '/absolute/path/to/project/package.json'; 16import { primary } from './index.mjs'; 17import { secondary } from '../index.mjs'; 18import { tertiary } from '../../index.mjs'; 19import dirImport from './some-dir/index.mjs'; 20import jsConfig from './jsconfig.json'; 21import projectConfig from './project.config.cjs'; 22import { add, double } from './src/numbers.mjs'; 23import { curry } from './src/typed/curry.mjs'; 24import styles from './src/less/styles.css'; 25 26export { triple, quadruple } from './lib/num-utils.mjs'; 27 28const thing = await import('./thing.mjs'); 29const anotherThing = require('./another-thing.mjs'); 30 31// Require calls and dynamic imports with a non-string-literal first argument 32// are transformed into function calls that dynamically return the rewritten 33// string: 34 35const thing2 = await import( 36 _rewrite(someFn(`./${someVariable}`) + '.json', _rewrite_options) 37); 38 39const anotherThing2 = require(_rewrite(someOtherVariable, _rewrite_options)); 40
[!NOTE]
See the full output of this example here.
For some real-world examples of this Babel plugin in action, check out
xscripts's babel.config.js
file (which uses transform-rewrite-imports to
replace both babel-plugin-module-resolver and tsconfig-replace-paths),
unified-utils, this very repository, or just take a peek at the
test cases.
Further documentation can be found under docs/
.
This is a CJS2 package with statically-analyzable exports built by Babel for Node.js versions that are not end-of-life.
That means both CJS2 (via require(...)
) and ESM (via import { ... } from ...
or await import(...)
) source will load this package from the same entry points
when using Node. This has several benefits, the foremost being: less code
shipped/smaller package size, avoiding dual package
hazard entirely, distributables are not
packed/bundled/uglified, and a less complex build process.
Each entry point (i.e. ENTRY
) in package.json
's
exports[ENTRY]
object includes one or more export
conditions. These entries may or may not include: an
exports[ENTRY].types
condition pointing to a type
declarations file for TypeScript and IDEs, an
exports[ENTRY].module
condition pointing to
(usually ESM) source for Webpack/Rollup, an exports[ENTRY].node
condition
pointing to (usually CJS2) source for Node.js require
and import
, an
exports[ENTRY].default
condition pointing to source for browsers and other
environments, and other conditions not enumerated
here. Check the package.json file to see which export
conditions are supported.
Though package.json
includes
{ "type": "commonjs" }
, note that any ESM-only entry points will
be ES module (.mjs
) files. Finally, package.json
also
includes the sideEffects
key, which is false
for
optimal tree shaking.
See LICENSE.
New issues and pull requests are always welcome and greatly appreciated! 🤩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Thank you!
See CONTRIBUTING.md and SUPPORT.md for more information.
Thanks goes to these wonderful people (emoji key):
Bernard 🚇 💻 📖 🚧 ⚠️ 👀 | ||||||
|
This project follows the all-contributors specification. Contributions of any kind welcome!
No vulnerabilities found.
No security vulnerabilities found.