Typescript, NodeJS and ES6/ESM Modules
Tags: typescript, node, javascript
Published: 2021-04-28 12:06:01

I write this as a sequel to [a previous article](https://dev.to/asteinarson/typescript-node-js-importing-knex-into-es6-module-1poc) of getting ES6 module imports to work with other NPM modules, from TypeScript. 

The _CommonJS_ import framework (**require**) is how node.js was developed and getting everything smooth and working with modern static **import** is sometimes not so easy, and often not well documented.

I favor static imports, because the syntax and lack of obvious structure around **require** is just so full of magic and _just works_ provided you have spent a number of years working inside that universe. 

#### Basic settings
As a starter, whether a Node project defaults to CommonJS modules or ESM is decided by two settings - which must correspond. 

*tsconfig.json*:
```json 
{
  "compilerOptions": {
    // ... 
    "module": "esnext", /* Or 'commonjs'. */
```
*package.json*:
```json 
{
   // ... 
   "type": "module",  /* Or 'commonjs' */ 
```
 
 
## The problem here - relating to TypeScript 

If I have a local TS file (_utils.ts_) like this: 
```typescript
// Invert keys and values of 1-level deep object
export function invert(o: Dict<BaseTypes>) {
    // ... implementation here ...
}
```
and then want to use it from another file: 
```typescript
import {invert} from './utils';
```
then that import will only work (everything else being default settings in TS and Node), as long as I stay with **CommonJS** modules. 

However, when I change over to ESM modules (see below), the compiled Javascript will no longer work. Because Node.js will be trying to import **exactly**: 
```javascript
import {invert} from './utils';
```
And honestly, there is no such file - that is _./utils_ - without the file extension added to it. 

The explanation is that as TypeScript transpiles this, then for CommonJS, it ends up calling **require()**, and some logic on top of that. And... **require()** accepts local javascript file names... without file extension.  

So if I want my code to work with Node with ESM, I need to change it to: 

```javascript
import {invert} from './utils.js';
```

That means, I need to have different code bases, if I target CommonJS or ESM. Not very good. 
 
 
## Sort of going forwards... 
For a while I accepted the compromise of adding _.js_ to every local import... But then I wanted to add **Jest** testing on top of this. Which (together with _ts-jest_) does its own building of the test files (behind the scenes, I think using Babel). And **ts-jest** (whatever my _tsconfig/package.json_ said) would not accept imports from explicit Javascript files: 

```javascript
import {invert} from './utils.js';  // ts-jest errors here
```

So I needed to fix it differently. (Understanding how/why _ts-jest_ configures TypeScript/Node differently I did not want to enter). 

## A couple of solutions
One approach is to use the NPM module **esm** - however I never went in that direction. And I would like to avoid pulling in dependencies to solve this. 

But it turns out there is a Node flag to do solve exactly this problem: _--experimental-specifier-resolution=node_. (You find it [towards the bottom here](https://nodejs.org/api/esm.html)). 

Wow, all settled ? 

Well, we also need to launch Node in some different ways (from terminal, from a Node **bin** script, from the VsCode debugger, and the last two presents small challenges). 

### Passing Node flags into the VsCode debugger
It turns out there is a **launch.json** entry for this: 
```JSON
    "configurations": [
        {
            "type": "pwa-node",
            // ...
            "runtimeArgs": ["--experimental-specifier-resolution=node"],
            "program": "${workspaceFolder}/src/cmd.ts",
```
(Scroll down to [the bottom of this page](https://code.visualstudio.com/docs/nodejs/nodejs-debugging) for docs). 

And it actually does the job (I found various other suggestions where to put the Node arg, however I think they targeted an old version of the Javascript debugger). 
 
 
### Passing Node flags into an Npm bin command
To run a JS script directly from the terminal (without prefixing it with _node_) we can use the *bin* section of *package.json*: 
```JSON 
... 
"bin": {
    "my_cmd": "./path/to/my/script",
  },
```
However, if the target script is JS/TS (yes, it would be), we need to insert a shebang there, to make it executable: 
```bash
#!/usr/bin/env node
// Rest of JS/TS here
```
But the obstacle here is that we cannot pass options to Node in the shebang. Sorry. We stumble into a _bash fact of life_ here, and there's no simple way around it, for Node/TS/JS. 

There is a magic hack to this [over here](http://sambal.org/2014/02/passing-options-node-shebang-line/). However that fulfills my definition of being so cryptic and non-intuitive (for anyone who did not patiently learn Bash internals) that I cannot recommend it. 

The solution instead is to let the _bin_ command point to a shell script, and let that one invoke Node with required options: 

For me: 
```JSON
  "bin": {
    "knemm": "./shell/knemm",
  },
```
and _knemm_ then being: 
```bash
#!/bin/bash 
# Get directory of calling script
DIR="$( cd "$( dirname "$0" )" &> /dev/null && pwd )"
if [ "$(echo $DIR | grep '.nvm')" ]; then
    DIR="$(dirname "$(readlink -f "$0")")"
fi 
/usr/bin/env node --experimental-specifier-resolution=node $DIR/../lib/cmd-db.js $@
```

The explanation of the ''DIR'' part is that the current directory (inside my Node project) is lost when the command is invoke as a symlink (see below). I need to point to the JS file in a relative way, so I then need the directory. [Here's background how to find the script dir](https://unix.stackexchange.com/questions/17499/get-path-of-current-script-when-executed-through-a-symlink).

To install this, as a global command, I run: 
```bash
$ npm link
```
Then a global symlink appears: 
```bash 
$ knemm 
... command outputs, all ESM imports are resolved! 
```
(At some point I needed to manually remove those symlinks generated by _npm link_ as it wouldn't change those, when I edited _package.json_.) 

## Discussion 
This has taken some hours (over some weeks) to work out, and I write this to summarize the effort and the learning. Partly so I remember better and then maybe it helps someone. 

I hope all rough parts of using modern JS under Node will be gradually smoothened / paved out. 

Of course, the last part of my solution is Linux/Bash centric. But nowadays, with WSL/WSL2, also anyone on Windows can access a good Linux environment. So I do not see a downside with that (that's how all of this was developed).