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

Устранение критических изменений с помощью генераторов рабочего пространства Nx

Экосистема Nx превосходна, потому что, с одной стороны, она позволяет разным командам работать в рамках одного и того же моно-репозитория и создавать надежные корпоративные решения. С другой стороны, базовая функциональность Nx относительно проста. Вот почему решения на базе Nx в прошлый раз оказались более популярными. Следовательно, все большее число монорепо включает в себя такие технологии, как React, Angular, Nests и даже Golang. Более того, эти библиотеки и фреймворки не одиноки в рамках monorepos. Они часто используют множество сторонних 3-d библиотек и пользовательских модулей. Одной из самых болезненных тем, связанных с разработкой программного обеспечения, включая Nx, является управление обновлениями зависимостей. Особенно зависимости с критическими изменениями, которые необходимо устранить, чтобы вернуть приложение в рабочее состояние. В этой статье предлагается одно из возможных решений. Это не подразумевается как окончательное решение и/или источник истины. Это только начальные мысли по теме.

Официальная документация Nx говорит нам о миграции следующее.

"nx migrate не только обновляет вас до последней версии Nx, но также обновляет версии зависимостей, которые мы поддерживаем и тестируем, такие как Jest и Cypress. Вы также можете использовать команду migrate для обновления любого плагина Nx."

Команда Nx проделала отличную работу, потому что вы можете обновить все решение максимально эффективно. Вот почему мы рекомендуем следующее чтение. Многие пакеты поддерживали это. Но "многие" не равнозначны "всем". Давайте рассмотрим следующий случай.

Существует демо-моно-репозиторий. Он содержит приложение React. Кроме того, он включает в себя общую библиотеку.

Давайте углубимся в демонстрационную функциональность и специфику кода.

Клиент

Клиентское приложение React довольно простое. Давайте запустим его.

git clone git@github.com:buchslava/nx-custom-migration-demo.git
npm i
npx nx run client:serve

Поведение пары приведенных выше форм аналогично. Они демонстрируют сумму двух чисел. Соответствующий код размещен здесь. Давайте посмотрим на код.

import { sumProxy } from '@nx-custom-migration-demo/common-lib';
import { useState } from 'react';
import { deprecatedSum } from 'try-lib';

export function App() {
  const [a, setA] = useState<number>(0);
  const [b, setB] = useState<number>(0);
  const [c, setC] = useState<number>();

  const [d, setD] = useState<number>(0);
  const [e, setE] = useState<number>(0);
  const [f, setF] = useState<number>();

  return (
    <>
      <div>
        <h2>Using internal lib</h2>
        <div>
          <input
            value={a}
            onChange={(e) => {
              setA(+e.target.value);
            }}
          />
          +
          <input
            value={b}
            onChange={(e) => {
              setB(+e.target.value);
            }}
          />
          <button
            onClick={() => {
              setC(sumProxy(a, b));
            }}
          >
            is
          </button>
          <span>{c}</span>
        </div>
      </div>

      <div>
        <h2>Using external lib</h2>
        <div>
          <input
            value={d}
            onChange={(e) => {
              setD(+e.target.value);
            }}
          />
          +
          <input
            value={e}
            onChange={(e) => {
              setE(+e.target.value);
            }}
          />
          <button
            onClick={() => {
              setF(deprecatedSum(d, e));
            }}
          >
            is
          </button>
          <span>{f}</span>
        </div>
      </div>
    </>
  );
}

export default App;

Обратите внимание на следующие моменты.

<button
  onClick={() => {
    setC(sumProxy(a, b));
  }}
>
  is
</button>

Приведенный выше код описывает вычисление результата для первой формы. Мы берем someProxy из внутренней библиотеки @nx-custom-migration-demo/common-lib.

<button
  onClick={() => {
    setF(deprecatedSum(d, e));
  }}
>
  is
</button>

Приведенный выше код описывает вычисление результата для второй формы. Мы берем deprecatedSum из внешней библиотеки try-lib.

Если мы посмотрим на package.json, мы сможем найти библиотеку.

"try-lib": "^1.0.1",

Внутренняя (общая) Библиотека

Библиотека размещена здесь.

import { deprecatedSum } from "try-lib";

export function sumProxy(a: number, b: number): number {
  return deprecatedSum(a, b);
}

Это похоже на вычисление для второй формы на клиенте из-за deprecatedSum из try-lib.

Внешняя библиотека

Пришло время взглянуть на внешнюю библиотеку. Вы можете найти библиотеку на github и npm.

Есть пара версий.

Version 1.0.1

Эта версия основана на следующем коде.

