Руководство по созданию монорепозитория машинописного текста с использованием рабочих пространств npm и Nx.
В этой статье объясняется, как вы можете иметь несколько проектов typescript в монорепозитории. Я пишу эту статью, потому что мне надоели простые примеры, такие как пакет even
или odd
, которые даже близко не представляют реальный вариант использования.
Готовую реализацию того, о чем говорится в этой статье, можно найти здесь:
Что такое монорепо
Монорепозиторий — это набор релевантного кода, сгруппированного вместе. Как правило, он включает в себя несколько приложений и библиотек, от которых зависят приложения. Монорепо отслеживается в одном репозитории, отсюда и название.
Существует множество причин для использования монорепозитория, в том числе упрощение запуска тестов, операций сборки и создания библиотек, чтобы сделать код повторно используемым в нескольких приложениях, и одна из моих личных любимых причин, позволяющая командам фронтенда и бэкенда перестать вести себя так, как будто они находятся на двух разных планетах.
Что мы здесь делаем?
Мы создаем монорепозиторий с тремя пакетами typescript, два из которых являются приложениями, а один — библиотекой, импортируемой обоими этими приложениями.
Мы настроим все это на машинописном языке и сделаем так, чтобы при внесении изменений в импортированную библиотеку автоматически запускалась пересборка приложений, которые ее импортируют.
Пример проблемы
Допустим, у нас есть простое старомодное приложение с интерфейсом и сервером, и оба они зависят от одной и той же библиотеки. Это может быть что-то, используемое для проверки, общие типы доменов или что-то еще.
Если бы серверная часть и внешний интерфейс были в разных репозиториях, нам пришлось бы либо дублировать код для них обоих, что все и их бабушки знают не делать в этот момент, либо нам пришлось бы экспортировать пакет в npm и добавить его как в серверную часть, так и во внешний интерфейс в качестве зависимости, или что-то в этом роде.
Это звучит немного запутанно, и, возможно, вы не хотите публиковать свой пакет в npm, потому что он содержит конфиденциальные данные домена, которые определяют ту самую проблему, которую пытается решить ваше программное обеспечение.
Решение
Поскольку и интерфейс, и сервер написаны на машинописном языке, они могут просто поделиться кодом. Это может быть сделано с помощью библиотеки, которую импортируют как внешний, так и внутренний интерфейс.
Таким образом, обновление библиотеки в одном месте мгновенно вносит изменения как в интерфейс, так и в серверную часть, и любые проблемы обнаруживаются, например, во время конвейера сборки.
Создание монорепозитория с помощью Npm Workspaces и Nx
Используя рабочие пространства npm (представленные в версии 7) и Nx, мы создадим монорепозиторий с двумя приложениями, интерфейсом и сервером, а также общей библиотекой, назначение которой мы определим чуть позже.
Обратите внимание, что при выполнении большинства будущих команд мы будем использовать флаг -w
рядом с именем пакета, на который мы хотим повлиять:
npm run -w package-name-here command
Это позволит нам запускать все наши команды из корня монорепозитория без необходимости перехода к какому-либо из пакетов.
1. Создание монорепозитория
Во-первых, давайте создадим новый монорепозиторий под названием HelloWorld
. Самый простой способ сделать это — использовать следующую команду npx:
npx create-nx-workspace@latest HelloWorld --preset=npm
Если вам будет предложено следующее:
Need to install the following packages: [email protected]
Затем введите y
и нажмите Enter.
Вам также может быть предложено следующее:
Enable distributed caching to make your CI faster … Yes I want faster builds No
Это выходит за рамки данной статьи, поэтому вам следует ответить «нет» и двигаться дальше.
Вы получите следующее:
Внутри package.json
вы увидите раздел под названием workspaces
:
Вот как npm узнает, какие пакеты у вас есть в вашем монорепозитории. Строка packages/*
сообщает, что все в папке packages
является пакетом монорепозитория.
Мы добавим сюда еще одну строку, чтобы наши библиотеки также могли быть обнаружены:
"workspaces": [ "packages/*", "libs/*" ]
Затем создайте пустую папку в корне вашего проекта с именем libs
:
Мы добавим библиотеку в эту папку в следующем разделе.
Далее давайте добавим в монорепозиторий два приложения: серверное приложение, использующее NestJS, и внешнее приложение, использующее Angular.
2. Создание бэкенда
Приложение NestJS можно создать и добавить, выполнив следующую команду:
npx nx nest new packages/backend
После того, как это завершится, Nest создаст новый репозиторий git. Обязательно удалите его. Это можно сделать, удалив папку .git
в папке packages/backend
, что можно сделать с помощью следующей команды:
rm -rf packages/backend/.git
Чтобы запустить бэкенд, вам не нужно переходить к его пакету, вместо этого вы можете сделать следующее:
npm run -w backend start:dev
Где часть -w backend
указывает npm запустить команду start:dev
в рабочей области backend
. Имя backend
взято из поля name
в файле package.json внутреннего пакета.
На данный момент бэкенд готов.
2.1. Создание интерфейса
Чтобы создать интерфейс с помощью Angular, сначала установите cli (глобально):
npm install -g @angular/cli
затем выполните следующую команду
cd packages ng new frontend
Вы можете просто спамить через подсказки, так как они не имеют никакого отношения к этой статье.
Вы можете протестировать интерфейсный пакет, используя следующее:
npm run -w frontend start
3. Создание общей библиотеки
Сложность создания монорепозитория с использованием машинописного текста связана с тем, что нам нужно сначала преобразовать код в javascript, сохраняя при этом такие вещи, как типы, исходные карты и т. д., доступными для всех, кто импортирует библиотеку.
Такие фреймворки, как NestJS и Angular, сами компилируются для нас, и мы все равно никуда их не импортируем. А вот с библиотекой другая история.
Короче говоря, чтобы создать общую библиотеку, нам нужно подумать о нескольких вещах:
- Транспиляция из Typescript в Javascript
- Сохранение типов без изменений
- Карты исходного кода для обеспечения легкого доступа к исходному коду с помощью инструментов IDE, таких как «Перейти к ссылкам».
- Автоматическое перестроение при внесении изменений
- Потому что любое место, где эта библиотека была импортирована, будет перестроена, если у нее есть собственный режим просмотра.
В качестве примера мы заставим библиотеку представлять собой очень простой регистратор, который окружает заданное сообщение кучей смайликов и записывает его в консоль. Итак, приступим!
Сначала мы создадим библиотеку с помощью команды npm. Мы сделаем это с помощью следующей команды:
npm init -w ./libs/fancy-logger -y
Это создаст файл package.json
под libs/fancy-logger
со (более или менее) следующим содержимым:
Чтобы позволить нам использовать машинописный текст в этом пакете, нам нужно установить машинописный текст и добавить пару записей в скрипты в этом файле package.json
.
Мы начнем с установки машинописного текста с помощью следующей команды:
npm i -w fancy-logger --dev typescript
Напоминание: убедитесь, что вы запускаете эти команды из корня монорепозитория. Вам не нужно переходить к вашему пакету в терминале, так как npm предоставляет способ нацеливания на определенный пакет при выполнении команды.
Установив typescript, замените раздел scripts в package.json fancy-logger
.
"scripts": { "build": "tsc", "build:watch": "tsc -w" },
Первый скрипт, build
, строит библиотеку один раз, а build:watch
строит библиотеку один раз, затем следит за любыми изменениями и перестраивает библиотеку.
Еще одна вещь — добавить файл tsconfig.json
. Вы можете скопировать и вставить следующее в новый файл под libs/fancy-logger/tsconfig.json
:
{ "compilerOptions": { "moduleResolution": "nodenext", "module": "commonjs", "baseUrl": "./", "outDir": "./dist", "lib": ["es6"], "declaration": true, "declarationMap": true, "target": "ES2021", "sourceMap": true } }
Вот разбивка того, что происходит в файле:
moduleResolution: "nodenext"
согласно рекомендациям typescript docsmodule: "commonjs"
устанавливает, какую систему модулей будет использовать сгенерированный javascript.baseUrl: "./"
, чтобы разрешить абсолютный импорт, напримерsrc/../..
(документы)outDir: "./dist"
— это место для встроенной библиотеки.lib: ["es6"]
, чтобы включить объявления типов для функций es6, таких какPromise
,Set
,Map
и т. д.declaration: true
для создания.d.ts
файлов и добавления их в сборкуdeclarationMap: true
, чтобы среда IDE могла найти результирующие.d.ts
файлов (используя такие вещи, как «Перейти к ссылке»)target: "ES2021"
устанавливает целевой синтаксис для сгенерированного javascriptsourceMap: true
, чтобы отладчики могли найти исполняемый в данный момент javascript.
Вы можете изменить lib
, declaration
, declarationMap
, target
и sourceMap
без особых хлопот.
module
и moduleResolution
изменяют способ генерации javascript в отношении импорта как внутреннего в вашу библиотеку, так и того, как импортируется сама библиотека. Эти свойства влияют друг на друга и должны быть изменены при ссылке на документы. nodenext
обычно является хорошим выбором для проектов, использующих современную версию node.
Кроме того, будьте внимательны при изменении baseUrl
или outDir
, так как они требуют внесения изменений в другие файлы, как мы вскоре увидим.
На этом этапе я также рекомендую добавить .gitignore
либо вверху вашего монорепозитория (который, вероятно, уже существует, поэтому просто добавьте его в конец), либо в самой этой библиотеке со следующим содержимым:
dist node_modules
Затем мы должны установить пару записей в package.json
библиотеки, чтобы убедиться, что она обнаружена и работает правильно при импорте.
Записи, которые мы собираемся установить, это main
и types
, первая из которых устанавливает основной файл javascript, представляющий запись для библиотеки и экспортирующий все из нее.
types
указывает, где можно найти сгенерированный *.d.ts
. Мы укажем его как шаблон глобуса, чтобы перехватывать все файлы типов в проекте.
Вот как будут выглядеть записи main
и types
(остальные записи опущены):
{ "main": "dist/index.js", "types": "dist/*/**.d.ts" }
Обязательно замените все существующие main
или types
, если они там есть.
Теперь мы можем приступить к написанию кода. Начните с создания файла index.ts
под libs/fancy-logger/
, а затем создайте рядом с ним папку с именем src
:
index.ts
будет точкой входа в библиотеку, экспортируя все, что мы хотим из библиотеки.
Давайте добавим базовый класс, задача которого состоит только в том, чтобы записывать сообщения с красивыми смайликами. Мы добавим это под libs/fancy-logger/src/fancy-logger.ts
:
export class FancyLogger { static log(message: string) { console.log(`🚀🚀🚀 ${message} 🚀🚀🚀`); } }
Затем экспортируйте его из соседнего файла index.ts
:
export * from "./src/fancy-logger";
Вот основная организация файлов на данный момент:
Давайте соберем пакет и посмотрим, все ли идет по плану. Выполните следующую команду:
npm run -w fancy-logger build
Сделав это, вы увидите новую папку dist
в библиотеке со следующим содержимым:
Вот, построенная библиотека! Теперь мы можем импортировать его в наш интерфейс и сервер и сделать с ним небольшой пример. Обратите внимание, что содержимое папки dist
может быть другим, если вы изменили свойства module
или moduleResolution
в файле tsconfig.json
.
Чтобы импортировать пакет, добавьте его в массив зависимостей файлов package.json
оба бэкенда и внешнего интерфейса следующим образом:
"dependencies": { // ... "fancy-logger": "*" }
После добавления его как для интерфейса, так и для сервера, запустите npm install
, чтобы npm связал и установил пакеты для нас. Это то, что нам нужно сделать только один раз, когда мы впервые настраиваем библиотеку.
Теперь, если вы проверите папку node_modules
в верхней части монорепозитория, вы увидите там пакет fancy-logger
:
Давайте используем пакет во внешнем и внутреннем интерфейсе, чтобы регистрировать сообщение при первом запуске. Для серверной части мы можем сделать это в файле main.ts
под packages/backend/src
:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; // Importing our library 🚀 import { FancyLogger } from 'fancy-logger'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); // Using the library 🚀 FancyLogger.log('Backend has started'); } bootstrap();
То же самое сделаем для внешнего интерфейса в файле app.component.ts
под packages/frontend/src/app/
:
import { Component } from "@angular/core"; // Importing our library 🚀 import { FancyLogger } from "fancy-logger"; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"], }) export class AppComponent { title = "frontend"; constructor() { // Using the library 🚀 FancyLogger.log("Frontend is up and running"); } }
Обратите внимание, что вывод для внешнего интерфейса будет в консоли браузера, а не в терминале.
Теперь давайте запустим серверную часть, внешний интерфейс и библиотеку в режиме просмотра и посмотрим, что происходит.
Команды:
npm run -w backend start:dev npm run -w frontend start npm run -w fancy-logger build:watch
Вы можете запустить каждый из них в отдельном терминале/панели.
Теперь, на момент истины, вот вывод:
С бэкендом слева и интерфейсом справа вы можете ясно видеть вывод нашего регистратора.
Теперь, поскольку наша библиотека работает в режиме просмотра, изменение окружающих смайликов должно привести к мгновенному изменению журналов:
Вы можете найти репозиторий со всем, что мы сделали в этой статье, здесь:
Предостережения
- Мы не используем сборщик. Есть еще много возможностей для оптимизации, такой как встряхивание деревьев и более быстрая компиляция.
- Как вы могли заметить в терминале при запуске Angular, интерфейс не любит
commonjs
модулей. - Мы не настроили
nx.json
для обработки наших операций сборки. Я рекомендую вам взглянуть на документы для этого.
Бонус: Скрипт для мгновенного создания библиотеки
Слушай, я понимаю. Мы все ленивы. Ни у кого нет времени повторять всю вышеописанную работу при создании новой библиотеки, поэтому я создал скрипт, который создаст ее для вас! Вы можете найти это здесь:
Заключение
Используя рабочие области npm и nx, мы создали монорепозиторий машинописных текстов с двумя приложениями и библиотекой, где два приложения импортируют и используют библиотеку.
Мы создали библиотеку typescript и настроили ее tsconfig для генерации javascript, который можно импортировать в наши приложения, с режимом наблюдения для пересборки библиотеки при изменениях, а также для пересборки импортирующих приложений.