feat(turbo-gen): event generator (publish mode)

Wires `pnpm turbo gen event` with publish/consume mode prompt; publish
branch generates the contract file + test and threads it through the
feature's // <gen:events> anchor. Consume branch is a stub that throws
until Task 40 lands the handler-side templates and modify-blocks.
This commit is contained in:
2026-05-08 16:16:04 +02:00
parent 0801469e89
commit 5cc6099015

View File

@@ -1,4 +1,7 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
import type { PlopTypes } from "@turbo/gen";
import { assertAnchors } from "./lib/anchor-validate.js";
/**
* Turbo generator: `feature`
@@ -325,6 +328,138 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void {
},
],
});
/**
* Turbo generator: `event`
*
* Mode `publish` scaffolds an event contract under
* `packages/<feature>/src/events/<event-kebab>.event.ts` (+ test) and
* re-exports it from the feature's public surface at the `// <gen:events>`
* anchor. Mode `consume` (Task 40) scaffolds a handler in the consumer
* feature.
*/
plop.setGenerator("event", {
description: "Scaffold an event contract (publish) or handler (consume)",
prompts: [
{
type: "list",
name: "mode",
message: "Mode:",
choices: ["publish", "consume"],
},
{
type: "input",
name: "feature",
message:
"Owning feature (kebab-case; for publish: contract package; for consume: consumer package):",
validate(input: string) {
if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
if (!existsSync(join(process.cwd(), "packages", input, "src"))) {
return `packages/${input}/src does not exist`;
}
return true;
},
},
{
type: "input",
name: "event",
message: "Event slug (dotted-kebab past tense, e.g. 'user.signed-up'):",
validate(input: string) {
if (!/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/.test(input)) {
return "Must be dotted-kebab past tense (e.g. user.signed-up)";
}
return true;
},
},
{
type: "input",
name: "publisher",
message: "Publisher feature (kebab-case; only for consume mode):",
when(answers: { mode: string }) {
return answers.mode === "consume";
},
validate(input: string, answers: { event: string }) {
if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
const eventKebab = answers.event.replace(/\./g, "-");
const path = join(
process.cwd(),
"packages",
input,
"src",
"events",
`${eventKebab}.event.ts`,
);
if (!existsSync(path)) {
return `Publisher contract not found: ${path}`;
}
return true;
},
},
],
actions(answers) {
const a = answers as {
mode: "publish" | "consume";
feature: string;
event: string;
publisher?: string;
};
if (a.mode === "publish") return publishActions(a);
throw new Error("consume mode is wired in Task 40");
},
});
}
function publishActions(a: {
feature: string;
event: string;
}): PlopTypes.ActionType[] {
const eventKebab = a.event.replace(/\./g, "-");
const indexPath = `packages/${a.feature}/src/index.ts`;
return [
() => {
assertAnchors(process.cwd(), indexPath, ["// <gen:events>"]);
return `Anchors verified in ${indexPath}`;
},
{
type: "add",
path: `packages/${a.feature}/src/events/${eventKebab}.event.ts`,
templateFile: "templates/event/publish/event.ts.hbs",
data: { event: a.event, feature: a.feature, eventKebab },
},
{
type: "add",
path: `packages/${a.feature}/src/events/${eventKebab}.event.test.ts`,
templateFile: "templates/event/publish/event.test.ts.hbs",
data: { event: a.event, feature: a.feature, eventKebab },
},
{
type: "modify",
path: indexPath,
pattern: /\/\/ <gen:events>/,
template: `// <gen:events>\nexport {\n {{camelCase event}}Event,\n {{camelCase event}}EventSchema,\n type {{pascalCase event}}Event,\n} from "./events/{{kebabCase event}}.event";`,
data: { event: a.event },
},
() => printPublishNextSteps(a),
];
}
function printPublishNextSteps(a: { feature: string; event: string }): string {
const eventKebab = a.event.replace(/\./g, "-");
return [
"",
"─────────────────────────────────────────────────────────────",
`Event ${a.feature}.${a.event} contract scaffolded.`,
"",
"Next steps (manual):",
` 1. Fill in the schema in packages/${a.feature}/src/events/${eventKebab}.event.ts`,
` 2. Pick a use case in packages/${a.feature}/src/application/use-cases/ that should publish.`,
` - Add 'bus: IEventBus' to the factory signature.`,
` - Call 'await bus.publish(${camel(a.event.replace(/\./g, "-"))}Event, payload)' after success.`,
` - Update the use case's DI binding to inject the bus.`,
` 3. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
"─────────────────────────────────────────────────────────────",
"",
].join("\n");
}
// Local helpers used inside the printNextSteps action — Plop's helpers aren't