export function deprecatedSum(a: number, b: number): number {
  return a + b;
}

Version 2.0.0

Главной особенностью этой версии является то, что эта версия содержит критическое изменение. Главное изменение заключается в том, что deprecatedSum изменилось на sum.

export function sum(a: number, b: number): number {
  return a + b;
}

Проблема

Обратите внимание, что теперь мы используем try-lib@1.0.1. Пара важных мест выглядит следующим образом.

Давайте изменим его на версию 2.0.0 и запустим npm i.

Результат легко предсказать.

У нас есть неработающее решение, потому что в try-lib есть критическое изменение.

Решение

Конечно, мы можем исправить критические изменения вручную и забыть эту историю. Но такой подход подходит в основном для Ad hoc или для индивидуальных разработчиков. При обсуждении команд или корпоративных решений нам необходимо учитывать следующие факторы.

Фактор 1. Время.

Мы не знаем, когда нам следует применить изменения. Давайте представим, что команда решила улучшить версию библиотеки. Один из разработчиков предоставляет новую ветку, включая исправления. Но менеджер отложил слияние этого филиала, и команда понятия не имела, когда команда продолжит выполнение этой задачи. Что там происходит? Команда потратила время впустую, потому что ветка устарела, и в большинстве случаев разумно забыть ветку и повторить критические изменения и исправление снова. Другим вариантом является разрешение конфликтов в системе контроля версий (VCS). Но этот подход наихудший, потому что мы рискуем повредить решение. Конечно, это зависит от количества новых модификаций и кардинальных изменений.

Фактор 2. История.

Конечно, VCS (скажем, Git) может регистрировать изменения. Несмотря на это, информация может быть потеряна из-за перебазирования. Возможно, нам следует изобрести новый подход для более эффективного протоколирования изменений.

Фактор 3. Независимость.

Как я уже говорил вам ранее, Nx обладает мощной функцией миграции, которая позволяет обновлять Nx с помощью других поддерживаемых им зависимостей. Но давайте представим ситуацию, когда нам не нужно обновлять версию Nx. Тем не менее, в то же время нам необходимо обновить отдельную зависимость, автоматически отключив изменения и исправив их точно так же, как это делает Nx migration.

Требования

Проанализировав вышесказанное, давайте соберем требования к задаче.

  • Нам необходимо обновить отдельную зависимость в любое время. Вот почему следующий подход имеет смысл. Разработчик или команда не работают с результирующим кодом; напротив, они готовят алгоритм (функциональность), который преобразует решение в соответствии с ожидаемой версией зависимости. Это позволяет команде применять изменения в любое время, когда это необходимо. С одной стороны, это удорожает проект. С другой стороны, это снижает многие риски. У нас есть предсказуемый поток, потому что мы знаем, что происходит (результат применения) и как это происходит (алгоритм / функциональность). Кроме того, есть еще одна важная вещь. Если произойдет что-то не так, мы можем откатить изменения и легко повторить их, потому что нам не нужно выполнять ручные задания.
  • Нам нужно иметь дело с различными версиями пакета.
  • Описанная выше функциональность должна быть дружественной к Nx.
  • Самое важное здесь то, что описанная выше функциональность может быть разделена между Nx society и использоваться как часть будущего плагина Nx. Более подробная информация здесь.

Реализация

Когда мы размышляли о приведенных выше соображениях по реализации, мы столкнулись с использованием генераторов рабочего пространства Nx для создания новых записей в блоге Юри Штрумпфлохнера. Мы рекомендуем эту статью, потому что нашли в ней этот полезный подход к решению нашей задачи. Вкратце, наш план заключается в создании нового генератора рабочей области. Генератор рабочей области должен получить соответствующее имя пакета и впоследствии изменить исходный код.

Пришло время углубиться в решение.

Сначала мы создали генератор нового рабочего пространства с помощью следующей команды:

npx nx g @nrwl/workspace:workspace-generator code-transformer

Это создает новую папку в tools/generators/code-transformer с файлом index.ts и schema.json.

Настройка schema.json

Этот файл описывает метаданные нашего генератора. Это позволяет Nx обнаруживать генератор и предоставляет метаданные, такие как возможный аргумент, который генератор принимает для целей проверки или динамического отображения пользовательского интерфейса, как это делает Nx Console.

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

Мы открыли tools/generators/code-transformer/schema.json и скорректировали схему в соответствии с нашими требованиями.

{
  "$schema": "http://json-schema.org/schema",
  "cli": "nx",
  "$id": "code-transformer",
  "type": "object",
  "properties": {
    "package": {
      "type": "string",
      "description": "Package",
      "$default": {
        "$source": "argv",
        "index": 0
      }
    }
  },
  "required": ["package"]
}

