Make tsc-compiled `es2020/esnext` bundles compatible with esm/mjs requirements
npm install tsc-esm-fix



- Problem
- Solutions
- Features
- Getting started
- Requirements
- Install
- Usage examples
- CLI
- JS/TS API
- Alternatives
- Contributing
- References
- License
.js extensions for relative module paths if compiled as es2020/esnext.import.meta is not allowed.#### moduleResolution: nodenext
Nightly build TypeScript 4.7 provides experimental esm support. But it still forces to add extensions by hand (tested on 4.7.0-dev.20220408).
``shell
src/main/ts/q/u/x/index.ts:1:21 - error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node12' or 'nodenext'. Did you mean '../../../foo.js'?
1 import { foo } from '../../../foo'
`nodenext
Moreover, if understand TS/49271 correctly, + pkg.json type: module requires .js extension to be added to all .d.ts files of external ESM packages too. Well, good luck with that.
This lib covers options 1 and 2.
→ import {foo} from './foo.js'
* import {baz} from 'external/baz' → import {baz} from 'external/baz.js'
* Note, including the file extension is only necessary for packages without an "exports" field. So in this case all the external refs remain as are.
* Pays attention to index files: import {bar} from './bar' → import {bar} from './bar/index.js'
* Handles . and .. shortcuts
export from '.' → export * from './index.js'
export from '..' → export * from '../index.js'
* Injects .js extensions into .d.ts libdef files
* Does not affect string literals and comments: depseek
* Handles conditional exports (https://nodejs.org/api/packages.html#conditional-exports)
* Follows outDir found in tsconfig.json.
* Searches and replaces __dirname and __filename refs with import.meta.
* Fills blank files with export {} (esbuild issue 1043)
* Patches source map files to point to the updated files.
* Patches require statements with new file refs if ext changes (hybrid/dual pkg)
* Changes file extensions (applied to local deps only).
* Supports Windows-based runtimes.Getting started
$3
Node.js >=16.0.0$3
`shell
npm i -dev tsc-esm-fix
yarn add -D tsc-esm-fixor w/o saving to package.json
npx tsc-esm-fix [options]
`$3
`shell
tsc-esm-fix [options]to post-process outputs each time
tsc-esm-fix --target='target/es6'to patch ts sources once
tsc-esm-fix --src='src/main/ts' --ext='.js'
``typescript
import { fix } from 'tsc-esm-fix'
await fix({
dirnameVar: true,
filenameVar: true,
ext: true
})
`Input
code ref
`js
import { foo } from './foo';
import './bar';// external cjs module
import * as e1def from 'e1/a/b/c';
import * as e1root from 'e1';
const { e1 } = e1def;
const { e1: e1x } = e1root;
export { e1, e1x };
// external esm module with
main in pkg.json
export { m1 } from 'm1';
export { m1 as m1x } from 'm1/index';// external esm module with
exports in pkg.json
export { e2 } from 'e2';
export { e2 as es3 } from 'e2/index';
export { e2 as es4 } from 'e2/alias';
export { e2foo } from 'e2/foo';
export { e2bar } from 'e2/bar-bundle';export * from './foo';
export * from './baz';
export * from './q/u/x';
export const foobaz = foo + 'baz';
export { foo as foo1 } from './foo.js';
// Dir with index.js file inside: ./qux.js/index.js
export { qux } from './qux.js';
export const dirname = __dirname;
export const filename = __filename;
console.log(foobaz);
`Output
`js
import { foo } from './foo.js';
import './bar.js';import * as e1def from 'e1/a/b/c/index.js';
import * as e1root from 'e1';
const { e1 } = e1def;
const { e1: e1x } = e1root;
export { e1, e1x };
export { m1 } from 'm1';
export { m1 as m1x } from 'm1/index.js';
export { e2 } from 'e2';
export { e2 as es3 } from 'e2/index';
export { e2 as es4 } from 'e2/alias';
export { e2foo } from 'e2/foo';
export { e2bar } from 'e2/bar-bundle';
export * from './foo.js';
export * from './baz/index.js';
export * from './q/u/x/index.js';
export const foobaz = foo + 'baz';
export { foo as foo1 } from './foo.js';
export { qux } from './qux.js/index.js';
export const dirname = /file:\\\\/\\\\/(.+)\\\\/[^/]/.exec(import.meta.url)[1];
export const filename = /file:\\\\/\\\\/(.+)/.exec(import.meta.url)[1];
`$3
`shell
tsc-esm-fix [opts]
`
| Option | Description | Default |
|------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| --tsconfig | Path to project's ts-config(s) | tsconfig.json |
| --src | Entry points where the ts-source files are placed. If defined src option suppresses target | |
| --target | tsc-compiled output directory | If not specified inherited from tsconfig.json compilerOptions.outDir |
| --dirnameVar | Replace __dirname usages with import.meta | true |
| --filenameVar | Replace __filename var references with import.meta statements | true |
| --ext | Append extension to relative imports/re-exports | .js |
| --ts-ext | Known TS extensions | .ts,.tsx,.mts,.mtsx,.cts,.ctsx |
| --js-ext | Known JS extensions | .js,.jsx,.mjs,.mjsx,.cjs,.cjsx |
| --unlink | Remove original files if ext changes | true |
| --fillBlank | Fill blank files with export {} | false |
| --forceDefaultExport | Injects export default undefined if not present | false |
| --sourceMap | Patch source map files to point to the updated files. | false |
| --cwd | cwd | process.cwd() |
| --out | Output dir. Defaults to cwd, so files would be overwritten | process.cwd() |
| --debug | Prints debug notes | |#### --target vs --src
When
--src option is used, the util just modifies file contents in place.
--target also renames files to change their extension.
You may prevent deletion original of files by using --no-unlink.#### glob patterns
By default, the util looks for
ts/tsx files in src directory and js/d.ts files in target. But you can specify custom patterns via corresponding options. For example: --src='src/main/ts/*/.ts'.
`js
const patterns =
sources.length > 0
? sources.map((src) => src.includes('') ? src : ${src}//.{ts,tsx})
: targets.map((target) => target.includes('') ? target : ${target}//.{js,d.ts})
`$3
`ts
import { fix, IFixOptions } from 'tsc-esm-fix'const fixOptions: IFixOptions = {
tsconfig: 'tsconfig.build.json',
dirnameVar: true,
filenameVar: true,
ext: true
}
await fix(fixOptions)
`
`typescript
export interface IFixOptions {
cwd: string
src?: string | string[]
target?: string | string[]
out?: string
tsconfig?: string | string[]
dirnameVar: boolean
filenameVar: boolean
fillBlank?: boolean
forceDefaultExport?: boolean
sourceMap?: boolean
ext: boolean | string
tsExt: string | string[]
jsExt: string | string[]
unlink?: boolean,
debug?: boolean | IFunction
}
`Alternatives
* https://github.com/mothepro/tsc-esm
* https://github.com/digital-loukoum/tsc-esm
* https://github.com/beenotung/fix-esm-import-pathContributing
Feel free to open any issues: bug reports, feature requests or questions.
You're always welcome to suggest a PR. Just fork this repo, write some code, add some tests and push your changes.
Any feedback is appreciated.References
* TypeScript/issues/13422: TypeScript and script type="module"
* TypeScript/issues/28288: Feature: disable extensionless imports
* ts-jest/issues/1174: import.meta not allowed
* esbuild/issues/1043: Empty file bundles as { default: {} }`