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)