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;
|
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, "_");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user