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

Практический урок по написанию собственного плагина. Реализуем generator и executor. generator - при запуске которого у нас будет обновлять корневой файл в проекте projects.json, в которой будем вносить все существующие приложения.

Получившийся результат (проект) можно посмотреть здесь.

А executor будет брать этот файл, читать и выводить в консоль содержимое этого файла.

Пример достаточно простой и не является рекомендацией к его применению на реальных проектах, но дает понять как работают generator и executor.

А в конце статьи расскажу про реальные кейсы.

Поговорим о generator

generator чаще всего вызывается через терминал. Создание собственного generator позволяет упростить повторные операции создания файлов и(-или) папок, как в корне монорепы, так и создавать приложения и библиотеки в ней. Также можно вносить изменения уже в существующие файлы и папки. На сайте nx.dev проекта также можете ознакомится с простым примером создания generator. А мы в свою очередь реализуем собственный (листайте ниже).

Например, у плагина @nx/react есть generator создания библиотеки:

nx g @nx/react:lib my-new-lib

или например для нового микрофронта:

nx g @nx/react:app my-new-app

или создания нового реакт компонента

nx g @nx/react:component my-new-component --project=my-new-app

Можно в принципе зайти в исходники его и взять какие-нибудь решения для своего решения.

Поговорим о executor

Позволяет производить различные манипуляции с приложением, библиотекой. Например: запускать тесты, проверка качества кода, сборка, запуск в режиме разработки и т.д. Под свои нужды можно создать что угодно. Чуть больше информации то, для чего нужен executor и как с ним работать тут. Как и для generator, для executor на сайте проекта также есть пример.

Главное, что executor необходимо указывать в файле project.json приложения или библиотеки. Созданный executor помещается в свойство executor, в options передаются параметры, которые принимает executor. Названия, по которым потом мы будем взаимодействовать с приложением или библиотекой, указываются в targets, например: build, test и т.д.

Итак, приступим к практике.

За основу возьмем шаблон приложения для react.

Подготовка проекта

Начнем с создания приложения:

npx create-nx-workspace@latest myorg
  • Выбираем react

  • На вопрос про framework, отвечаем N.

  • Затем выбираем integrated monorepo.

  • Application name оставляем тот же.

  • Сборщик выбираем webpack.

  • По стилизации оставляем css.

  • На вопрос "Enable distributed caching to make your CI faster" отвечаем N, т.к. не планируем использовать распределенных кэш разработчиков nx.

Микрофронтенды (приложения) будут находится в папке apps, а библиотеки в libs.

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

Если у вас не установлен nx глобально, то можем это сделать так:

npm install -g nx

Запустить приложение в режиме разработке можно так nx serve myorg

Где myorg - название проекта в папке apps, а serve это target, который указан в файле project.json (apps/myorg)

запускаем приложение в режиме разработки
запускаем приложение в режиме разработки

Более детально почитать по тому, что можно делать в текущем окружении можно тут. Там описано как добавлять к монорепе еще микрофронты и библиотеки.

Для начала создадим новый микрофронтенд:

nx g @nx/react:app geek

После выполнения команды, в папке apps появится еще приложение(микрофронтенд) - geek и папка для end-to-end тестов. В итоге, в папке apps у нас два приложения.

А также создадим библиотеку:

nx g @nx/react:lib list

На все вопросы отвечаем нет.

В файле tsconfig.base.json можно заметить, что добавилась строка @myorgg/list": ["libs/list/src/index.ts"], ее добавляет generator плагина react (@nx/react). Добавляется для того, чтобы мы могли в коде использовать импорт библиотеки через публичный интерфейс (public api).

Теперь можно приступить к разработке плагина.

Для того чтобы воспользоваться generator плагина, необходимо установить зависимосить:

npm install -D @nx/plugin@latest

Создадим свой плагин:

nx g @nx/plugin:plugin my-plugin

Разработка собственного generator

nx generate @nx/plugin:generator my-generator --project=my-plugin

После выполнения команды в папке libs/my-plugin/src появится папка generator.

Перейдем в нее и найдем generator/my-generator.

В файле schema.json указываются правила к полям, которые будут проверятся при запуске executor или generator.

Непосредственно nx запускает файл generator.ts.

У generator также может быть папка files, эти файлы будут копироваться в место назначения.

Обновим файл schema.json:

{
  "$schema": "http://json-schema.org/schema",
  "$id": "MyGenerator",
  "title": "",
  "type": "object",
  "properties": {}
}

Файл schema.d.ts можем удалить, т.к. у нас нет входных параметров. Также удалим файл generator.spec.ts, нас не интересуют тесты в данной статье. Папку files тоже удалим, т.к. мы ничего не копируем, за исключением создания файла apps.json в корне проекта.

Откроем файл generator.ts и заменим содержимое на это:

import {
  formatFiles,
  writeJson,
  Tree,
  getProjects
} from '@nx/devkit';

export async function myGeneratorGenerator(
  tree: Tree
) {
  const projects = [];
  for (const project of getProjects(tree)) {
    if (project[1].projectType === 'application') projects.push(project[0]);
  }
  writeJson(tree, 'apps.json', projects);
  await formatFiles(tree);
}

export default myGeneratorGenerator;

Проверить работу generator можем так:

nx generate @myorg/my-plugin:my-generator

Теперь в корне репозитория создается файл apps.json с таким содержимым:

["geek", "geek-e2e", "myorg", "myorg-e2e"]. В условие можно еще указать, чтобы имена проектов -e2e не попадали в файл.

Разработка собственного executor

nx generate @nx/plugin:executor my-executor --project=my-plugin

Обновим файл project.json библиотеки libs/list:

{
  "name": "list",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "libs/list/src",
  "projectType": "library",
  "tags": [],
  "targets": {
    "lint": {
      "executor": "@nx/linter:eslint",
      "outputs": ["{options.outputFile}"],
      "options": {
        "lintFilePatterns": ["libs/list/**/*.{ts,tsx,js,jsx}"]
      }
    },
    "readApps": {
      "executor": "@myorg/my-plugin:my-executor"
    }
  }
}

Мы добавили строку generate: {executor: @myorg/my-executor"}, это значит, что мы можем теперь запустить данный executor так:

nx readApps list

Где generate это имя targets, а list - имя проекта. Например мы не сможем такую же команду использовать для приложения geek или myorg, необходимо явно указать в targets файла project.json.

Внес правки в файл executor.ts

import { readJsonFile } from '@nx/devkit';
import { MyExecutorExecutorSchema } from './schema';

export default async function runExecutor(options: MyExecutorExecutorSchema) {
  console.log(readJsonFile('apps.json'));
  return {
    success: true,
  };
}

sucess необходимо возвращать, чтобы дать понять nx, что успешно или не успешно выполнилась команда.

После выполнения команды выведется в консоль список приложений из файла apps.json.

Выводы

В примерах выше я постарался как можно проще написать инструкции по взаимодействую с собственным плагином. nx/devkit позволяет использовать граф зависимостей tree в generator, и извлекать полезные данные, не реализую самостоятельно обход по директориям и чтения файлов проектов. А executor в свою очередь кроме options, вторым аргументом принимает executorContext, который содержит множество полезных свойств.

nx это всего лишь инструмент для того, чтобы определенные "сценарии" в проекте упростить, стандартизировать. Но это не значит что вам данный инструмент подойдет. Прежде чем его внедрять, необходимо понимание, что ваших компетенций достаточно чтобы адаптировать этот инструмент для вашего проекта, а также ВРЕМЯ.

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

Если возникнут вопросы или трудности, пишите в комментарии, постараюсь ответить.

Реальные кейсы

Начну с того, что у нас проект уже существовал. И необходимо было переехать с минимальными трудностями для разработчиков, чтобы они не заметили переезда. А в конечном итоге упростить их работу. Этих целей удалось достичь.

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

Поэтому решили пойти по пути меньшего сопротивления и большей эффективности в нашем случае.

Но на yarn мы все равно перешли, но только в роли пакетного менеджера. Но учитываем, что pnp у yarn по умолчанию, необходимо создать файл .yarnrc в проекте и указать nodeLinker: node-modules, чтобы nx взаимодействовал с пакетами корректно.

Переход на yarn увеличил скорость установки пакетов в разы. Раньше разработчик ждал по 10-15 минут при первоначальной установки пакетов. Теперь это может занимать 1-2 минуты.

Вообще чем nx понравился, так это графом зависимостей, что можно упростить себе жизнь при развертывании приложений. Допустим, у нас изменилась библиотека, а эта библиотека используется в нескольких микрофронтах. Благодаря команде nx affected, у нас есть возможность запустить множественную сборку микрофронтов, которые зависят от данной библиотеки.

А теперь о самом плагине в проекте:

  1. реализовали generator для микрофронтедов, библиотек и для создания динамического файла env проекта (берутся переменные окружения и подставляются в результирующий файл, который в свою очередь загружается клиентом и используется микрофронтами).

  2. реализовали executor для webpack. Стандартное @nx/webpack решение не подошло по двум причинам:

    1. часть плагинов и настройки жестко забиты и их нельзя убрать без костылей, обязательно требует чтобы некоторые файлы в проекте присутствовали. У нас например микрофронты лишены своего файла webpack.config.js, executor использует один общий и подставляет определенные настройки под определенные сценарии.

    2. хотелось бы иметь один общий файл для микрофронтов, где будет хранится название микрофронта, занимаемый его порт и название окружения. Это позволяет создавать новые микрофронты с произвольными портами проще, а разработчику сразу понять на каком порту у него будет определенный микрофронтенд и какие окружения он поддерживает.

Комментарии (2)


  1. SWATOPLUS
    06.08.2023 23:37

    Почему такие вещи как создание шаблонного кода или же выполнение команд должны реализовываться как nx-плагины?

    Приведенные вами примеры, можно прекрасно реализовать как скрипты на ts-node или deno, тем самым снизить зависимость от nx. Потому что при обновлении версии nx или же отказе от него все это может сломаться и его долго и больно надо чинить. А если это скрипт на deno то достаточно просто скопировать ts-файл в другой проект и даже не надо будет устанавливать пакеты, так как можно использовать импорты с url на конкретную версию.


    1. ko22012 Автор
      06.08.2023 23:37

      nx предлагает свои подходы в разработке. Нет смысла брать nx, если у тебя нет микрофронтов. Он просто упрощает взаимодействие, предлагает общие подходы и готовые плагины.

      А тут приведенные примеры для того чтобы просто показать как писать для nx. я с вами согласен, что подобный пример лучше реализовывать на простых инструментах.

      Мы выбрали nx для того чтобы работать с микрофронтами проще, чтобы процессы ci/cd работали из коробки. А свое плагины и генераторы в проектах это уже второстепенно. Зато они позволяют скрыть реализацию и настроить один раз окружение webpack и в каждом микрофронте не повторяться, просто ссылаться на общий executor. Или вызывать общий generator для всех. На проектах где используется yarn, придется самим писать очень много кода для того, чтобы были подобные плагины, которые есть в nx.