跳至主要内容

[TS] Setup TypeScript Monorepo Template

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

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

專案位置:pjchender/typescript-monorepo-template 另一個可參考的:fem-fullstack-ts

workspaces: setup with yarn

docs: npm workspaces

@@ -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/*"
+ ]
}
備註

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

如果是多個不同性質的專案,可以設定成類似:

// package.json
{
"workspaces": ["client", "server", "shared"]
}

typescript: composite project

setup tsconfig (build project not by tsc)

composite @ TypeScript > tsconfig

備註

下述設定方式適合用在透過 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 進行打包。

備註

執行 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

setup eslint for TS

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"
},

conventional commit

conventional-changelog/commitlint @ GitHub

$ yarn add -WD @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes commitlint husky lerna-changelog

# init husky
$ npx husky-init && yarn # Yarn 1

# test whether the commit message is valid
$ echo "build(api): change something in api's build" | yarn commitlint

# 根據 conventional commits 自動升版
$ lerna version --conventional-commits

補充 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