ts-nodeでサクッとWEBサイトのTypeScriptやNode.jsサンプルを動かすの巻

# はじめに
以前、サイトに掲載されているサンプルJavaプログラムをサクっと動かせるとして[jshell](https://qiita.com/SP-ya/items/9449d29291f577889180)の記事を投稿しましたが、今回はTypeScriptをサクっと動かせるツールとしてts-nodeの記事を書きました。

ts-nodeをREPLモードとtsファイル指定モードで設定を変えないといけないのがちょっと今一な記事となっています。だれか改善策を知っている方がいたら是非ご教示下さい。

独り言：ts-nodeをREPLで使う人って少ないのかなー。

# バージョン情報
- Nodejs:20.x
- TypeScript: 5.2.2
- ts-node: 10.9.1

# 環境構築

## Node.js

### Node.jsインストール
[ここ](https://nodejs.org/en)からインストーラダウンロードしてインストール。

### npm initコマンドでpackage.jsonを作成する

<details><summary>npm init実行サンプル</summary><div>

```console:npm 
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (ts-node) ts-node-verify-project
version: (1.0.0)
description: ts-node検証プロジェクト
entry point: (index.js)                                                                                                                                                                                                                  
test command:                                                                                                                                                                                                                            
git repository:                                                                                                                                                                                                                          
keywords:                                                                                                                                                                                                                                
author:                                                                                                                                                                                                                                  
license: (ISC)                                                                                                                                                                                                                           
About to write to C:\ts-node\package.json:

{
  "name": "ts-node-verify-project",
  "version": "1.0.0",
  "description": "ts-node検証プロジェクト",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) yes
```
</div></details>


## TypeScript

### TypeScriptインストール

```console
npm install -D typescript
```

### TypeScript初期設定

```console
tsc --init
```

「tsconfig.json」ファイルが作成されるので以下の箇所を修正する。

```json
"target": "es2016"
"module": "commonjs"
// "moduleResolution": "node10"
```
を
```json
"target": "es2022"  // top-level awaitが使いたいから
"module": "Node16"
"moduleResolution": "Node16"    // コメントアウトも外す
```

### Node.jsのTypeScript用定義をインストール
```
npm install -D @types/node
```

## ts-node

### ts-nodeをインストール

```console
npm install -D ts-node
```

# ts-nodeをREPLモードで使ってみる

残念ならが設定をREPLモードとtsファイル指定モードで変える必要がある。
詳細は[発生したトラブル](#発生したトラブル)を参照。

REPLは[Node.jsのREPL](https://nodejs.org/api/repl.html)がベースとなっているので、こちらも参照するとよいかもしれません。

## 設定

### tsconfig.json

```json
{
  "compilerOptions": {
    たくさんの設定
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS",                    
      "allowImportingTsExtensions": true,     
    }
  }
```

### pakages.json
typeの設定は不要。
ただし、自作のモジュールをimportしない場合は、削除しなくてもOK。
自作のモジュールとは、別tsファイルでexportするモジュールのこと。

```json
//  "type": "module"    // ある場合は削除
```

### 起動

```js
./node_modules/.bin/ts-node --esm 
// or ./node_modules/.bin/ts-node-esm 
```


## ts-nodeを使ってみる

では、さっそく。

```console
./node_modules/.bin/ts-node --esm
>
```
">"と表示され、入力待ち状態になる。

## 基本

```js
> let i = 1
undefined

> i
1

> console.log(i)
1
undefined

> .type i
let i: number

> function add(a: number, b:number): number { return a + b }
undefined

> console.log(add(1, 2))
3
undefined

```
入力するたびに「undefined」と表示されるが気にしない。
jshellみないに変数一覧とかのコマンドはなさそうだ。
Node.jsの[REPLのコマンド](https://nodejs.org/api/repl.html#repl_commands_and_special_keys)と同じコマンドは使える。   
と思ったら```.clear```が正常に動作しない。。。[これ](http://corpus.hubwiz.com/2/node.js/18971034.html)が原因か。。。.exitして再起動かな。

## awaitも使えるよ

```js
>  await Promise.resolve(123)
123
```

### fsなんかのNode.jsコアモジュールはimportしなくても使えた

```js
> let csvData = fs.readFileSync('sample1.csv', 'utf-8');
undefined
> console.log(csvData)
id1,data1
id2,data2
undefined
>
```
fsのpromise版の場合は、こんな感じ。
```js
> let csvData2 = await fs.promises.readFile('sample1.csv', 'utf-8');
undefined
> console.log(csvData2)
id1,data1
id2,data2
undefined
>
```

### 自作モジュールimportも出来るぞ

ディレクトリ構成
```console
C:/ts-node
│  package-lock.json
│  package.json
│  sample1.csv
│  tsconfig.json
│  
├─node_modules
│  
└─src
        main.ts
        Classes.ts
```

<details><summary>Classes.ts</summary><div>

```js:Classes.ts
import { format as dateFormat } from 'date-fns';

export class Util {
  static getToday(): string {
    return dateFormat(new Date(), 'yyyy/MM/dd');
  }
}

export class Person {
  private _name : string;
  constructor(name : string) {
    this._name = name;
  }
  get name(): string {
    return this._name;
  }
}
```
</div></details>

```js
> import { Person, Util } from './src/Classes.ts'
undefined

> let p = new Person('SP')
undefined

> p.name
'SP'
```

### ライブラリも使えるぞ

今回は日付操作のライブラリ「date-fns」を使用。

date-fnsをインストール
```console
npm i -D date-fns
```

```js
> import { format as dateFormat } from 'date-fns'
undefined
> console.log(dateFormat(new Date(), 'yyyy/MM/dd'))
2023/11/15
undefined 
```

### 終了

```console
> .exit
```
または、Ctrl+Cを２度。



## ts-node tsファイル起動モード

### tsconfig.json

```"module": "CommonJS"```などのts-nodeの設定は不要。

```json
{
  "compilerOptions": {
    たくさんの設定
  },
// ts-nodeの設定は不要。   // あったら削除する
//  "ts-node": {
//    "compilerOptions": {
//      "module": "CommonJS"
//    }
  }  
```

### pakages.json
typeを"module"設定する。

```json
"type": "module" 
```

### 起動

```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm .\src\main.ts
```

<details><summary>main.ts</summary><div>

```js
import { Person, Util } from './Classes.js';

// Personインスタンス生成
const person = new Person('AOKI');
console.log(person.name);

// 本日日付
console.log(Util.getToday());
```
</div></details>

# 発生したトラブル

## その１

```let i = 1```などのステートメントを入力すると

```console
export {};
^^^^^^
Uncaught SyntaxError: Unexpected token 'export'
```

とエラーが表示される。

解決策：tsconfig.jsonに```"module": "CommonJS"```を追加する。
[解決ソース](https://github.com/TypeStrong/ts-node/issues/935#issuecomment-913179458)

```json
{
  "compilerOptions": {
    たくさんの設定
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }  
}
```

## その２
その１の対応した状態で[ドキュメント](https://typestrong.org/ts-node/docs/usage)に書かれている通り

```console
./node_modules/.bin/ts-node --esm
```
で起動し自作モジュールをimportしてインスタンス生成するとエラーになる。

```js
./node_modules/.bin/ts-node --esm
// or ./node_modules/.bin/ts-node-esm

> import { Person, Util } from './src/Classes.ts';
<repl>.ts:4:30 - error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
4 import { Person, Util } from './src/Classes.ts';
```

その８で解決。

## その３
その２のimportをClasses**.ts**ではなく、Classes.**js**にしたらimportは成功したがインスタンス生成でエラーになる。

```js
// お、importは成功した！！
import { Person, Util } from './src/Classes.js'
undefined

// インスタンス生成
const p = new Person('SP')

node:internal/modules/cjs/loader:1051
  throw err;
  ^
Uncaught Error: Cannot find module './src/Classes.js'
```


## その４
起動方法を変えて、```node --loader ts-node/esm```で起動したらワーニングが発生した。

```js
node --loader ts-node/esm

Welcome to Node.js v20.9.0.
Type ".help" for more information.
> (node:34136) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
```

ワーニングが発生しないように対応。

```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm
Welcome to Node.js v20.9.0.
Type ".help" for more information.
```

**追記**

ワーニングを握りつぶさないで、ちゃんと指示通りに起動したほうがいいですね。

```js
node --import "data:text/javascript,import { register } from 'node:module'; import { pathToFileURL } from 'node:url'; register('ts-node/esm', pathToFileURL('./'));" .\src\main.ts
```

## その５

その４対応で自作モジュールをimportしたらエラーになった。

```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm

>import { Person, Util } from './src/Classes.ts'
import { Person, Util } from './src/Classes.ts'
^^^^^^
Uncaught:
SyntaxError: Cannot use import statement inside the Node.js REPL, alternatively use dynamic import: const { Person, Util } = await import("./src/Classes.ts");
```

エラーの指示通りimportの形式を変更したが別のエラーになった。
```js
> const { Person, Util } = await import("./src/Classes.ts");
Uncaught ReferenceError: exports is not defined in ES module scope
```


## その６
その４対応でtsファイル指定して起動するとエラーになった。

```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts

file:/src/main.ts:2
Object.defineProperty(exports, "__esModule", { value: true });
                      ^
ReferenceError: exports is not defined in ES module scope
```

### その7
その1の対応をやめて、その4の対応で起動し、import形式はconst await import形式したら
import、インスタンスの生成はうまくいったが、REPLがtsモードになっていない。。。
```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm

// import成功！
> const { Person, Util } = await import("./src/Classes.ts");
undefined

// インスタンス生成も成功！
> const p = new Person('SP')
undefined

> p.name
'SP'

// tsモードになっていない！！。だめだ。。
> let i:number = 1
let i:number = 1
     ^
Uncaught SyntaxError: Unexpected token ':'

```

### その８
ts-nodeのREPLをtsモードで起動するには``` ./node_modules/.bin/ts-node --esm```で起動するしかなさそうだ。
しかしimportしたら、やはりエラーになった（その２と同じ）。

```js
> import { Person, Util } from './src/Classes.ts'
<repl>.ts:4:30 - error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.

4 import { Person, Util } from './src/Classes.ts'
```

これは、tsconfig.jsonにallowImportingTsExtensionsを追加したら、解決した。

```json
{
  "compilerOptions": {
    たくさんの設定
  },
  "ts-node": {
    "esm": true,
    "compilerOptions": {
      "module": "CommonJS",    // これもあわせて「その１」解決策のために追加
      "allowImportingTsExtensions": true,
    }
  }  
}
```

### その９
その８の対策をしたらimportはうまくいったが、インスタンス作成でエラーになった。

```js
./node_modules/.bin/ts-node --esm 

> import { Person, Util } from './src/Classes.ts'
undefined

> const p2 = new Person('SP')
Uncaught:
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: C:\myfolder\ts-node\src\Classes.ts 
``` 

package.jsonの```"type": "module"```を削除したらうまくいった。 

tsconfingで```"module": "CommonJS"```設定しているからかなー。

# トラブル解決策
残念ながらts-nodeをREPLモードで起動する場合とmain.tsファイルなどを指定して起動する場合で、設定を変える必要がある。

## ts-node REPLモード

### tsconfig.json

```json
{
  "compilerOptions": {
    たくさんの設定
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS",                    // その１対策
      "allowImportingTsExtensions": true,      // その８対策
    }
  }
```

### pakages.json
typeの設定は不要。

```json
//  "type": "module"    // その９対策
```

### 起動

```js
./node_modules/.bin/ts-node --esm 
// or ./node_modules/.bin/ts-node-esm 
```

## ts-node tsファイル起動モード

### tsconfig.json

```"module": "CommonJS"```などのts-nodeの設定は不要。

```json
{
  "compilerOptions": {
    たくさんの設定
  },
// ts-nodeの設定は不要。   // その７で行った対策
//  "ts-node": {
//    "compilerOptions": {
//      "module": "CommonJS"
//    }
  }  
```

### pakages.json
typeを"module"設定する。

```json
"type": "module" 
```

### 起動

```js
node --no-warnings=ExperimentalWarning --loader ts-node/esm .\src\main.ts
```