Перейти к содержимому
GitHubXDiscord

migrateFromListenerMiddleware

Стабильный · с 0.1.0

Проходит по файлу, использующему createListenerMiddleware / startListening из Redux Toolkit, и генерирует один *.trigger.ts на каждую регистрацию startListening({ actionCreator, effect }). Исходный файл остаётся нетронутым — адоптеры просматривают сгенерированные триггеры, подвязывают их в компонентах через useEvent, а когда готовы — удаляют регистрацию middleware.

Поставляется как программный API и как CLI-команда (triggery-codemod migrate-from-listener-middleware).

import { migrateFromListenerMiddleware } from '@triggery/codemod';
function migrateFromListenerMiddleware(
  options: MigrateFromListenerMiddlewareOptions,
): MigrateFromListenerMiddlewareResult;

interface MigrateFromListenerMiddlewareOptions {
  readonly file: string;
  readonly outDir?: string;
  readonly dryRun?: boolean;
  readonly project?: import('ts-morph').Project;
}

interface MigrateFromListenerMiddlewareResult {
  readonly file: string;
  readonly migrated: readonly MigratedListener[];
}

interface MigratedListener {
  readonly eventName: string;
  readonly triggerFilePath: string;
  readonly triggerFileContent: string;
}
ПараметрТипОбязательныйОписание
filestringдаПуть к файлу, объявляющему middleware.
outDirstringнетДиректория для сгенерированных файлов триггеров. По умолчанию — директория исходника.
dryRunbooleanнетСпланировать изменения и вернуть их без записи на диск.
projectts-morph.ProjectнетПредсуществующий project — переиспользуй между батчами.
ПолеОписание
fileИсходный файл, который был обработан.
migratedПо одной записи на каждый обработанный вызов startListening — имя, выходной путь, сгенерированный исходник.

Codemod распознаёт каноническую форму RTK:

startListening({
  actionCreator: someAction,
  effect: (action, listenerApi) => { /* … */ },
});

Другие формы — matcher, predicate или type — обнаруживаются, но в V1 не трансформируются. Исходный файл не переписывается, так что повторные прогоны идемпотентны.

triggery-codemod migrate-from-listener-middleware [--out-dir <path>] [--dry-run] <file>
ФлагОбязательныйОписание
--out-dirнетПереопределить дефолтную выходную директорию.
--dry-runнетРаспечатать запланированные изменения без записи.
import { migrateFromListenerMiddleware } from '@triggery/codemod';

const result = migrateFromListenerMiddleware({
  file: 'src/store/listenerMiddleware.ts',
  outDir: 'src/triggers',
});

for (const m of result.migrated) {
  console.log(m.eventName, '→', m.triggerFilePath);
}
triggery-codemod migrate-from-listener-middleware --out-dir src/triggers src/store/listenerMiddleware.ts

Вывод:

Migrated 3 listener(s) from src/store/listenerMiddleware.ts:
  • user-logged-in → src/triggers/user-logged-in.trigger.ts
  • product-added  → src/triggers/product-added.trigger.ts
  • cart-checked-out → src/triggers/cart-checked-out.trigger.ts

Для каждого startListening({ actionCreator: userLoggedIn, effect: (action, api) => {...} }) codemod пишет:

import { createTrigger } from '@triggery/core';

/**
 * Auto-migrated from a Redux Toolkit listenerMiddleware `startListening`
 * registration. Review the generated handler …
 */
export const userLoggedInTrigger = createTrigger<{
  events: { 'user-logged-in': unknown };
  conditions: Record<string, never>;
  actions: Record<string, never>;
}>({
  id: 'user-logged-in',
  events: ['user-logged-in'],
  required: [],
  async handler({ event, conditions, actions, check }) {
    // TODO: original RTK effect body — refactor dispatch/getState into actions/conditions.
    const action = event.payload;
    /* … original effect body … */
  },
});

Имя события выводится из текста символа actionCreator. Codemod не пытается разрезолвить .type на action creator’е — это требовало бы type info — поэтому если slug’и action creator’а не совпадают с type-строкой слайса, имена событий правь руками.

Сгенерированные триггеры — это стартеры. После прогона codemod-скрипта:

  1. Замени listenerApi.dispatch(x) на действие Triggery — добавь actions.<name> в схему, зарегистрируй через useAction в подходящем компоненте.
  2. Замени чтения listenerApi.getState() типизированными условиями — регистрируй через useCondition.
  3. Подключи зажигание нового события: компоненты, которые раньше диспатчили слушаемое действие, теперь зовут useEvent(trigger, 'name').
  4. Когда каждый потребитель подвязан — удаляй блок startListening.