diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index b153d94..00fd61b 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -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//src/events/.event.ts` (+ test) and + * re-exports it from the feature's public surface at the `// ` + * 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, ["// "]); + 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: /\/\/ /, + template: `// \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