Программа преобразования кода

Затем мы открыли tools/generators/code-transformer/index.ts и поместили туда свое решение.

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

import { Tree } from '@nrwl/devkit';
import * as fs from 'fs';
import * as path from 'path';
import * as semver from 'semver';
import * as util from 'util';
import { Project } from 'ts-morph';
import compile from './compiler';

const readFile = util.promisify(fs.readFile);

export default async function (tree: Tree, schema: any) {
  // Get "package.json"
  const packageJson: any = JSON.parse(
    (await readFile('package.json')).toString()
  );
  // "schema.package" contains the related dependency name
  // if it does not exist in "package.json" then
  // throw an error and stop the generator
  if (!packageJson.dependencies[schema.package]) {
    throw Error(`Dependency "${schema.package}" is not found in package.json`);
  }
  // Get an existing version of the related dependency
  const existingVersion = packageJson.dependencies[schema.package].replace(
    /^[\D]{1}/,
    ''
  );
  // Get `updates.json` config
  const updatesJson: any = JSON.parse(
    (await readFile('tools/updates/updates.json')).toString()
  );
  const activities: string[] = [];
  // Iterate over `updates` array
  for (const record of updatesJson.updates) {
    // if the existing (bumped) version from the package is more than "changes"-based and the package is expected
    // add the related function to activities
    if (schema.package === record.package && semver.gte(existingVersion, record.version)) {
      activities.push(path.resolve(process.cwd(), record.implementation));
    }
  }
  // Compile the related TS files
  compile(activities, {});

  // In this example, we use only "ts-morph" for the source code transformation
  const project = new Project();
  // Pass expected paths of source code
  const sources = [
    'libs/**/*.ts',
    'libs/**/*.tsx',
    'apps/client/**/*.ts',
    'apps/client/**/*.tsx',
  ];

  for (const activity of activities) {
    // Get related transformer-function
    const fn = require(activity).default;
    // Run it
    fn(project, sources);
  }
  // Save the changes
  await project.save();
}

Вы можете прочитать объясненный исходный код выше здесь.

Пришло время углубиться в концепцию "Updates". Существует следующая структура:

Файл update/update.json (https://github.com/buchslava/nx-custom-migration-demo/blob/main/tools/updates/updates.json) представляет конфигурацию, касающуюся всех обновлений.

  1. Значение updates представляет собой массив записей.
  2. Каждая запись содержит следующие данные: package (имя зависимости), description (изменение) и version. Но самые важные данные здесь - это implementation. Он представляет собой имя файла, содержащее код преобразования.

В нашем случае зависимость try-lib содержит изменения для версии 2.0.0 или более поздней, чем эта версия.

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

Одно важное замечание. Следующая идея преобразования Typescript основана на библиотеке ts-morph. Пожалуйста, ознакомьтесь с документацией ts-morph.

Давайте посмотрим на tools/updates/try-lib.2.0.0.fix-deprecated-sum.ts (вы можете найти соответствующий источник здесь)

import { Project, SyntaxKind, Node } from 'ts-morph';

// This is a recursive function that changes 
// some code fragments ("oldName" to "newName")
// according to its Node Type
function walkAndChange(
  node: Node,
  nodeKinds: SyntaxKind[],
  oldName: string,
  newName: string
) {
  // criteria matching
  if (
    nodeKinds.includes(node.getKind()) &&
    node.getFullText().trim() === oldName
  ) {
    node.replaceWithText(newName);
    return;
  }
  // recursive call
  node.forEachChild((c) => walkAndChange(c, nodeKinds, oldName, newName));
}

// Function-transformer takes Ts-morph project and related paths
export default function (project: Project, sources: string[]) {
  project.addSourceFilesAtPaths(sources);
  const files = project.getSourceFiles();

  // iterate the related source files
  for (const file of files) {
    // change "deprecatedSum" to "sum" in 
    // imports (SyntaxKind.ImportSpecifier) and 
    // in the rest (SyntaxKind.Identifier) of the code
    walkAndChange(
      file,
      [SyntaxKind.Identifier, SyntaxKind.ImportSpecifier],
      'deprecatedSum',
      'sum'
    );
  }
}

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

Используйте решение

Пришло время использовать решение, и давайте повторим его с самого начала.

Удар

Измените версию try-lib с 1.0.1 на 2.0.0 и запустите npm i.

Решающие изменения

Фиксация

Выполните следующую команду.

npx nx workspace-generator code-transformer -- try-lib --dry-run

Результат

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

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

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

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

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