DevGang
Авторизоваться

Как настроить монорепозиторий TypeScript

В последние годы монорепозитории стали популярной темой в ИТ-сообществе. При использовании монорепозитория организация хранит все свои проекты в одном репо. Монорепозитории особенно популярны среди веб-разработчиков, поскольку большинство их проектов используют JavaScript или TypeScript и полагаются на одни и те же зависимости npm.

В этом руководстве мы рассмотрим, что такое монорепозиторий, почему и когда вам следует подумать о его внедрении, а также как настроить монорепозиторий TypeScript с помощью npm.

Monorepo

Monorepo — это подход к разработке программного обеспечения, при котором один репозиторий содержит код и активы нескольких проектов. Это глобальный проект, который содержит более мелкие проекты. Каждый из этих проектов может быть чем угодно, от отдельного приложения до повторно используемых пакетов компонентов или служебных функций. В монорепозитории эти пакеты обычно называются локальными пакетами.

Обычно монорепозиторий включает множество приложений и несколько пакетов; пакет может зависеть от других пакетов. Например, пакет ui может использовать функции, предоставляемые пакетом utils. С другой стороны, приложения обычно не зависят друг от друга.

Монорепо не следует путать с монолитным приложением! Монолит — это единый проект, компоненты которого должны быть развернуты вместе. С другой стороны, монорепозиторий состоит из нескольких независимых приложений, которые находятся в одном репозитории и совместно используют код через локальные пакеты, но могут быть развернуты самостоятельно. Таким образом, монорепозитории обеспечивают большую гибкость развертывания, чем монолиты.

Преимущества монорепозитория

Есть три конкретные причины, по которым следует рассмотреть возможность использования монорепозитория:

  • Легче стандартизировать код и инструменты между командами. Поскольку весь код хранится в одном месте, проще применять одни и те же правила для отступов и линтинга. Каждая команда использует одни и те же библиотеки линтинга кода и форматирования, которые являются частью монорепозитория.
  • Это обеспечивает лучшую видимость и совместную работу между командами. Разработчики имеют доступ ко всем проектам, что упрощает повторное использование и совместное использование кода.
  • Это облегчает организацию файлов и упрощает управление зависимостями кода. В монорепозитории есть одна версия для каждой зависимости. Это означает, что вам больше не нужно беспокоиться о несовместимости или конфликтующих версиях внешних библиотек.

Использование монорепозитория

Идеальное время для принятия монорепозитория — это когда вы начинаете проект. Таким образом, вы сможете воспользоваться всеми его преимуществами с первого дня.

Стратегия монорепозитория особенно привлекательна, если у вас большое количество проектов или вы планируете быстро масштабироваться. С монорепозиторием вы не начинаете с нуля при создании нового проекта; все, что вам нужно сделать, это добавить его в монорепозиторий, и вы сразу получите доступ к нескольким пакетам и существующей настройке CI. Монорепозитории особенно полезны, если ваши проекты основаны на тех же технологиях, поскольку это позволяет совместно использовать код. Даже такие технологические гиганты, как Google, полагаются на огромные монорепозитории.

В то же время подход монорепозитория имеет свои подводные камни.

Во-первых, не забывайте, что настроить конвейер сборки для ваших монорепозиториев может быть непросто. Это особенно верно, если ваш монорепозиторий состоит из нескольких приложений, которые следует развертывать в определенном порядке. Если ваш конвейер не настроен идеально, ваши развертывания могут привести к простою или сбою.

Во-вторых, координация управления версиями всех продуктов, услуг и библиотек, входящих в монорепозиторий, — сложный процесс. Если вы принимаете монорепозиторий, вам нужно уделять еще больше внимания каждому коммиту. Кроме того, каждый член команды разработчиков должен хорошо разбираться в Git или аналогичной системе контроля версий.

Поэтому, если ваши команды используют очень разные технологии, полирепозиторий может быть лучшим подходом. Но имейте в виду, что переход от полирепозитория к монорепозиторию может быть обременительным, сложным и трудоемким. Хотя эта миграция иногда может стоить усилий, чем раньше вы примете монорепозиторий, тем лучше.

Создание монорепозиторий TypeScript с помощью NPM

Создание монорепозитория на TypeScript — непростая задача, поэтому в настоящее время на рынке представлено несколько инструментов для создания монорепозиториев, упрощающих задачу. Наиболее популярными инструментами сборки монорепозиториев являются LernaNx, и Turborepo. С их помощью вы можете настроить монорепозиторий на TypeScript с помощью набора команд npm. Однако вы можете быть не в состоянии понять, что эти инструменты делают за кулисами и почему.

Единственный способ освоить монорепозитории в TypeScript — понять, как они работают. Итак, давайте узнаем, как реализовать монорепозиторий TypeScript на основе рабочих пространств npm.

Вы можете взглянуть на окончательный результат, клонировав репозиторий GitHub, который поддерживает это руководство, с помощью следующей команды:

git clone https://github.com/Tonel/typescript-monorepo

Теперь давайте создадим монорепозиторий TypeScript с нуля!

Предпосылки

Чтобы создать монорепозиторий TypeScript с рабочими пространствами npm, вам потребуется:

Обратите внимание, что вам нужно 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 следующим образом:

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 следующим образом:

eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ]
}

Точно так же создайте более красивый файл конфигурации. Опять же, вы можете определить файл .prettierrc.json, как показано ниже:

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 содержит следующее содержимое:

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. Инициализируйте его следующим образом:

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 следующим образом:

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 следующим образом:

Папка packages/utils/build
Папка packages/utils/build

Определение глобального файла tsconfig.json

Инициализируйте файл 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 выглядит следующим образом:

tsconfig.json
"references": [
  {
    "path": "./packages/utils"
  },
  {
    "path": "./packages/ui"
  }
]

Добавьте новый скрипт build в глобальный файл ./package.json:

package.json
"scripts": {
  "build": "tsc --build --verbose"
}

Флаг --verbose, чтобы tsc регистрировал все, что он делает в терминале.

Теперь, если вы запустите npm run build в корневом каталоге, tsc должен вывести:

Output
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, и вы увидите:

package.json
"dependencies": {
  "@monorepo/utils": "^1.0.0"
}

Это означает, что @monorepo/utils был корректно добавлен в качестве зависимости.

Теперь в ./packages/ui/index.ts вы можете получить доступ к служебным функциям, предоставляемым @monorepo/utils, следующим образом:

index.ts
// ./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:

package.json
"dependencies": {
  // ...
  "moment": "^2.29.4"
}

Вы можете убедиться, что все прошло так, как ожидалось, проверив, что во всем проекте монорепозитория есть только одна папка node_modules.

Затем вы можете использовать moment, как показано ниже:

index.ts
// ./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:

package.json
import moment from "moment"

Сборка в единое

Теперь добавьте @monorepo/utils и @monorepo/ui в качестве зависимостей проекта в глобальный файл ./package.json с помощью этой команды npm:

npm install @monorepo/utils @monorepo/ui

Затем создайте файл ./src/index.ts и заставьте его использовать функции, предоставляемые вашими локальными пакетами:

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 работает поверх самых популярных систем непрерывной интеграции, таких как JenkinsCircleCI, GitHub Actions и AWS CodeBuild, и вы можете легко адаптировать его для настройки конвейера монорепозитория.

#TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу