Nx Generator: имба или не стоит разбираться?
Всем привет, меня зовут Дима, я angular-разработчик из департамента среднего и малого бизнеса в Тинькофф. Недавно мой коллега рассказал, почему мы выбрали Nx, а я расскажу про самый мощный инструмент Nx — Nx Generator.
Покажу, что нужно установить для запуска генераторов, и объясню, как писать собственные генераторы и создавать с ними шаблонные файлы. А еще рассмотрим генераторы из коробки от Nx. Предлагаю начинать.
Шаблонные генераторы
Nx Generator — инструмент от Nx, который отвечает за шаблонную генерацию кода в Nx workspace
Начнем с создания нового Nx-пространства. Если у вас уже есть Nx-пространство, то все последующие зависимости устанавливайте той версии, которой у вас @nrwl/cli.
npx create-nx-workspace@latest
Теперь у нас есть Nx-пространство, но пока без нужных зависимостей. Установим еще в зависимости проекта ангулярные шаблоны:
npm i @nrwl/angular@latest --save-dev
Этот пакет предоставляет набор шаблонных генераторов, благодаря которым можно создать наше приложение:
nx generate @nrwl/angular:app main-app
Пока все выглядит как магия, но позже мы разберемся, что происходит в написанных командах. Сократим команду generate и будем писать ее как g. Создадим библиотеку и компонент в ней:
nx g @nrwl/angular:lib library-name
nx g @nrwl/angular:component component-name --project=library
Несколькими командами мы создали много шаблонного кода. Полный список возможностей того, что умеют ангуляровские шаблонные генераторы, можно найти на гитхабе Nx. Получается, что из коробки есть необходимый минимум для комфортной разработки.
Workspace-генераторы
Что делать, когда нужно генерировать преднастроенные линтеры в библиотеках или шаблон тестов в компоненте, чтобы все использовали одну и ту же структуру кода? Для этого существуют кастомные Nx-генераторы. Давайте разберемся, как они работают, как их создавать и генерировать определенный набор файлов.
Начнем с устаревшего способа создания Nx-генераторов через @nrwl/workspace. В Nx-проектах версией ниже 13.10 встречаются такие генераторы.
Установим зависимость:
npm install @nrwl/workspace@latest --save-dev
Создадим кастомный генератор:
nx g @nrwl/workspace:workspace-generator lib
Запускаем:
nx workspace-generator lib library-name
Раньше таким способом происходила генерация, изменение и создание новых файлов в проекте, но с версии Nx 15.9.3 workspace-generators больше не поддерживаются и вместо создания генератора попросят установить @nrwl/nx-plugin.
Плагины
Плагин — набор инструментов, предоставляемых Nx, функционально связанных между собой и направленных на решение одной конкретной задачи. Они содержат executors, generators, migrations и еще ряд полезных вещей.
Чтобы в Nx workspace создавать собственные локальные плагины, нужно подключить зависимости от @nrwl/nx-plugin:
npm i @nrwl/nx-plugin@latest --save-dev
Важно, что @nrwl/nx-plugin корректно работает только с версии 13.10, так что если в проекте версия Nx меньше — стоит обновиться.
Выше у нас тоже был плагин — @nrwl/angular, и все инструменты в этом плагине направлены на работу с angular приложением.
Создадим локальный плагин custom-plugin для работы с Nx-проектом. Мы сможем изменять его и создавать в нем собственные генераторы:
nx g @nrwl/nx-plugin:plugin custom-plugin
В результате создается библиотека, в которой есть дефолтный генератор с прописанной шаблонной генерацией файлов.
Попробуем запустить шаблонный генератор. Если в nx.json выставлен npmScope нашего приложения app-name, то команда, запускающая генератор, будет напоминать создание библиотеки с помощью angular-плагина:
nx g @app-name/custom-plugin:custom-plugin customLibrary
В данном случае app-name — это префикс нашего angular-приложения, custom-plugin — имя локального плагина, custom-plugin — имя дефолтного генератора, который мы хотим запустить, а customLibrary — название библиотеки, которую хотим создать. Имя плагина и имя генератора совпадают, такова шаблонная генерация локального плагина.
В @nrwl/angular мы также могли указать после двоеточия, что именно планируем создавать — app, component, module и тому подобное. Так, мы выбираем генератор, который хотим использовать для создания той или иной сущности.
Можем попытаться создать, например, ангуляровский сервис через @nrwl/angular, но его нет в списке генераторов этого плагина. Это происходит потому, что если нет подходящего генератора, то плагин в своей реализации пытается найти соответствующий схематик в библиотеке @angular/schematics. Так что существует обратная совместимость с этой библиотекой.
Создание генератора
Создадим новый генератор и разберемся, что в нем находится. Для создания локального генератора существует свой генератор из @nrwl/nx-plugin. Воспользуемся им:
nx g @nrwl/nx-plugin:generator awesomeComponent --project=custom-plugin
В проекте может быть несколько локальных плагинов, поэтому нужно указать через project, в каком конкретно создается новый генератор. Чтобы сократить команду генерации, можно больше не писать префикс плагина, в котором лежит нужный нам генератор. Даже если названия генераторов пересекаются с названиями встроенных генераторов, то Nx предоставляет выбор между ними в консоли.
Теперь можно запустить локальный генератор и убедиться, что он что-то генерирует:
nx g awesomeComponent something
В итоге получается вполне удобная и простая в использовании команда. При разработке собственного генератора часто хочется посмотреть, какие файлы создаются. Чтобы постоянно не удалять неправильно сгенерированный код из файловой системы, можно использовать параметр --dry-run.
С его помощью команда отобразит в консоли то, что будет создано, но никак не поменяет файловую систему. Используем команду с этим флагом:
nx g awesomeComponent newComponent --dry-run
> NX Generating @app-name/custom-plugin:awesomeComponent
CREATE libs/new-component/project.json
CREATE libs/new-component/src/index.ts
NOTE: The "dryRun" flag means no changes were made.
Никаких изменений нет, но в консоли видно, что локальный генератор хотел создать.
Структура генератора
Рассмотрим файловую структуру локального плагина и генератора в нем:
Когда вызывается команда запуска генератора, сначала @nrwl/cli пытается прочитать package.json нашего плагина. Если @nrwl/cli его не находит, то выкидывает ошибку. В самом package.json находится путь до generators.json:
{
"name": "@app-name/custom-plugin",
"version": "0.0.1",
"main": "src/index.js",
"generators": "./generators.json",
"executors": "./executors.json"
}
В generators.json объявлены все существующие генераторы плагина:
{
"$schema": "http://json-schema.org/schema",
"name": "custom-plugin",
"version": "0.0.1",
"generators": {
"awesomeComponent": {
"factory": "./src/generators/awesome-component/generator",
"schema": "./src/generators/awesome-component/schema.json",
"description": "awesomeComponent generator"
}
}
}
По имени генератора, указанного при запуске, берется factory — функция, которая запускает манипуляции с файловой системой, и schema — шаблон для принимаемых параметров, которые мы указываем в качестве аргументов в консоли и с которыми работает функция генерации.
Schema — это файл, который связывает аргументы, передаваемые в консоль, с параметрами и которые мы в дальнейшем можем использовать в функции генерации кода.
Schema.json — конфиг в локальном генераторе, описывающий принимаемые командой запуска генератора аргументы из консоли. Рассмотрим его структуру файла:
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "AwesomeComponent",
"title": "",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"tags": {
"type": "string",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed"
}
},
"required": ["name"]
}
В поле properties прописаны объекты, указывающие на то, какие аргументы мы можем передать в консоль, чтобы в дальнейшем их использовать — обязательное имя, теги проекта и директория, куда помещать генерируемые файлы:
nx g awesomeComponent something --directory=components/component --tags=wow
Имя прописано в поле required и берется из командной строки как дефолтный параметр. Для тех, кто использует Typescript, необходим интерфейс, описывающий передаваемые в консоли аргументы. За это отвечает файл schema.d.ts:
export interface AwesomeComponentGeneratorSchema {
name: string;
tags?: string;
directory?: string;
}
Поля tags и directory опциональные, не являются обязательными в schema.json и могут не прийти.
Теперь можно перейти к самому важному файлу, который и производит модификацию файлового дерева, — generator.ts:
export default async function (tree: Tree, options: AwesomeComponentGeneratorSchema) {
const normalizedOptions = normalizeOptions(tree, options);
addProjectConfiguration(
tree,
normalizedOptions.projectName,
{
root: normalizedOptions.projectRoot,
projectType: 'library',
sourceRoot: `${normalizedOptions.projectRoot}/src`,
targets: {
build: {
executor: "@app/plugin:build",
},
},
tags: normalizedOptions.parsedTags,
}
);
addFiles(tree, normalizedOptions);
await formatFiles(tree);
}
В этом файле стоит обратить внимание на функцию выше. По сути она является точкой входа, и именно ее вызывает @nrwl/cli для работы генератора. Исходными параметрами она принимает tree — абстрактное дерево файловой системы, из которого мы можем достать все необходимые файлы в нашем проекте, и объект options, который реализует интерфейс AwesomeComponentGeneratorSchema и отвечает за те аргументы, которые мы передали при запуске команды.
Изменение файловой системы
В Nx-проекте уже есть зависимость от пакета @nrwl/devkit, который содержит много полезных функций для работы с генераторами. Например, выше в коде мы использовали функцию addProjectConfiguration, она создает project.json файл.
Иногда в такие функции нужно передавать абстрактное файловое дерево, потому что в своей реализации функциям нужно взаимодействовать с файловой системой. Полный список функций есть на странице библиотеки @nx/devkit.
Есть базовый набор функций, которые помогут нам взаимодействовать с файловой системой.
Создание файлов
Универсальная функция для создания любых файлов:
generateFiles(tree, path.join(__dirname, 'files'), options.projectRoot, templateOptions);
Первый параметр — сущность абстрактного файлового дерева. Второй — путь до папки с теми файлами, которые мы хотим сгенерировать.
Мы уже видели в структуре генератора папку files, а в ней index.ts__template__. Указывая путь до этой папки, мы проходим по шаблону каждого файла в ней и создаем готовые файлы в проекте в соответствии с указанным шаблоном. Постфикс __template__ нужен для того, чтобы динамически изменять файл в зависимости от переданных переменных. Например, можно создать в папке Typescript-файл, но его название и содержимое всегда будут одним и тем же при генерации.
Постфикс __tepmlate__ убирается компилятором при генерации и показывает, что в этом файле возможна динамическая подстановка переменных. Чтобы изменять значение, в названии файла нужно указать __name__ — имя переменной, которое мы передаем в templateOptions, а чтобы менять переменные в самом файле, необходимо использовать синтаксис <%= name %>.
Третий параметр — это путь до папки, куда хотим создавать файлы, и последний параметр — templateOptions — это объект, в который мы кладем те переменные, которые хотим использовать при генерации файлов.
Благодаря функции generateFiles и шаблонным файлам можно сгенерировать себе армию файлов и жить прекрасно. А еще в функции генератора мы можем использовать другие генераторы:
await libraryGenerator(tree, {
name: normalizedOptions.name,
directory: normalizedOptions.directoryWithoutName,
});
Мы использовали генератор библиотеки от @nrwl/angular — при вызове он создаст необходимые файлы, которые можно изменять или удалять.
Обновление файлов
В @nrwl/devkit есть ряд полезных функций, начинающихся с update. Самая универсальная из них — updateJson:
updateJson(tree, path.join('libs', plugin, 'package.json'), (config) => ({...config, something: 'something'}));
Функция изменяет существующий package.json локального плагина, который мы создали. В качестве аргументов она принимает объект абстрактного файлового дерева, путь до существующего json-файла и функцию, которая принимает аргументом объект конфига и возвращает модифицированный объект конфига. В конкретном примере мы просто дописали поле something в существующий конфиг, и если мы запустим генератор с данной командой, то увидим соответствующие изменения в файловой системе.
Для модификации Typescript-файлов можно посмотреть в сторону ng-morph.
Эта библиотека позволяет проще взаимодействовать с ts-файлам и сочетается с функцией генератора: и там, и там используется абстрактное файловое дерево. Еще она содержит много инструментов для angular-разработки, что упрощает изменения angular-файлов.
Удаление файлов
Абстрактное файловое дерево содержит в себе все файлы и предоставляет возможность их удаления. В примере ниже мы проходим циклом по заранее созданным файлам — последовательно удаляем их, а функция принимает единственный параметр — путь до файла, который нужно удалить:
[
'.babelrc',
'README.md',
'package.json',
'tsconfig.json',
'tsconfig.spec.json',
'.eslintrc.json',
].forEach(file => tree.delete(joinPath(toProjectLibPath(schema), file)));
После запуска генератора мы увидим изменения файлового дерева, где не будет вышеперечисленных файлов по указанному пути.
Вот и все
Мы разобрали, что такое Nx-plugin и Nx-generator, рассмотрели готовые реализации плагинов, создали свой локальный плагин и изучили возможности генераторов.
Надеюсь, эта информация облегчит кому-то работу с написанием рутинного кода и сделает жизнь чуточку лучше. А я буду рад обсудить любые вопросы по теме в комментариях!