текст перекладу
Nx пропонує багато функцій, і я часто бачу, що користувачі Nx не знають або боюються розширювати Nx за допомогою реалізації Nx плагінів.
Я працював над багатьма різними монорепозиторіями (великими, маленькими, розподіленими тощо), і налаштування архітектури Nx плагінів допомогло мені вирішити багато проблем. Це спрощує підтримку Nx і полегшує уніфікацію робочих процесів для різних технологій та команд.
У цій статті я просто хочу записати кілька порад, які допомагають ефективно реалізувати Nx плагіни.
1. Почніть використовувати Nx плагіни
Ви створюєте багаторазові функції? Створюєте утиліти? Якщо так, то це саме те, для чого використовуються Nx плагіни в екосистемі Nx.
Я часто бачу, що користувачі Nx бояться і вважають Nx плагіни складною конфігурацією. Однак вони працюють з складними монорепозиторіями з дубльованими конфігураціями.
Якщо ви хочете дізнатися більше про те, що можуть дати Nx плагіни, я раджу вам ознайомитися з моєю наступною статтею:
[
🏘️ Полі-монорепозиторії з Nx
Полі або моно репозиторії? Краще з двох світів!
itnext.io
](https://itnext.io/%EF%B8%8F-poly-monorepos-with-nx-dd7f5578e3fa?source=post_page-----16c07e8bb3e0--------------------------------)
2. Використовуйте інферування замість генераторів, виконавців та міграцій
До існування проекту Nx Project Crystal, плагіни були обмежені налаштуванням:
- Генератори: для генерації кастомних файлів і конфігурацій під час створення проекту.
- Виконавці: для інтеграції кастомного коду, який виконується під час виконання завдання.
- Міграції: для виконання генераторів під час оновлення розподіленого репозиторію.
З часу появи Nx Project Crystal, багато налаштувань можна просто видалити:
Спрощення генераторів
Вони стають простішими, оскільки кастомні конфігурації обробляються всередині самого плагіна:
Наприклад, вам не потрібно використовувати генератори для генерації конфігурацій проекту, оскільки це буде підтримуватися безпосередньо вашим плагіном. Для деяких випадків ви навіть можете видалити кастомний генератор і використовувати основний плагін Nx.
Видалити виконавців
Замість написання кастомного виконавця, ви тепер можете виконувати кастомні команди Nx або встановлювати кастомні налаштування в залежності від проекту:
Наприклад, якщо раніше вам був потрібен виконавець для генерування типів на корені вашого проекту, таких як “executor”: “@org/openapi:typescript”
, то це більше не потрібно. Тепер ця функціональність може бути автоматично оброблена вашим плагіном, @org/openapi/plugin
.
Видалення міграцій
Тепер потрібно менше міграцій. Насправді, замість виконання міграцій на розподілених репозиторіях, вам достатньо просто оновити плагіни, і ви отримаєте всі нові конфігурації безкоштовно.
Наприклад, якщо раніше вам потрібно було оновлювати конфігурації проекту під час оновлення, тепер ви можете просто оновити ваш проект за допомогою нової версії плагінів Nx, і ви автоматично отримаєте оновлену інферовану конфігурацію.
3. Прийміть вторинні точки входу
Nx плагін може виконувати кілька функцій (генератори, виконавці, інферування тощо). І всі ці функції будуть використовуватися:
- Окремо: виконавці викликаються при виконанні завдання, генератори/міграції на вимогу, і інферування автоматично при генерації графа.
- У різних випадках використання: Ви можете використовувати локальні плагіни Nx або безпосередньо з npm пакету.
текст перекладу
Ви також можете імпортувати існуючі плагіни Nx, створюючи власні.
🚫 Якщо ви змішаєте та відкриєте всі ці функції в одному великому файлі barrel, ви можете імпортувати небажані бібліотеки та стикнутися з проблемами продуктивності при їх використанні:
При використанні локальних плагінів, Nx компілює весь код плагіна під час виконання, навіть якщо використовується лише один плагін.
✅ Важливо мати одну точку входу для кожної функції, особливо для інферованих конфігурацій:
Один з основних плагінів Nx, який чудово ілюструє цей підхід, це@nx/angular
пакет.
⚠️ На даний момент неможливо використовувати вторинні точки входу для локальних інферувань. Більше інформації в наступному обговоренні:
[
Покращити завантаження локальних плагінів Nx · nrwl nx · Обговорення #26668
Опис: У Nx можна створювати спеціалізовані бібліотеки, звані Nx Plugin, які включають потужні вбудовані…
github.com
](https://github.com/nrwl/nx/discussions/26668?source=post_page-----16c07e8bb3e0--------------------------------)
4. Використовуйте простий файл замість бібліотеки плагіна Nx
Вам не потрібно створювати бібліотеку лише для використання інферування. Ви можете оголосити прості файли у плагінах вашого nx.json
:
{
"$schema": "packages/nx/schemas/nx-schema.json",
...
"plugins": [
"./tools/plugins/my-plugin.ts",
"./libs/my-plugin/plugin.ts"
],
...
}
Безпосередньо пов'язано з Підказкою 3. Прийміть вторинні точки входу, вказавши простий файл, ви уникнете необхідності компілювати цілу бібліотеку і дозволите кращу гранулярність для ваших інферованих конфігурацій.
5. Вкладені плагіни Nx
Як і для будь-якої бібліотеки, рекомендується структурувати ваш проект за доменами. Цей принцип також має застосовуватися до архітектури плагінів. Однак на сьогоднішній день неможливо створювати кілька типів конфігурацій для одного плагіна.
Альтернативою є використання більш загального шаблону. Цей підхід дозволяє запускати плагін для кількох типів файлів, а потім направляти кожен файл до певного підмножини конфігурацій.
Ознайомтеся з Підказкою 9. Створення багаторазових утиліт для ваших плагінів для реалізації цього підходу, що використовує утиліту combinePattern
, надану Nx.
Іншою альтернативою є використання Підказки 4: Використовуйте простий файл замість бібліотеки плагіна Nx.
6. Перелічіть проекти, на які впливає плагін Nx
Одним із аспектів, який важко контролювати, є кількість проектів, на які впливає один плагін, особливо в великих монорепозиторіях. Насправді деякі проекти можуть бути вплинуті, оскільки вони відповідають небажаним проектам.
Якщо ви використовуєте команду nx show project [projectName]
, ви побачите, які конфігурації генерує плагін:
Але якщо ви хочете отримати глобальний огляд впливу плагіна на проекти, для кожного плагіна ви можете додати тег, пов'язаний з цим плагіном.
Наприклад, ви могли б додати тег nx-plugin:jest
у вашому кастомному плагіні jest.
Оскільки проект може бути вплинутий кількома плагінами, ви можете мати кілька тегів.
Тоді, якщо ви хочете перерахувати всі проекти, на які впливає один плагін, ви можете використати команду Nx:
// перелічити всі проекти, на які впливає плагін jest
nx show projects --projects "tag:nx-plugin:jest"
// перелічити всі проекти, на які впливають плагіни
nx show projects --projects "tag:nx-plugin:*"
І потім ви можете перевірити, чи список проектів відповідає вашим очікуванням.
7. Ознайомтесь з основними плагінами Nx
Це проста порада, але вона багато чому мене навчила.
текст перекладу
Читання коду, написаного командою Nx, і дослідження репозиторію Nx дає багато інформації про те, як ви можете структурувати або писати архітектуру вашого плагіна.
Зазвичай структура завжди однакова, і ви можете знайти реалізації плагінів в packages/[packageName]/src/plugins/plugin.ts
.
8. Використання кількох рівнів плагінів Nx
Як зазначено в попередньому підказці, якщо ви перевірите список плагінів в репозиторії Nx, ви помітите, що вони організовані за технологіями.
Однак монорепозиторій організації часто узгоджений з командами або продуктами. Тому має сенс мати кілька типів плагінів, орієнтованих на конкретні домени, для яких вони призначені.
- Перший рівень буде безпосередньо використовувати плагіни Nx.
- Другий рівень буде спеціалізованим і розширювати існуючі плагіни.
- Наступні рівні будуть групувати інші плагіни залежно від доменів (команди/продукти/...).
Більше деталей про архітектуру плагінів можна знайти в моїй наступній статті 🏘️ Полі Монорепозиторії з Nx
9. Створення багаторазових утиліт для ваших плагінів
Написання та підтримка утиліт може бути обтяжливим, оскільки багато кроків часто повторюються кілька разів. Використання утиліт спрощує впровадження ваших плагінів і дозволяє узагальнювати стандартні поведінки, такі як додавання тегу, як описано в Підказці 6: Перелічіть проекти, на які впливає плагін Nx.
Зазвичай я маю одну утиліту для генерування конфігурацій:
import { dirname } from 'node:path';
import { CreateNodesContext, CreateNodesContextV2, CreateNodesResult } from 'nx/src/project-graph/plugins/public-api';
import { calculateHashForCreateNodes, ConfigCache } from './cache-config.utils';
import { isAttachedToProject } from './is-attached-to-project.util';
import { ProjectConfiguration } from '@nx/devkit';
export type GenerateConfig<T> = (
projectRoot: string,
filePath: string,
options: T,
context: CreateNodesContextV2
) => Partial<ProjectConfiguration> | Promise<Partial<ProjectConfiguration>>;
export type WithProjectRoot = (filePath: string, options: T, context: CreateNodesContextV2) => string;
export type SkipIf = (projectRoot: string, filePath: string, options: T, context: CreateNodesContextV2) => boolean;
export type WithOptionsNormalizer = (options: Partial<T>) => T;
export type CreateNodesInternal<T> = readonly [
projectFilePattern: string,
createNodesInternal: CreateNodesInternalFunction<T>
];
export type CreateNodesInternalFunction<T> = (
filePath: string,
options: T,
context: CreateNodesContext & { pluginName: string },
configCache: ConfigCache
) => Promise<CreateNodesResult<T>>;
export function createNodesInternalBuilder<T>(projectFilePattern: string, generateConfig: GenerateConfig<T>) {
let withOptionsNormalizer: WithOptionsNormalizer;
let withProjectRoot: WithProjectRoot;
const skipIf: SkipIf[] = [];
const builder = {
withProjectRoot(fn: WithProjectRoot) {
withProjectRoot = fn;
return builder;
},
withOptionsNormalizer(fn: WithOptionsNormalizer) {
withOptionsNormalizer = fn;
return builder;
},
skipIf(fn: SkipIf) {
skipIf.push(fn);
return builder;
},
build(): CreateNodesInternal<T> {
return [
projectFilePattern,
async (filePath, options, context, configCache) => {
// Normalize the options if a normalizer function is provided.
options ??= {} as T;
options = withOptionsNormalizer ? withOptionsNormalizer(options) : options;
// Get project root from the file path. By default, take the directory of the file.
текст перекладу
const projectRoot = withProjectRoot ? withProjectRoot(filePath, options, context) : dirname(filePath);
// Пропустити, якщо одна з функцій skipIf повертає true. За замовчуванням вона повинна бути пов'язана з project.json.
const isNotAttachedToProject: SkipIf = (projectRoot, filePath) => !filePath.includes('project.json') && !isAttachedToProject(projectRoot);
const shouldSkip = [isNotAttachedToProject, ...skipIf].some((fn) => fn(projectRoot, filePath, options, context));
if (shouldSkip) return {};
// Обчислення хешу на основі параметрів і шаблону
const nodeHash = await calculateHashForCreateNodes(projectRoot, options, context);
const hash = `${nodeHash}_${projectFilePattern}`;
// якщо конфігурація ще не в кеші, згенерувати її
if (!configCache[hash]) {
// logger.verbose(`Devkit ${context.pluginName}: Перерахувати кеш для ${filePath}`);
// додати за замовчуванням тег для
const pluginTag = `nx-plugin:${context.pluginName}`;
const config = await generateConfig(projectRoot, filePath, options, context);
configCache[hash] = {
...config,
tags: [...(config?.tags ?? []), pluginTag],
};
}
return {
projects: {
[projectRoot]: {
root: projectRoot,
...configCache[hash],
},
},
};
},
];
},
};
return builder;
}
І в мене також є утиліта для спрощення об'єднання кількох конфігурацій:
import { createNodesFromFiles, CreateNodesV2 } from '@nx/devkit';
import { minimatch } from 'minimatch';
import { join } from 'node:path';
import { hashObject } from 'nx/src/hasher/file-hasher';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import { readConfigCache, writeConfigToCache } from './cache-config.utils';
import { CreateNodesInternal } from './create-nodes-internal-builder.utils';
export function combineCreateNodes<T>(
pluginName: string,
createNodesInternals: CreateNodesInternal<T>[]
): CreateNodesV2 {
const projectFilePatterns = createNodesInternals.map(([globPattern]) => globPattern);
return [
combineGlobPatterns(projectFilePatterns),
async (files, opt, context) => {
const options = opt as T;
const optionsHash = hashObject(options);
const cachePath = join(workspaceDataDirectory, `${pluginName}-${optionsHash}.hash`);
const configCache = readConfigCache(cachePath);
try {
return await createNodesFromFiles(
(filePath, nestedOpt, context) => {
const options = nestedOpt as T;
// знайти вкладену конфігурацію створення на основі шаблону
const createNodesInternal = createNodesInternals.find(([globPattern]) => minimatch(filePath, globPattern, { dot: true }));
if (!createNodesInternal) throw new Error(`Не знайдено createNodesInternal для ${filePath}`);
const nestedCreateNodesInternal = createNodesInternal[1];
return nestedCreateNodesInternal(filePath, options, { ...context, pluginName }, configCache);
},
files,
options,
context
);
} finally {
writeConfigToCache(cachePath, configCache);
}
},
];
}
В кінці кожен plugin.ts
виглядає так:
const normalizeOptions: WithOptionsNormalizer = (options) => ({
buildTargetName: options.buildTargetName ?? 'build',
testTargetName: options.testTargetName ?? 'test'
});
const createNodesInternalForApp: CreateNodesInternal =
createNodesInternalBuilder('apps/domain-a/**/*-app/project.json', (projectRoot, filePath, options) => ({
tags: ['scope:domain-a'],
targets: {
[options.buildTargetName]: {
// ...
},
[options.testTargetName]: {
// ...
текст перекладу
const normalizeOptions: WithOptionsNormalizer = (options) => ({
buildTargetName: options.buildTargetName ?? 'build',
testTargetName: options.testTargetName ?? 'test'
});
const createNodesInternalForApp: CreateNodesInternal =
createNodesInternalBuilder('apps/domain-a/**/*-app/project.json', (projectRoot, filePath, options) => ({
tags: ['scope:domain-a'],
targets: {
[options.buildTargetName]: {
// ...
},
[options.testTargetName]: {
// ...
}
}
}))
.withOptionsNormalizer(normalizeOptions)
.build();
const createNodesInternalForFeature: CreateNodesInternal =
createNodesInternalBuilder('libs/domain-a/**/*-feat/project.json', (projectRoot, filePath, options) => ({
tags: ['scope:domain-a'],
targets: {
[options.testTargetName]: {
// ...
}
}
}))
.withOptionsNormalizer(normalizeOptions)
.withOptionsNormalizer(normalizeOptions)
.build();
export const createNodesV2 = combineCreateNodes('domain-a-nx-plugin', [
createNodesInternalForApp,
createNodesInternalForFeature
]);
Звичайно, ці утиліти повинні бути адаптовані до ваших потреб.
10. Орієнтований на проект чи на файл
Є два підходи для призначення конфігурацій для проекту:
Орієнтований на файл
Для кожного плагіна шаблон буде співпадати з конкретним файлом. Ви матимете кілька виконань плагіна для кожного проекту:
Наприклад, якщо один проект містить файл jest.config.ts
та tsconfig.json
, два плагіни можуть генерувати відповідні конфігурації для цього проекту. Потім обидві конфігурації будуть об'єднані.
У великих монорепозиторіях цей підхід може спричиняти проблеми з продуктивністю при виконанні команд.
Орієнтований на проект
Шаблон буде співпадати тільки з project.json
або структурою, яка дозволить генерувати проект тільки один раз. Потім плагін сканує файли навколо цього проекту, щоб призначити конфігурації.
Вибір між орієнтованим на файл підходом чи орієнтованим на проект залежить від кількох факторів, таких як розмір вашого монорепозиторію або спосіб, яким реалізовано ваш плагін.
Підсумок
Мати архітектуру плагінів Nx може бути надзвичайно корисним для підтримки та уніфікації вашого коду.
Сподіваюся, цей список порад допоможе вам почуватися більш впевнено у прийнятті рішень і їх адаптації для створення архітектури плагінів Nx.
Якщо у вас є додаткові поради або запитання, не соромтесь звертатися до мене або забронювати дзвінок. Більше інформації доступно на моєму вебсайті 👇
Ресурси
[
Розширення Nx за допомогою плагінів
Дізнайтесь, як розширити Nx, створюючи та випускаючи власний плагін Nx.
nx.dev
](https://nx.dev/extending-nx/intro/getting-started?source=post_page-----16c07e8bb3e0--------------------------------)
[
🏘️ Полі Монорепозиторії з Nx
Полі чи моно репозиторії? Краще з обох світів!
itnext.io
](https://itnext.io/%EF%B8%8F-poly-monorepos-with-nx-dd7f5578e3fa?source=post_page-----16c07e8bb3e0--------------------------------)
[
Уникнення конфліктів портів з кількома екземплярами Storybook
Останні новини від команди Nx & Nx Cloud
nx.dev
](https://nx.dev/blog/dynamic-targets-with-inference-tasks?source=post_page-----16c07e8bb3e0--------------------------------)
[
💎 Виявлення магії Nx Project Crystal
Огляд еволюції конфігурацій Nx до безперешкодного розвитку
javascript.plainenglish.io
](https://javascript.plainenglish.io/discovering-nx-project-crystals-magic-7f42faf2a135?source=post_page-----16c07e8bb3e0--------------------------------)
Перекладено з: 💡 10 Tips for Successful Nx Plugin Architecture