DEV Community

loading...

TS and ts-jest meet “type”: “module”

Anton Golub
System Architect at QIWI
Updated on ・4 min read

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'
Enter fullscreen mode Exit fullscreen mode

and tsconfig.json

{
  "compilerOptions": {
    "module": "es2020",
    "outDir": "target/es6"
  }
}
Enter fullscreen mode Exit fullscreen mode

gives:

import { generate } from './license'; // ; ← is the diff

Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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, Linux sed should use just sed -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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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. 


GitHub logo antongolub / tsc-esm-fix

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

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

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)
Enter fullscreen mode Exit fullscreen mode
export interface IFixOptions {
  cwd: string
  out?: string,
  target?: string | string[]
  tsconfig: string | string[]
  dirnameVar: boolean
  filenameVar: boolean
  ext: boolean | string
}
Enter fullscreen mode Exit fullscreen mode

UPD (2021-08-15) Alternatives

Refs

Discussion (0)