# 建立 Node.js 使用 ESM 搭配 TypeScript, ESLint 與 VSCode 的專案範本

我在兩年前曾經寫過一篇[如何快速建置一個 Node.js 專案並使用 TypeScript 與 VSCode 進行開發](/post/2021/07/18/Build-Nodejs-Dev-Env-with-TypeScript-Nodemon-and-VSCode)文章，由於現在的 Node.js 大多以 ESM 為主流，TypeScript 也更新到 5.x 版，而且當時的 TSLint 也都被換成 ESLint 為主了，所以這篇文章就來更新一下內容，幫助大家更方便、更快速的建立一個支援 [TypeScript](https://www.typescriptlang.org/), [ESLint](https://typescript-eslint.io/), [VSCode](https://code.visualstudio.com/) 與 [ESM](https://israynotarray.com/javascript/20210424/616364031/) 並搭配 [esbuild](https://esbuild.github.io/) 的專案範本。

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/4862df3268a16299ca87-6c0f03a0-d815-420b-b1b4-f9008e9ed538.webp)

### TL;DR (Too Long; Don't Read)

依照慣例，沒空看文章的人可以直接使用我寫好的 [Duotify.Templates.DotNetNew](https://www.nuget.org/packages/Duotify.Templates.DotNetNew) 專案範本，快速建立專案：

1.  使用 [.NET CLI](https://dotnet.microsoft.com/download) 命令列工具，安裝我寫好的專案範本
    
    ```bash
    dotnet new install Duotify.Templates.DotNetNew
    ```
    
2.  使用 `dotnet new tsnode-esm` 建立專案範本並執行 `npm install` 安裝 npm 套件
    
    ```bash
    mkdir myproj && cd myproj
    dotnet new tsnode-esm -c "Will 保哥"
    npm install
    ```
    
3.  開啟 Visual Studio Code 並對 `src/app.ts` 檔案進行開發
    
    ```bash
    code .
    ```
    
4.  按下 `Ctrl+Shift+B` 即可自動執行 `npm start` 命令
    
    程式會自動監視檔案變更並自動重啟程式！
    

### 如何設定 TypeScript Node.js 與原生的 ESM 模組支援

這篇文章我就不會重頭建立起專案了，完整的建立步驟依然可以參見 [如何快速建置一個 Node.js 專案並使用 TypeScript 與 VSCode 進行開發](/post/2021/07/18/Build-Nodejs-Dev-Env-with-TypeScript-Nodemon-and-VSCode) 文章的說明。

要設定 TypeScript 搭配 Node.js 並使用 ESM 模組系統，最有可能先遇到的問題，就是以下錯誤：

```
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
```

而這個錯誤必須靠許多設定上的調整來解決，老實說，知識含量有點大，以下是大致的更新步驟！

1.  首先，我們必須在 `package.json` 檔案中加入以下設定：
    
    ```json
    {
      "name": "my-project",
      "type": "module",  // <-- 加入這一段
    }
    ```
    
    這段設定可以讓 Node 在執行程式時，主要採用 ESM 為預設模組系統。
    
2.  所有的 `import` 語法載入專案內的 `*.ts` 檔案都一定要這樣寫：
    
    ```javascript
    import { qux } from './bar/baz.js'; // <-- 注意: 副檔名一定要加上 .js 結尾
    ```
    
3.  更新你的 `tsconfig.json` 設定檔
    
    這裡我們會參考到 [https://github.com/tsconfig/bases](https://github.com/tsconfig/bases) 這個專案，這裡有寫好一堆常用的 `tsconfig.json` 設定檔，其中也包含 Node LTS 的環境，我們可以直接從自己專案的 `tsconfig.json` 參考並繼承這裡的設定檔，就可以大幅簡化我們的專案內的 `tsconfig.json` 設定。
    
    先安裝支援 Node LTS 的 npm 套件：
    
    ```bash
    npm install --save-dev @tsconfig/node-lts
    ```
    
    然後修改你的 `tsconfig.json` 繼承 `@tsconfig/node-lts/tsconfig.json` 設定檔：
    
    ```json
    "extends": "@tsconfig/node-lts/tsconfig.json"
    ```
    
    附帶補充一下我從 `node_modules\@tsconfig\node-lts\tsconfig.json` 看到的設定內容如下：
    
    ```json
    // This file was autogenerated by a script
    // Equivalent to a config of: node18
    {
      "$schema": "https://json.schemastore.org/tsconfig",
      "display": "Node LTS",
      "_version": "18.1.0",
      "compilerOptions": {
        "lib": ["es2023"],
        "module": "node16",
        "target": "es2022",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "moduleResolution": "node16"
      }
    }
    ```
    
    所以我們的 `tsconfig.json` 就只需要這樣設定即可：
    
    ```json
    {
        "$schema": "https://json.schemastore.org/tsconfig",
        "extends": "@tsconfig/node-lts/tsconfig.json",
        "compilerOptions": {
            "resolveJsonModule": true,
            "outDir": "./dist"
        },
        "include": [
            "src/**/*"
        ]
    }
    ```
    
4.  設定 [ts-node](https://www.npmjs.com/package/ts-node) 可以支援 ESM
    
    加入以下設定到 `tsconfig.json` 之中：
    
    ```json
    {
        ...,
    
        "ts-node": {
            "esm": true
        }
    }
    ```
    
    目前我們的 `tsconfig.json` 設定如下：
    
    ```json
    {
        "$schema": "https://json.schemastore.org/tsconfig",
        "extends": "@tsconfig/node-lts/tsconfig.json",
        "compilerOptions": {
            "resolveJsonModule": true,
            "outDir": "./dist"
        },
        "include": [
            "src/**/*"
        ],
        "ts-node": {
            "esm": true
        }
    }
    ```
    
    > 注意: 目前 [ts-node](https://github.com/TypeStrong/ts-node) 尚未支援 TypeScript 5.0 的 `moduleResolution: bundler` 設定，你可以到 [moduleResolution bundler support #2034](https://github.com/TypeStrong/ts-node/issues/2034) 追蹤這個議題。這個 `moduleResolution: bundler` 設定可以讓你的程式在 `import` 時，在 `from` 的地方不用加上 `.js` 副檔名，例如：`import { version } from "./utils";`，所以現在 `import` 的時候只好乖乖加上 `.js` 結尾。詳見 [TypeScript 5.0: new mode bundler & ESM](https://dev.to/ayc0/typescript-50-new-mode-bundler-esm-1jic) 文章。
    
5.  加入 ESLint 設定
    
    基本上我是參考 [Getting Started | typescript-eslint](https://typescript-eslint.io/getting-started) 這篇文章進行設定。
    
    不過我有稍微做一點修正，加上了 `env: { node: true }` 設定：
    
    ```javascript
    /* eslint-env node */
    module.exports = {
        extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
        parser: '@typescript-eslint/parser',
        plugins: ['@typescript-eslint'],
        root: true,
        env: {
            node: true,
        },
    };
    ```
    
6.  設定 [esbuild](https://esbuild.github.io/) Bundler
    
    我們在準備發佈應用程式時，就有可能會透過 [esbuild](https://esbuild.github.io/) 來做 Bundling 的動作，徹底減少部署的檔案數，避免發佈 `node_modules` 資料夾！
    
    以下是我對 `package.json` 的 `scripts` 區段所做出的設定：
    
    ```json
    {
      "name": "myproj",
      "type": "module",
      ...
      "main": "dist/app.js",
      "scripts": {
        "esbuild-base": "esbuild ./src/app.ts --bundle --outfile=dist/app.js --format=esm --platform=node",
        "esbuild:prod": "npm run esbuild-base -- --minify",
        "esbuild": "npm run esbuild-base -- --sourcemap",
        "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch",
    
        "start": "nodemon src/app.ts",
        "build": "tsc",
        "node": "tsc && node dist/app.js"
      },
      ...
    }
    ```
    

### 相關連結

-   [typescript-eslint](https://typescript-eslint.io/)
    -   [Getting Started | typescript-eslint](https://typescript-eslint.io/getting-started/)
-   [ESLint](https://eslint.org/)
    -   [Configuration Files - ESLint - Pluggable JavaScript Linter](https://eslint.org/docs/latest/use/configure/configuration-files)
-   [tsconfig/bases](https://github.com/tsconfig/bases#node-lts-tsconfigjson): Hosts TSConfigs to extend in a TypeScript app, tuned to a particular runtime environment
-   [ts-node - npm](https://www.npmjs.com/package/ts-node)
-   [esbuild](https://esbuild.github.io/)
    -   [esbuild - Getting Started](https://esbuild.github.io/getting-started/#bundling-for-node)
-   [Using TypeScript Node.js with native ESM](https://gist.github.com/slavafomin/cd7a54035eff5dc1c7c2eff096b23b6b)
-   [TypeScript 5.0: new mode bundler & ESM - DEV Community](https://dev.to/ayc0/typescript-50-new-mode-bundler-esm-1jic)
-   常見錯誤
    -   [SyntaxError: Cannot use import statement outside a module #922](https://github.com/TypeStrong/ts-node/issues/922)
    -   [Unknown file extension ".ts" error in ts-node \[Solved\]](https://bobbyhadz.com/blog/typescript-unknown-file-extension-error)
