Skip to main content

[TS] Setup TypeScript Monorepo Template

此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料:

monorepos 指的是在一個 git repository 中有多個 libraries/projects 在內,而不是把它拆成多個獨立的 git repository 去維護。

workspaces: setup with yarn#

@@ -1,9 +1,12 @@
{
"name": "js-ts-monorepos",
"version": "1.0.0",
"main": "index.js",
"repository": "git@github.com:mike-north/js-ts-monorepos.git",
"author": "Mike North <michael.l.north@gmail.com>",
"license": "BSD-2-Clause",
- "private": true
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ]
}
note

在 npm 7 之後也有支援 workspaces,用法可以參考最下方的補充。

typescript: composite project#

composite @ TypeScript > tsconfig

note

下述設定方式適合用在透過 tsc 編譯專案時,若是用其他編譯工具需自行調整。

我們不需要在每個 libraries/projects 中都建立各自的 tsconfig.json,而是可以把它定義在一個地方,library 內的 tsconfig 就去繼承它就好。

tsconfig.jsoncompilerOptions 中加上 composite: true 表示它是其中一部分的設定檔,加上這個項目後:

  • 若沒有指定 rootDir 預設會是帶有 tsconfig.json 檔案的那個目錄
  • 套用此項目的檔案一定要有 includefiles
  • declaration 預設會是 true
{
"extends": "../tsconfig.settings.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"include": [
"src"
]
}

執行 tsc 打包專案後,專案中會出現一支 tsconfig.tsbuildinfo,這支檔案用來告訴 TypeScript 打包好的檔案和當前的原始碼(source code)是否一致。

實作步驟#

  • packages 資料夾中建立 tsconfig.settings.json,這支檔案是要讓 packages/typespackages/utils 裡面的 tsconfig.json 繼承用的

    // packages/tsconfig.settings.json
    // 給 packages/ 中的 tsconfig 讀取用的共用設定檔
    {
    "compilerOptions": {
    "module": "CommonJS",
    "types": [],
    "sourceMap": true,
    "target": "ES2018",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "declaration": true
    }
    }
  • packages/types/tsconfig.jsonpackages/utils/tsconfig.json 則可以直接繼承 packages/tsconfig.settings.json,另外因為這裡面只是一部分的設定擋,所以要加上 composite: true

    // packages/types/tsconfig.json
    // packages/utils/tsconfig.json
    {
    "extends": "../tsconfig.settings.json",
    "compilerOptions": {
    "composite": true,
    "outDir": "dist",
    "rootDir": "src"
    },
    "include": ["src"]
    }
  • packages/tsconfig.json 則是打包檔案執行 tsc -b . 時執行會吃的 TypeScript 設定檔:

    // packages/tsconfig.json
    // tsc 會執行的 root 是這隻
    // 這支會對所有的 packages 打包所有必要的檔案
    {
    "files": [], // 只需針對 packages 內的程式碼執行 build,而不需要針對 packages 內的所有檔案都產生對應的 .js 檔
    "references": [
    {
    "path": "utils"
    },
    {
    "path": "types"
    }
    ]
    }
  • packages/types/package.jsonpackages/utils/package.json 中添加測試用的 script:

    // packages/types/package.json
    // packages/utils/package.json
    "scripts": {
    + "build": "tsc -b .",
    },

最終的專案結構會長像這樣:

├── package.json
├── packages
│   ├── tsconfig.json
│   ├── tsconfig.settings.json
│   ├── types
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── utils
│   ├── package.json
│   └── tsconfig.json
├── tsconfig.json

只需專案的「根目錄」執行 yarn tsc -b packages,即會針對 packages 資料夾中的每個 package 進行打包。

note

執行 tsc -b packages 時,會使用 packages/tsconfig.json 這支檔案。

testing: setup jest with babel#

延伸閱讀:

建立 packages/.babelrc

// packages/.babelrc
{
"presets": [
[
"@babel/preset-env",
{
// babel 會打包成相容於 node 12 的程式碼
"targets": {
"node": 12
}
}
],
"@babel/preset-typescript"
]
}

接著建立 packages/types/.babelrcpackages/utils/.babelrc,讓他們的設定檔都繼承自 packages/.babelrc

// packages/types/.babelrc
// packages/utils/.babelrc
{
"extends": "../.babelrc"
}

