From 4308762caa0e3c638762829dc4ab00609d259d28 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Sat, 9 May 2026 13:32:46 +0200 Subject: [PATCH] feat(generators): wire realtime entry into core-package dispatch table --- turbo/generators/config.test.ts | 22 +++++++- turbo/generators/config.ts | 95 ++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/turbo/generators/config.test.ts b/turbo/generators/config.test.ts index e87ffd3..545527b 100644 --- a/turbo/generators/config.test.ts +++ b/turbo/generators/config.test.ts @@ -3,7 +3,7 @@ import type { PlopTypes } from "@turbo/gen"; import generator from "./config"; describe("core-package generator", () => { - it("is registered with an empty choices list initially", () => { + it("is registered with realtime in choices list", () => { const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = []; const plopMock = { setHelper: () => {}, @@ -15,6 +15,24 @@ describe("core-package generator", () => { expect(corePkg).toBeDefined(); const prompts = corePkg!.def.prompts as Array<{ name: string; choices: unknown[] }>; expect(prompts[0]!.name).toBe("name"); - expect(prompts[0]!.choices).toEqual([]); + expect(prompts[0]!.choices).toContain("realtime"); + }); +}); + +describe("core-package realtime", () => { + it("emits actions covering package files, transpilePackages, and ESLint rules", () => { + // Capture the actions returned by the realtime entry. + const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = []; + const plop = { + setHelper: () => {}, + setGenerator: (n: string, d: unknown) => captured.push({ name: n, def: d as PlopTypes.PlopGeneratorConfig }), + } as unknown as PlopTypes.NodePlopAPI; + generator(plop); + const corePkg = captured.find((c) => c.name === "core-package")!.def; + const actions = (corePkg.actions as (a: { name: string }) => PlopTypes.ActionType[])( + { name: "realtime" }, + ); + // Expectations: at least one assertNotPresent guard, multiple `add` actions, and a transpilePackages action. + expect(actions.length).toBeGreaterThan(20); // 28 files + extras }); }); diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index 3a4d8e3..d39195d 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -2,6 +2,14 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import type { PlopTypes } from "@turbo/gen"; import { assertAnchors } from "./lib/anchor-validate.js"; +import { + assertOptionalPackageNotPresent, + addToTranspilePackages, + splicePluginRulesAt, + splicePluginImportsAt, + addBoundariesEntry, + emitTemplateTree, +} from "./lib/core-package-utils.js"; /** * Turbo generator: `feature` @@ -498,9 +506,63 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { * name maps to a verbatim snapshot of the package captured at generator-add * time. Phases 3-6 each register one entry in CORE_PACKAGE_GENERATORS. */ + const REALTIME_RULE_IMPORTS = `import noDirectSocketIO from "./rules/no-direct-socket-io.js"; +import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";`; + + const REALTIME_RULE_BLOCK = ` { + files: ["**/*.{ts,tsx,mjs,cjs,js}"], + plugins: { + "repo-rules": { + rules: { + "no-direct-socket-io": noDirectSocketIO, + "no-realtime-handler-reexport": noRealtimeHandlerReexport, + }, + }, + }, + rules: { + "repo-rules/no-direct-socket-io": "error", + "repo-rules/no-realtime-handler-reexport": "error", + }, + },`; + const CORE_PACKAGE_GENERATORS: Record PlopTypes.ActionType[]> = { - // Phases 3-6 each insert one entry here. + realtime: () => [ + () => { + assertOptionalPackageNotPresent("core-realtime"); + return "Guard passed — packages/core-realtime does not exist yet."; + }, + ...emitTemplateTree("core-package/realtime", "packages/core-realtime"), + ...emitTemplateTree( + "core-package/realtime-eslint-rules", + "packages/core-eslint/rules", + ), + () => { + addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-realtime"); + return "Added @repo/core-realtime to transpilePackages."; + }, + () => { + addBoundariesEntry("packages/core-eslint/base.js", "packages/core-realtime", { mode: "folder" }); + return "Added core-realtime boundaries entry."; + }, + () => { + splicePluginImportsAt( + "packages/core-eslint/base.js", + "realtime-rules-imports", + REALTIME_RULE_IMPORTS, + ); + return "Added realtime rule imports to base.js."; + }, + () => { + splicePluginRulesAt( + "packages/core-eslint/base.js", + "realtime-rules", + REALTIME_RULE_BLOCK, + ); + return "Added realtime rule block to base.js."; + }, + printRealtimeNextSteps, + ], }; plop.setGenerator("core-package", { @@ -510,7 +572,7 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { type: "list", name: "name", message: "Which optional core package?", - choices: [], + choices: ["realtime"], }, ], actions: (answers) => { @@ -1005,6 +1067,35 @@ function printHandlerNextSteps(a: { ].join("\n"); } +function printRealtimeNextSteps(): string { + return [ + "─────────────────────────────────────────────────────────────", + "@repo/core-realtime scaffolded.", + "", + "Next steps (manual wiring — not generated):", + "", + " 1. pnpm install # link the new workspace package", + "", + " 2. apps/web-next/server.ts:", + ' - import { Server as IOServer } from "socket.io";', + ' - import { RealtimeHandlerRegistry, SocketIORealtimeBroadcaster, SocketIORealtimeServer } from "@repo/core-realtime";', + " - create IOServer on the http server, construct broadcaster + registry", + " - call bindAll({ realtime: broadcaster, realtimeRegistry: registry })", + " - start SocketIORealtimeServer", + "", + " 3. apps/web-next/src/server/bind-production.ts:", + ' - import { IRealtimeBroadcaster, IRealtimeHandlerRegistry, InMemoryRealtimeBroadcaster, RealtimeHandlerRegistry } from "@repo/core-realtime";', + " - add realtime + realtimeRegistry to BindAllDeps", + " - update BindProductionContext generic args", + " - add maybeRegisterRealtimePing + bindRealtimeBridge functions", + "", + " 4. Add @repo/core-realtime to each feature package.json that uses realtime", + "", + " 5. pnpm typecheck && pnpm lint && pnpm test", + "─────────────────────────────────────────────────────────────", + ].join("\n"); +} + // Local helpers used inside the printNextSteps action — Plop's helpers aren't // available outside template strings, so we replicate the bits we need. function cap(input: string): string {