TS and ts-jest meet “type”: “module”
Anton Golub
Posted on May 11, 2021
To save anybody’s googling time
ES modules are becoming more widespread pkg format. So dependency updates break our es5-builds more often. This is expected. This is inevitable. This is the price of progress.
Trouble #1
Typescript code can be easily compiled into the latest versions of javascript. Almost. Imagine code snippet:
import {generate} from './license'
and tsconfig.json
{
"compilerOptions": {
"module": "es2020",
"outDir": "target/es6"
}
}
gives:
import { generate } from './license'; // ; ← is the diff
Everything seems ok until ”type”: “module”
is not added to package.json:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module ‘~/projects/license/target/es5/license' imported from /~/projects/license/target/es5/cli.js
Did you mean to import ../../../license?
at finalizeResolution (internal/modules/esm/resolve.js:276:11)
at moduleResolve (internal/modules/esm/resolve.js:699:10)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
at Loader.resolve (internal/modules/esm/loader.js:86:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
TypeScript/issues/13422: TS should add .js
extensions for rel paths as required by ECMA standard. But is doesn’t yet. Well, the dirty fix may be found in issue comments.
find www/js -type f -name '*.js' -print0 | xargs -0 sed -i '' -E 's/from "([^"]+)";$/from "\1.js";/g'
Restrictions:
- String replacer can not properly handle ‘./module’ and
./module/index
loading cases. - Does not handle dynamic imports like
import(‘./foo’).then(…)
-
sed -i '' -E
works on Mac only, Linuxsed
should use justsed -i -e
. And you need to handle this in your build script:bash if [[ "$OSTYPE" == "darwin"* ]]; then … else … fi
or maybe run perl instead:perl -pi -e
.
Trouble #2
__dirname
. And __filename
too.
ReferenceError: __dirname is not defined
at loadTemplate (file:///~/projects/license/target/es5/license.js:1:651)
at render (file:///~/projects/license/target/es5/license.js:1:535)
at generate (file:///~/projects/license/target/es5/license.js:1:800)
at file:///~/projects/license/target/es5/cli.js:2:692
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at async Loader.import (internal/modules/esm/loader.js:166:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)
Now we have to use import.meta
as the official NodeJS documentation says:
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
Unfortunately, ts-jest does not support this API now: ts-jest/issues/1174. Therefore, we have to keep __dirname/__filename
in TS sources and perform the replacement in the bundles. Behold the glory of regex inside the regex replacer with escaped backslash escapes:
"build:fix-module-dirname": "find target/es5 ./target/es6 -type f -name '*.js' -print0 | xargs -0 perl -pi -e \"s/__dirname/\\/file:\\\\\\\\\\\\/\\\\\\\\\\\\/(.+)\\\\\\\\\\\\/\\[^\\/\\]\\/.exec(import.meta.url)[1]/g\""
.
This piece of code just replaces all __dirname
occurrences with /file:\/\/(.+)\/[^/]/.exec(import.meta.url)[1]
.
Restrictions:
- Poor readability
- Requires
sed / perl
Fix
Here’s an attempt to solve mentioned issues in a more convenient and maintainable form — as js util.
antongolub / tsc-esm-fix
Make Typescript projects compatible with esm/mjs requirements
Features
- Finds and replaces
__dirname
and__filename
refs withimport.meta
. - Injects extentions to relative imports/re-exports statements.
-
import {foo} from './foo'
→import {foo} from './foo.js'
- Pays attention to index files:
import {bar} from './bar'
→import {bar} from './bar/index.js'
-
- Follows
outDir
found in tsconfig.json. - Changes files extentions if specified by opts.
- Supports Windows-based runtimes.
Install
yarn add tsc-esm-fix -D
CLI
tsc-es2020-fix [opts]
Option | Description | Default |
---|---|---|
--tsconfig |
Path to project's ts-config(s) | tsconfig.json |
--target |
Entry points where compiled files are placed for modification | If not specified inherited from tsconfig.json compilerOptions.outDir |
--dirnameVar |
Replace __dirname usages with import.meta
|
true |
--filenameVar |
Replace __filename var references import.meta
|
true |
--ext |
Append extension to relative imports/re-exports | .js |
--cwd |
cwd | process.cwd() |
--out |
Output dir. Defaults to cwd, so files will be overridden |
JS/TS
import { fix, IFixOptions } from 'tsc-esm-fix'
const fixOptions: IFixOptions = {
tsconfig: 'tsconfig.build.json',
dirnameVar: true,
filenameVar: true,
ext: true
}
await fix(fixOptions)
export interface IFixOptions {
cwd: string
out?: string,
target?: string | string[]
tsconfig: string | string[]
dirnameVar: boolean
filenameVar: boolean
ext: boolean | string
}
UPD (2021-08-15) Alternatives
Refs
- TypeScript/issues/13422
- TypeScript/issues/28288
- ts-jest/issues/1174
- stackoverflow.com/how-to-use-import-meta-when-testing-with-jest
- Pure ESM package
- stackoverflow.com/alternative-for-dirname-in-node-when-using-the-experimental-modules-flag
- ecma262/#sec-imports
- ERR_REQUIRE_ESM
- Publishing Node modules with TypeScript and ES modules
Posted on May 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.