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:
@@ -404,7 +404,10 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
||||
publisher?: string;
|
||||
};
|
||||
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 {
|
||||
const eventKebab = a.event.replace(/\./g, "-");
|
||||
return [
|
||||
@@ -475,3 +600,14 @@ function camel(input: string): string {
|
||||
const p = cap(input);
|
||||
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, "_");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user