Как настроить монорепозиторий TypeScript
В последние годы монорепозитории стали популярной темой в ИТ-сообществе. При использовании монорепозитория организация хранит все свои проекты в одном репо. Монорепозитории особенно популярны среди веб-разработчиков, поскольку большинство их проектов используют JavaScript или TypeScript и полагаются на одни и те же зависимости npm.
В этом руководстве мы рассмотрим, что такое монорепозиторий, почему и когда вам следует подумать о его внедрении, а также как настроить монорепозиторий TypeScript с помощью npm.
Monorepo
Monorepo — это подход к разработке программного обеспечения, при котором один репозиторий содержит код и активы нескольких проектов. Это глобальный проект, который содержит более мелкие проекты. Каждый из этих проектов может быть чем угодно, от отдельного приложения до повторно используемых пакетов компонентов или служебных функций. В монорепозитории эти пакеты обычно называются локальными пакетами.
Обычно монорепозиторий включает множество приложений и несколько пакетов; пакет может зависеть от других пакетов. Например, пакет ui
может использовать функции, предоставляемые пакетом utils
. С другой стороны, приложения обычно не зависят друг от друга.
Монорепо не следует путать с монолитным приложением! Монолит — это единый проект, компоненты которого должны быть развернуты вместе. С другой стороны, монорепозиторий состоит из нескольких независимых приложений, которые находятся в одном репозитории и совместно используют код через локальные пакеты, но могут быть развернуты самостоятельно. Таким образом, монорепозитории обеспечивают большую гибкость развертывания, чем монолиты.
Преимущества монорепозитория
Есть три конкретные причины, по которым следует рассмотреть возможность использования монорепозитория:
- Легче стандартизировать код и инструменты между командами. Поскольку весь код хранится в одном месте, проще применять одни и те же правила для отступов и линтинга. Каждая команда использует одни и те же библиотеки линтинга кода и форматирования, которые являются частью монорепозитория.
- Это обеспечивает лучшую видимость и совместную работу между командами. Разработчики имеют доступ ко всем проектам, что упрощает повторное использование и совместное использование кода.
- Это облегчает организацию файлов и упрощает управление зависимостями кода. В монорепозитории есть одна версия для каждой зависимости. Это означает, что вам больше не нужно беспокоиться о несовместимости или конфликтующих версиях внешних библиотек.
Использование монорепозитория
Идеальное время для принятия монорепозитория — это когда вы начинаете проект. Таким образом, вы сможете воспользоваться всеми его преимуществами с первого дня.
Стратегия монорепозитория особенно привлекательна, если у вас большое количество проектов или вы планируете быстро масштабироваться. С монорепозиторием вы не начинаете с нуля при создании нового проекта; все, что вам нужно сделать, это добавить его в монорепозиторий, и вы сразу получите доступ к нескольким пакетам и существующей настройке CI. Монорепозитории особенно полезны, если ваши проекты основаны на тех же технологиях, поскольку это позволяет совместно использовать код. Даже такие технологические гиганты, как Google, полагаются на огромные монорепозитории.
В то же время подход монорепозитория имеет свои подводные камни.
Во-первых, не забывайте, что настроить конвейер сборки для ваших монорепозиториев может быть непросто. Это особенно верно, если ваш монорепозиторий состоит из нескольких приложений, которые следует развертывать в определенном порядке. Если ваш конвейер не настроен идеально, ваши развертывания могут привести к простою или сбою.
Во-вторых, координация управления версиями всех продуктов, услуг и библиотек, входящих в монорепозиторий, — сложный процесс. Если вы принимаете монорепозиторий, вам нужно уделять еще больше внимания каждому коммиту. Кроме того, каждый член команды разработчиков должен хорошо разбираться в Git или аналогичной системе контроля версий.
Поэтому, если ваши команды используют очень разные технологии, полирепозиторий может быть лучшим подходом. Но имейте в виду, что переход от полирепозитория к монорепозиторию может быть обременительным, сложным и трудоемким. Хотя эта миграция иногда может стоить усилий, чем раньше вы примете монорепозиторий, тем лучше.
Создание монорепозиторий TypeScript с помощью NPM
Создание монорепозитория на TypeScript — непростая задача, поэтому в настоящее время на рынке представлено несколько инструментов для создания монорепозиториев, упрощающих задачу. Наиболее популярными инструментами сборки монорепозиториев являются Lerna, Nx, и Turborepo. С их помощью вы можете настроить монорепозиторий на TypeScript с помощью набора команд npm. Однако вы можете быть не в состоянии понять, что эти инструменты делают за кулисами и почему.
Единственный способ освоить монорепозитории в TypeScript — понять, как они работают. Итак, давайте узнаем, как реализовать монорепозиторий TypeScript на основе рабочих пространств npm.
Вы можете взглянуть на окончательный результат, клонировав репозиторий GitHub, который поддерживает это руководство, с помощью следующей команды:
git clone https://github.com/Tonel/typescript-monorepo
Теперь давайте создадим монорепозиторий TypeScript с нуля!
Предпосылки
Чтобы создать монорепозиторий TypeScript с рабочими пространствами npm, вам потребуется:
- Node.js >= 14
- npm >= 7
Обратите внимание, что вам нужно npm >= 7
, потому что рабочие области npm были представлены в версии 7. Мы скоро рассмотрим, зачем они вам нужны и что они из себя представляют.
Инициализирование структуры каталогов
Вот как должна выглядеть структура каталогов вашего монорепозитория:
typescript-monorepo
├── src
├── node_modules
├── ...
└── packages
В папке src
хранится приложение Node.js TypeScript, а в /packages
— общие локальные библиотеки, определенные как рабочие области npm. Обратите внимание, что во всей кодовой базе есть только одна папка node_modules
. Это означает, что у каждого пакета есть свои зависимости npm, хранящиеся в глобальной папке node_modules
.
Давайте создадим эту базовую настройку папки с помощью следующих команд:
# create the monorepo root directory
mkdir typescript-monorepo
# enter the newly created directory
cd typescript-monorepo
# creating the subdirectory
mkdir src
mkdir packages
Определение глобального файла package.json
Внутри каталога monorepo-typescript
запустите следующую команду:
npm init -y
Это инициализирует для вас файл package.json
. Обратите внимание, что флаг -y
указывает npm init
автоматически отвечать утвердительно на все вопросы, которые npm в противном случае задавал бы вам в процессе инициализации.
Теперь давайте обновим /package.json
следующим образом:
{
"name": "monorepo-typescript",
"version": "1.0.0",
"description": "A monorepo in TypeScript",
"private": true,
"workspaces": [
"packages/*"
]
}
В частности, убедитесь, что свойство workspaces
присутствует и настроено, как указано выше. Рабочие пространства Npm позволяют вам определять несколько пакетов в одном корневом пакете. В этой конфигурации каждая папка внутри /packages
с файлом package.json
считается локальным пакетом.
Когда вы запускаете npm install
в корневом каталоге, папки в packages/
становятся символическими ссылками на папку node_modules
. Например, предположим, что у вас есть локальный пакет ui
. После запуска npm install
ваш проект монорепозитория должен иметь следующую структуру папок:
typescript-monorepo
├── ...
├── node_modules
│ ├── ui -> ../packages/ui
│ └── ...
├── package.json
├── package-lock.json
└── packages
└── ui
├── ...
└── package.json
Как видите, папка пакета ui
также находится в каталоге node_modules
. В частности, папка ui
внутри node_modules
ссылается на папку ui
внутри ./packages
.
Установка основных зависимостей
Поскольку вы настраиваете монорепозиторий TypeScript, вам необходимо установить typescript
в качестве корневой зависимости с помощью следующей команды:
npm install typescript
Обратите внимание, что установленные здесь библиотеки следует рассматривать как основную зависимость проекта.
Вам также понадобится ts-node
и его типы. Установите их как зависимости для разработчиков с помощью команды npm ниже:
npm install --save-dev @types/node ts-node
ts-node
преобразует TypeScript в JavaScript и позволяет выполнять TypeScript на Node.js без предварительной компиляции. Поскольку src
содержит приложение Node.js, вам потребуется ts-node
для запуска файлов, размещенных в папке src
.
Все пакеты в вашем приложении должны следовать одним и тем же правилам линтинга и отступов. Вот почему вы должны добавить eslint
и prettier
к зависимостям вашего корневого проекта. Поскольку это кодовая база TypeScript, вам также понадобятся @typescript-eslint/eslint-parser
и @typescript-eslint/eslint-plugin
. Это парсер TypeScript и плагин для eslint соответственно.
Установите их все как зависимости для разработчиков с помощью команды npm ниже:
npm install eslint prettier @typescript-eslint/eslint-parser @typescript-eslint/eslint-plugin
Затем создайте файл конфигурации eslint. Например, вы можете инициализировать файл .eslintrc.json
следующим образом:
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
]
}
Точно так же создайте более красивый файл конфигурации. Опять же, вы можете определить файл .prettierrc.json
, как показано ниже:
{
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 120,
"semi": false,
"singleQuote": false,
"bracketSpacing": true
}
Теперь весь код в кодовой базе монорепозитория имеет одинаковый стиль и подчиняется одним и тем же правилам.
Добавление локального пакета
Теперь давайте настроим локальный пакет. Пакет @monorepo/utils
включает в себя все служебные функции, которые можно использовать во всем монорепозитории.
Сначала создайте папку utils
внутри /packages
:
mkdir utils
Инициализируйте файл package.json
внутри utils
с помощью этой команды npm:
npm init --scope @monorepo --workspace ./packages/utils -y
Флаг --scope
указывает имя области npm в имени пакета.
Вы должны использовать одно и то же имя области действия npm для всех ваших локальных пакетов. Это сделает ваш монорепозиторийnode_modules
более чистым, так как все ваши локальные пакеты будут отображаться в виде ссылок в одной и той же папке@<scope_name>
. Кроме того, это делает локальный импорт более элегантным и легко распознаваемым из глобальных библиотек npm.
Убедитесь, что package.json
содержит следующее содержимое:
{
"name": "@monorepo/utils",
"version": "1.0.0",
"description": "The package containing some utility functions",
"main": "build/index.js",
"scripts": {
"build": "tsc --build"
}
}
Здесь вы просто определяете базовый файл package.json
с пользовательским скриптом build
, который запускает tsc --build
. Если вы не знакомы с этой командой, tsc
— это компилятор TypeScript. В частности, tsc
компилирует TypeScript в JavaScript в соответствии с правилами, определенными в tsconfig.json
. Вот почему вам также нужен файл tsconfig.json
. Инициализируйте его следующим образом:
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"strict": true,
"incremental": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"rootDir": "./src",
"outDir": "./build",
"composite": true
}
}{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"strict": true,
"incremental": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"rootDir": "./src",
"outDir": "./build",
"composite": true
}
}
Это базовый шаблон tsconfig.json
. В частности, обратите внимание на последние три варианта.
Чтобы сделать пакет чище, поместите весь свой код в локальный каталог src
под ui
. С помощью параметра rootDir
вы можете определить корневой каталог пакета, в котором вы собираетесь разместить свой код. Точно так же опция outDir
обеспечивает сборку пакета в локальном каталоге ./build
внутри ui
.
Как поясняется в документации по TypeScript, установите для composite
параметра значение true
. Поскольку вы, вероятно, будете ссылаться на этот проект в других частях монорепозитория, это позволит вам ссылаться на этот пакет в другом файле tsconfig.json
. Поскольку вы собираетесь учиться, это будет полезно на следующих нескольких шагах.
Теперь определите логику пакета, создав каталог src
:
cd packages/ui
mkdir src
Эта папка содержит весь ваш код. Затем инициализируйте файл index.ts
следующим образом:
// ./packages/utils/index.ts
export function isEven(n: number): boolean {
return n % 2 === 0
}
Обратите внимание, что это всего лишь простой пример. В реальном сценарии определите все свои служебные функции в папке ./src
.
Вот как выглядит файловая структура локального пакета @monorepo/utils
:
utils
└── src
│ └── index.ts
├── package.json
└── tsconfig.json
Вы только что узнали, как определить локальный пакет для вашего монорепозитория. Повторите этот процесс столько раз, сколько вам нужно, в зависимости от количества локальных пакетов, которые вы хотите определить.
Как объяснялось ранее, npm автоматически обрабатывает любой файл package.json
в каталоге /packages
, чтобы создать связь между локальной папкой пакета и папкой пакета в node_modules
. Именно поэтому для каждого пакета требуется файл package.json
. Итак, каждый раз, когда вы определяете локальный пакет, вы должны запускать npm install
в корневой папке вашего каталога.
Проверка работоспособности локального пакета
Вы можете собрать локальный пакет, чтобы увидеть, работает ли он, с помощью следующей команды:
npm run build --workspace ./packages/utils
Обязательно запустите его в корневом каталоге монорепозитория. Эта команда выполняет сценарий build
, определенный в локальном файле package.json
пакета, указанного с помощью флага --workspace
. Не забывайте, что локальный пакет — это не что иное, как рабочее пространство npm, поэтому вам нужно использовать флаг --workspace
.
В конце процесса компиляции, если все работает как положено, вы можете найти результаты компиляции в папке .packages/utils/build
следующим образом:
Определение глобального файла tsconfig.json
Инициализируйте файл tsconfig.json
в корневом каталоге со следующим содержимым:
{
"compilerOptions": {
"incremental": true,
"target": "es2022",
"module": "commonjs",
"declaration": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"rootDir": "./src",
"outDir": "./build"
},
"files": [
],
"references": [
{
"path": "./packages/utils"
}
]
}
Благодаря ссылкам на TypeScript вы можете разделить проект TypeScript на более мелкие части. С помощью опции references
вы можете определить список пакетов, из которых состоит ваше монорепозиторий TypeScript. При запуске tsc -- build
в корневом каталоге компилятор TypeScript получает доступ ко всем пакетам, определенным в ссылках, и компилирует их один за другим по порядку.
Например, предположим, что вы также добавили пакет ui
. В этом случае параметр references
файла ./tsconfig.json
выглядит следующим образом:
"references": [
{
"path": "./packages/utils"
},
{
"path": "./packages/ui"
}
]
Добавьте новый скрипт build
в глобальный файл ./package.json
:
"scripts": {
"build": "tsc --build --verbose"
}
Флаг --verbose
, чтобы tsc
регистрировал все, что он делает в терминале.
Теперь, если вы запустите npm run build
в корневом каталоге, tsc
должен вывести:
Projects in this build:
* packages/utils/tsconfig.json
* packages/ui/tsconfig.json
* tsconfig.json
Как видите, tsc
создает проекты в порядке, указанном в поле references
.
В конце процесса компиляции для каждого локального пакета у вас будет:
- Папка
./packages/<package-name>/build
- Ссылка из
node_modules/@<scope_name>/<package-name>
на./packages/<package-name>
Использование локального пакета внутри другого локального пакета
Теперь предположим, что вы хотите использовать некоторые служебные функции из @monorepo/utils
в пакете @monorepo/ui
. Все, что вам нужно сделать, это запустить следующую команду npm в корневом каталоге:
npm install @monorepo/utils --workspace ./packages/ui
Это добавляет @monorepo/utils
в качестве зависимости в @monorepo/utils
.
Взгляните на локальный файл package.json
внутри ./packages/ui
, и вы увидите:
"dependencies": {
"@monorepo/utils": "^1.0.0"
}
Это означает, что @monorepo/utils
был корректно добавлен в качестве зависимости.
Теперь в ./packages/ui/index.ts
вы можете получить доступ к служебным функциям, предоставляемым @monorepo/utils
, следующим образом:
// ./packages/ui/index.ts
import { isEven } from "@monorepo/utils"
export function FooComponent() {
// giving a random integer number between 0 and 5
const randomNumber = Math.floor(Math.random() * 5)
console.log(`FooComponent: ${randomNumber} -> isEven: \
${isEven(randomNumber)}`)
// UI component implementation ...
}
В частности, вы можете импортировать функции из пакета @monorepo/utils
с помощью следующей строки:
import { isEven } from "@monorepo/utils"
Добавление внешнего пакета npm в локальный пакет
Для ваших локальных пакетов могут потребоваться внешние библиотеки npm. В этом случае не запускайте команду npm install
внутри папки пакета; это добавит зависимость к локальному файлу package.json
и, следовательно, создаст локальную папку node_modules
. Это нарушает основную идею о том, что монорепозиторий имеет только один node_modules
.
Вместо этого выполните эту процедуру, чтобы добавить внешний пакет npm в локальный пакет вашего монорепозитория.
Предположим, вы хотите добавить [moment](https://www.npmjs.com/package/moment)
в пакет @monorepo/ui
. Запустите следующую команду npm install
в корневой папке вашего монорепозитория:
npm install moment --workspace ./packages/ui
Это установит moment
в папку node_modules
монорепозитория и добавит следующий раздел в локальный packages.json
внутри ./packages/ui
:
"dependencies": {
// ...
"moment": "^2.29.4"
}
Вы можете убедиться, что все прошло так, как ожидалось, проверив, что во всем проекте монорепозитория есть только одна папка node_modules
.
Затем вы можете использовать moment
, как показано ниже:
// ./packages/ui/index.ts
import { isEven } from "@monorepo/utils"
import moment from "moment"
export function FooComponent() {
// giving a random integer number between 0 and 5
const randomNumber = Math.floor(Math.random() * 5)
console.log(`[${moment().toISOString()}] FooComponent: \
${randomNumber} -> isEven: ${isEven(randomNumber)}`)
// UI component implementation ...
}
В частности, вы можете импортировать moment
и использовать его в @monorepo/ui
со следующим оператором import
:
import moment from "moment"
Сборка в единое
Теперь добавьте @monorepo/utils
и @monorepo/ui
в качестве зависимостей проекта в глобальный файл ./package.json
с помощью этой команды npm:
npm install @monorepo/utils @monorepo/ui
Затем создайте файл ./src/index.ts
и заставьте его использовать функции, предоставляемые вашими локальными пакетами:
import { FooComponent } from "@monorepo/ui"
import { isEven } from "@monorepo/utils"
console.log(isEven(4))
FooComponent()
Теперь проверьте, работает ли файл Node.js ./src/index.ts
:
# building the monorepo and all its packages
npm run build
# running the compiled index.js
node src/index.js
Это печатает:
true
[2022-11-23T14:28:14.220Z] FooComponent: 4 -> isEven: true
Вы только что узнали, как настроить монорепозиторий TypeScript на основе рабочих пространств npm.
Заключение
Как вы знаете, монорепозиторий состоит из нескольких приложений, каждое из которых опирается на множество пакетов, которые могут зависеть друг от друга. Таким образом, чтобы убедиться, что вы можете правильно развернуть приложение, являющееся частью монорепозитория, важно собрать и развернуть каждый пакет в правильном порядке. Поэтому вам необходимо определить конвейер монорепозитория.
Earthly может помочь вам в этом. Это инструмент автоматизации сборки, который позволяет запускать все сборки в контейнерах. Earthly работает поверх самых популярных систем непрерывной интеграции, таких как Jenkins, CircleCI, GitHub Actions и AWS CodeBuild, и вы можете легко адаптировать его для настройки конвейера монорепозитория.