TS and ts-jest meet “type”: “module”
Tags: typescript, tsc, es2020, esm
Published: 2021-05-11 15:02:57

_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:
```ts
import {generate} from './license' 
```
and **tsconfig.json**
```json
{
  "compilerOptions": {
    "module": "es2020",
    "outDir": "target/es6"
  }
} 
```
gives:
```js 
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](https://github.com/microsoft/TypeScript/issues/13422): TS should add `.js` extensions for rel paths as required by [ECMA standard](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). But is doesn’t yet. Well, [the dirty fix](https://github.com/microsoft/TypeScript/issues/13422#issuecomment-585725443) 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, Linux `sed` [should use](https://unix.stackexchange.com/questions/13711/differences-between-sed-on-mac-osx-and-other-standard-sed/381201#381201) just `sed -i -e`. And you need to handle this in your [build script](https://stackoverflow.com/a/65371526): 
```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](https://nodejs.org/api/esm.html#esm_import_meta_url):
```javascript
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](https://github.com/kulshekhar/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](https://unix.stackexchange.com/questions/379572/escaping-both-forward-slash-and-back-slash-with-sed/379573): 
` "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.   

{% github antongolub/tsc-esm-fix no-readme %}

### Features
* Finds and replaces `__dirname` and `__filename` refs with `import.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 | <cwd>

### JS/TS
```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
  out?: string,
  target?: string | string[]
  tsconfig: string | string[]
  dirnameVar: boolean
  filenameVar: boolean
  ext: boolean | string
}
```

## UPD (2021-08-15) Alternatives
* https://www.npmjs.com/package/tsc-esm
* https://www.npmjs.com/package/@digitak/tsc-esm

## Refs
* [TypeScript/issues/13422](https://github.com/microsoft/TypeScript/issues/13422)
* [TypeScript/issues/28288](https://github.com/microsoft/TypeScript/issues/28288)
* [ts-jest/issues/1174](https://github.com/kulshekhar/ts-jest/issues/1174)
* [stackoverflow.com/how-to-use-import-meta-when-testing-with-jest](https://stackoverflow.com/questions/64961387/how-to-use-import-meta-when-testing-with-jest) 
* [Pure ESM package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) 
* [stackoverflow.com/alternative-for-dirname-in-node-when-using-the-experimental-modules-flag](https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-when-using-the-experimental-modules-flag)
* [ecma262/#sec-imports](https://tc39.es/ecma262/#sec-imports)
* [ERR_REQUIRE_ESM
](https://dev.to/antongolub/errrequireesm-4j0h)
* [Publishing Node modules with TypeScript and ES modules](https://blog.logrocket.com/publishing-node-modules-typescript-es-modules/)