feat(turbo-gen): wire gen event consume actions

Adds consumeActions for the event generator: scaffolds the handler file
+ test, the Payload event-task that completes the production-bus loop,
modifies the consumer feature's symbols (// <gen:event-handler-symbols>),
both binders (// <gen:event-handlers>), and the cms re-export
(// <gen:job-tasks>). The bind block wraps the handler in span+capture,
binds it into the per-feature container, and subscribes it on the bus.
This commit is contained in:
2026-05-08 16:18:12 +02:00
parent d53869f712
commit 31f825a5d7

View File

@@ -404,7 +404,10 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void {
publisher?: string; publisher?: string;
}; };
if (a.mode === "publish") return publishActions(a); if (a.mode === "publish") return publishActions(a);
throw new Error("consume mode is wired in Task 40"); if (!a.publisher) {
throw new Error("consume mode requires a publisher feature");
}
return consumeActions(a as Required<typeof a>);
}, },
}); });
} }
@@ -443,6 +446,128 @@ function publishActions(a: {
]; ];
} }
function consumeActions(a: {
feature: string;
event: string;
publisher: string;
}): PlopTypes.ActionType[] {
const eventKebab = a.event.replace(/\./g, "-");
const handlerName = `on${pascalCase(a.publisher)}${pascalCase(a.event)}`;
const symbolFile = `packages/${a.feature}/src/di/symbols.ts`;
const bindProdFile = `packages/${a.feature}/src/di/bind-production.ts`;
const bindDevFile = `packages/${a.feature}/src/di/bind-dev-seed.ts`;
const cmsIndexFile = `packages/${a.feature}/src/integrations/cms/index.ts`;
return [
() => {
assertAnchors(process.cwd(), symbolFile, ["// <gen:event-handler-symbols>"]);
assertAnchors(process.cwd(), bindProdFile, ["// <gen:event-handlers>"]);
assertAnchors(process.cwd(), bindDevFile, ["// <gen:event-handlers>"]);
assertAnchors(process.cwd(), cmsIndexFile, ["// <gen:job-tasks>"]);
return "All required anchors present";
},
{
type: "add",
path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`,
templateFile: "templates/event/consume/handler.ts.hbs",
data: a,
},
{
type: "add",
path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.test.ts`,
templateFile: "templates/event/consume/handler.test.ts.hbs",
data: a,
},
{
type: "add",
path: `packages/${a.feature}/src/integrations/cms/jobs/__events-${a.publisher}-${eventKebab}.task.ts`,
templateFile: "templates/event/consume/event-task.ts.hbs",
data: a,
},
{
type: "modify",
path: symbolFile,
pattern: /\/\/ <gen:event-handler-symbols>/,
template: `// <gen:event-handler-symbols>\n IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler: Symbol.for("@repo/${a.feature}/${handlerName}"),`,
},
{
type: "modify",
path: bindProdFile,
pattern: /\/\/ <gen:event-handlers>/,
template: handlerBindBlock(a),
},
{
type: "modify",
path: bindDevFile,
pattern: /\/\/ <gen:event-handlers>/,
template: handlerBindBlock(a),
},
{
type: "modify",
path: cmsIndexFile,
pattern: /\/\/ <gen:job-tasks>/,
template: `// <gen:job-tasks>\nexport { on${pascalCase(a.publisher)}${pascalCase(a.event)}EventTask } from "./jobs/__events-${a.publisher}-${eventKebab}.task";`,
},
() => printConsumeNextSteps(a),
];
}
function handlerBindBlock(a: {
feature: string;
event: string;
publisher: string;
}): string {
const handlerFn = `on${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`;
const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`;
const handlerSymbol = `${constantCase(a.feature)}_SYMBOLS.IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`;
const wrappedVar = `wrapped${pascalCase(a.publisher)}${pascalCase(a.event)}`;
const containerVar = `${camel(a.feature)}Container`;
return `// <gen:event-handlers>
// ${handlerFn} subscription — generated, edit the handler file (not this block) for behavior.
const ${wrappedVar} = withSpan(
tracer,
{ name: "${a.feature}.${handlerFn}", op: "event-handler" },
withCapture(
logger,
{
feature: "${a.feature}",
layer: "event-handler",
name: "${a.feature}.${handlerFn}",
},
${handlerFn}(),
),
);
if (${containerVar}.isBound(${handlerSymbol})) {
${containerVar}.unbind(${handlerSymbol});
}
${containerVar}.bind(${handlerSymbol}).toConstantValue(${wrappedVar});
bus.subscribe(${eventConst}, "${a.feature}", ${wrappedVar});`;
}
function printConsumeNextSteps(a: {
feature: string;
event: string;
publisher: string;
}): string {
const eventKebab = a.event.replace(/\./g, "-");
const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`;
return [
"",
"─────────────────────────────────────────────────────────────",
`Handler on-${a.publisher}-${eventKebab} scaffolded in ${a.feature}.`,
"",
"Next steps (manual):",
` 1. Implement the handler body in packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`,
` 2. Add the import at the top of bind-production.ts AND bind-dev-seed.ts:`,
` import { ${eventConst} } from "@repo/${a.publisher}";`,
` import { ${camel(`on-${a.publisher}-${a.event.replace(/\./g, "-")}-handler`)} } from "../events/handlers/on-${a.publisher}-${eventKebab}.handler";`,
` 3. If your handler needs deps, extend the factory signature and pass them in the generated bind block.`,
` 4. Add "@repo/${a.publisher}": "workspace:*" to packages/${a.feature}/package.json dependencies (if not already present).`,
` 5. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
"─────────────────────────────────────────────────────────────",
"",
].join("\n");
}
function printPublishNextSteps(a: { feature: string; event: string }): string { function printPublishNextSteps(a: { feature: string; event: string }): string {
const eventKebab = a.event.replace(/\./g, "-"); const eventKebab = a.event.replace(/\./g, "-");
return [ return [
@@ -475,3 +600,14 @@ function camel(input: string): string {
const p = cap(input); const p = cap(input);
return p ? p[0]!.toLowerCase() + p.slice(1) : ""; return p ? p[0]!.toLowerCase() + p.slice(1) : "";
} }
function pascalCase(input: string): string {
// Same as `cap` but also splits on dots, so "user.signed-up" → "UserSignedUp".
return input
.split(/[-_.\s]+/)
.filter(Boolean)
.map((s) => s[0]!.toUpperCase() + s.slice(1))
.join("");
}
function constantCase(input: string): string {
return input.toUpperCase().replace(/[-.\s]/g, "_");
}