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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user