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
Top comments (0)