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 type { PlopTypes } from "@turbo/gen";
|
||||||
|
import { assertAnchors } from "./lib/anchor-validate.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turbo generator: `feature`
|
* 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
|
// Local helpers used inside the printNextSteps action — Plop's helpers aren't
|
||||||
|
|||||||
Reference in New Issue
Block a user