migrateFromListenerMiddleware
Walks a file that uses Redux Toolkit’s createListenerMiddleware / startListening and generates one *.trigger.ts per startListening({ actionCreator, effect }) registration. The source file is left untouched — adopters review the generated triggers, wire them into their components via useEvent, then delete the middleware registration when ready.
Provided as both a programmatic API and a CLI command (triggery-codemod migrate-from-listener-middleware).
Import
Section titled “Import”import { migrateFromListenerMiddleware } from '@triggery/codemod';
Signature
Section titled “Signature”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;
}Parameters
Section titled “Parameters”| Param | Type | Required | Description |
|---|---|---|---|
file | string | yes | Path to the file declaring the middleware. |
outDir | string | no | Directory for the generated trigger files. Defaults to the source file’s directory. |
dryRun | boolean | no | Plan the changes and return them without writing to disk. |
project | ts-morph.Project | no | Pre-existing project — reuse across batches. |
Returns
Section titled “Returns”| Field | Description |
|---|---|
file | The source file that was processed. |
migrated | One entry per startListening call the codemod handled — name, output path, generated source. |
Supported shapes
Section titled “Supported shapes”The codemod recognises the canonical RTK pattern:
startListening({
actionCreator: someAction,
effect: (action, listenerApi) => { /* … */ },
});Other shapes — matcher, predicate, or type — are detected but not transformed in V1. The source file isn’t rewritten, so re-runs are idempotent.
triggery-codemod migrate-from-listener-middleware [--out-dir <path>] [--dry-run] <file>| Flag | Required | Description |
|---|---|---|
--out-dir | no | Override the default output directory. |
--dry-run | no | Print planned changes without writing. |
Examples
Section titled “Examples”Programmatic
Section titled “Programmatic”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.tsOutput:
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.tsWhat gets generated
Section titled “What gets generated”For each startListening({ actionCreator: userLoggedIn, effect: (action, api) => {...} }), the codemod writes:
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 … */
},
});The event name is derived from the actionCreator symbol’s text. The codemod doesn’t try to resolve .type on the action creator — that would require type info — so name the events by hand if your action creator slugs don’t match the slice’s type string.
Follow-up steps
Section titled “Follow-up steps”The generated triggers are starters. After running the codemod:
- Replace
listenerApi.dispatch(x)with a Triggery action — addactions.<name>to the schema, register it viauseActionin the appropriate component. - Replace
listenerApi.getState()reads with typed conditions — register viauseCondition. - Wire the new event firing: components that used to dispatch the listened-for action now call
useEvent(trigger, 'name')instead. - Once every consumer is wired, delete the
startListeningblock.