packages/types/package.jsonpackages/utils/package.json 中添加測試用的 script:

// packages/types/package.json
// packages/utils/package.json
"scripts": {
+ "test": "jest",
},

最終的資料夾結構長這樣:

├── packages
│   ├── .babelrc
│   ├── tsconfig.json
│   ├── tsconfig.settings.json
│   ├── types
│   │   ├── .babelrc
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── utils
│   ├── .babelrc
│   ├── package.json
│   └── tsconfig.json
└── tsconfig.json

linter: setup ESLint#

Mike North 根據過去經驗,強烈建議把 eslint 的設定擋放在專案的根目錄(.)而不是 packages 這個 workspaces 中,避免有些編輯器找不到 eslint 的設定。以 VSCode 來說,它只會認得一個 eslint 的設定,而沒辦法知道不同 workspaces 內的 package 要套用不同的 eslint 設定。因此,ESLint 的設定會是 workspaces level 的,一個設定檔會適用所有的 workspaces 內的所有 packages。

實作步驟#

  • 安裝 eslint 和 @typescript-eslint

    # -W 是指安裝在 workspaces
    $ yarn add -WD eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
  • 在根目錄建立 .eslintrc,這支會給編輯器和其他 packages 中的 .eslintrc 使用

    // ./eslintrc
    {
    "env": {
    "es2021": true
    },
    "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
    "ecmaVersion": 12
    },
    "plugins": ["@typescript-eslint"],
    "rules": {
    "prefer-const": "error",
    "@typescript-eslint/no-unsafe-member-access": "off",
    "@typescript-eslint/no-unsafe-call": "off",
    "@typescript-eslint/no-unsafe-assignment": "off"
    }
    }
  • 建立 /packages/types/.eslintrc/packages/utils/.eslintrc,如果不同專案有需要個別啟用的規則,可以在這裡設定(例如 JSX):

    // packages/types/.eslintrc.js
    // packages/utils/.eslintrc.js
    module.exports = {
    extends: '../../.eslintrc',
    parserOptions: {
    project: 'tsconfig.json',
    tsconfigRootDir: __dirname, // monorepo 的話要加上這行,才不會去找到最外層
    },
    };
  • /packages/types/package.json/packages/utils/package.json 中建立 lint 的指令:

    "scripts": {
    "build": "tsc -b .",
    "test": "jest",
    + "lint": "eslint src --ext js,ts",
    "clean": "rimraf dist *.tsbuildinfo"
    }
  • 建立 .eslintignore,將不需要 lint 的檔案忽略:

    packages/*/tests/*.ts

最後進到 /packages/types/packages/utils 後,就可以透過 yarn lint 執行 eslint。

clean:快速移除打包好的檔案#

透過 rimraf 這個套件來幫忙把專案中把包好的檔案清除。

  • 在根目錄執行 yarn add -WD rimraf 來安裝 rimraf

    # -W 是指安裝在 workspaces
    $ yarn add -WD rimraf
  • packages/types/package.jsonpackages/utils/package.json 中加入清除用的指令:

    // packages/types/package.json
    // packages/utils/package.json
    "scripts": {
    "build": "tsc -b .",
    "test": "jest",
    + "clean": "rimraf dist *.tsbuildinfo"
    },

packages manage: Lerna#

透過 lerna 可以管理 workspaces 內的所有套件,包含執行所有 packages 內的 run script、建立各 packages 間的關聯等。

Lerna 一樣是屬於 workspaces 層級的套件:

yarn add -WD lerna

接著在專案根目錄(.)建立 lerna.json

  • version 可以有幾種不同的做法
    • 定版號(稱作 lock step),這種方式會讓所有 workspaces 的 packages 在發布時有同樣的版號,適合用在每個 package 有一定程度的相依性時
    • independent:每個 package 可以有獨立的版號,適合用在每個 package 有獨立的 API 和改變時使用,例如只有某個 component 有升版,其他不動
// ./lerna.json
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.0.1",
"useWorkspaces": true,
"nohoist": ["parcel-bundler"]
}

scripty:統一管理 package.json 中的 script#

yarn add -WD scripty

基本使用#

scripty 可以執行專案中 scripts 資料夾內的檔案,但要先將該資料夾內的檔案賦予執行的權限:

chmod -R +x scripts # scripts/ 資料夾內的所有檔案都可以被執行,權限會是 -rwxr-xr-x (755)
chmod +x scripts/foobar # scripts/foobar 這支檔案可以被執行

修改 ./packages.json 的設定:

  • 在沒有指定 scripty 要執行檔案的位置時,scripty 自動會執行 ./scripts 資料夾內對應的 greet 檔:
// ./package.json
"scripts": {
"greet": "scripty",
},

實作步驟#

先針對根目錄的 package.json 進行修改,當執行的檔案放在資料夾中時,可以在 package.json 中透過 scripty 來指定:

  • scripty 會執行 ./scripts/workspace 資料夾內,對應的 greetbuildcleantestlint 的檔案:

    // ./package.json2
    "scripts": {
    "greet": "scripty",
    "build": "scripty",
    "clean": "scripty",
    "lint": "scripty",
    "test": "scripty"
    },
    // 告知 scripty 要執行檔案的位置
    "scripty": {
    "path": "./scripts/workspace"
    },
  • 資料夾結構長這樣:

    ├── scripts
    ├── packages
    │   ├── build.sh
    │   ├── lint.sh
    │   └── test.sh
    └── workspace
    ├── build.sh
    ├── clean.sh
    ├── greet
    ├── lint.sh
    └── test.sh

接著針對 workspaces 內的各 packages 進行設定:

// ./packages/utils/package.json
// ./packages/types/package.json
"scripts": {
"build": "scripty",
"test": "scripty",
"lint": "scripty",
"clean": "rimraf dist *.tsbuildinfo"
},
// 告知 scripty 要執行檔案的位置
"scripty": {
"path": "../../scripts/packages"
},

補充 npm workspaces#

workspaces @ npmjs

npm 7 支援 workspaces 的功能,讓開發者可以從單一個上層的 root package 管理底下多個 packages。

定義 workspaces#

只需要在 package.json 中使用 workspaces 這個欄位,裡面指的是 root packages 裡面的其他 packages:

// package.json
{
"name": "my-workspaces-powered-project",
"workspaces": ["workspace-a", "packages/*"]
}

workspaces 中套件安裝的位置#

當我們的資料夾結構是:

.
├── packages
│   ├── bar
│   └── foo
└── workspace-a

. 執行 npm install 時,會在根目錄的 node_modules 內會多一個名為 workspace-afoobar 的捷徑,它會指向 ./workspace-apackages/foopackages/bar 的專案位置。像是這樣:

.
├── node_modules
│   ├── bar -> ../packages/bar
│   ├── foo -> ../packages/foo
│   └── workspace-a -> ../workspace-a
├── packages
│   ├── bar
│   └── foo
└── workspace-a

對於會重複在多個 packages 中使用到的套件,實際套件會安裝在根目錄的 ./node_modules 中。

直接把 workspaces 內的套件當成 package 匯入#

由於會在根目錄的 node_modules 中建立捷徑,因此我們也可以直接把 workspaces 中定義好的套件 workspace-apackages/foopackages/bar 直接匯入。例如:

// ./workspace-a/index.js
module.exports = 'workspace-a';
// ./packages/foo/index.js
module.exports = 'foo';
// ./packages/bar/index.js
module.exports = 'bar';

匯入這些 workspaces 中的套件:

// ./index.js
const a = require('workspace-a'); // 'workspace-a'
const b = require('foo'); // 'foo'
const c = require('bar'); // 'bar'
// ./packages/foo/index.js
const bar = require('bar'); // 也可以直接匯入 'bar'
module.exports = 'foo';

在 root package 執行 npm 指令#

在執行 npm 指令時,可以透過 --workspace="package-name" 這個 option 來執行 workspaces 內某 package 的指令;如果是想要一次執行 workspaces 內的所有 npm script,則可以使用 --workspaces(多 s)。

例如,當執行:

# 只執行 workspace-a 裡的 npm script
$ npm run start --workspace=workspace-a
> [workspace-a] I am workspace-a
# 執行 workspace-a 和 bar 裡的 npm script
$ npm run start --workspace=workspace-a --workspace=bar
> [workspace-a] I am workspace-a
> [bar] I am bar
# 執行所有 workspaces,記得要加 s
$ npm start --workspaces

Reference#

Last updated on