Logo

dev-resources.site

for different kinds of informations.

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

Published at
5/11/2021
Categories
typescript
tsc
es2020
esm
Author
antongolub
Categories
4 categories in total
typescript
open
tsc
open
es2020
open
esm
open
Author
10 person written this
antongolub
open
TS and ts-jest meet “type”: “module”

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 Typescript projects 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

esm Article's
30 articles in total
Favicon
Bundling without a bundler with esm.sh
Favicon
Building NPM packages for CommonJS with ESM dependencies
Favicon
Web Development Without (Build) Tooling
Favicon
Dual Node TypeScript Packages - The Easy Way
Favicon
Oh CommonJS! Why are you mESMing with me?! Reasons to ditch CommonJS
Favicon
The Ongoing War Between CJS & ESM: A Tale of Two Module Systems
Favicon
How I optimized Carousel for EditorJS 2x in size.
Favicon
Transitioning from CommonJS to ESM
Favicon
Node.js, TypeScript and ESM: it doesn't have to be painful
Favicon
Set up Hot Reload for Typescript ESM projects
Favicon
Set up a Node.js project + TypeScript + Jest using ES Modules
Favicon
ESM & CJS: The subtle shift in bundlejs' behaviour
Favicon
Mastering the Art of ESM and CJS Package Handling
Favicon
Modules & Modules & Modules, Oh My!
Favicon
How to build TypeScript to ESM and CommonJS
Favicon
ES Modules & Import Maps: Back to the Future
Favicon
How to use ESM on the web and in Node.js
Favicon
Custom ESM loaders: Who, what, when, where, why, how
Favicon
Fix NX Node executor ERR_REQUIRE_ESM Error
Favicon
Creating a Node.js module for both CommonJS & ESM consumption
Favicon
STOP using require() in node backend
Favicon
JavaScript Module Ecosystem
Favicon
Declarative database modelling
Favicon
Expressjs: Javascript written in ECMAScript 2015 (ES6)
Favicon
How to use ES Modules with Node.js
Favicon
What does it take to support Node.js ESM?
Favicon
Build modular app with Alpine.js
Favicon
TS and ts-jest meet “type”: “module”
Favicon
ESM doesn't need to break the ecosystem
Favicon
constructor() dynamic import()

Featured ones: