From a7f0a13d342b53bdad5c17421e60d84200fb570e Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Sat, 9 May 2026 13:28:54 +0200 Subject: [PATCH] feat(generators): capture realtime ESLint rules as template files --- .../no-direct-socket-io.js.hbs | 34 ++++++++++++++ .../no-direct-socket-io.test.js.hbs | 33 +++++++++++++ .../no-realtime-handler-reexport.js.hbs | 47 +++++++++++++++++++ .../no-realtime-handler-reexport.test.js.hbs | 35 ++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs create mode 100644 turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs create mode 100644 turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs create mode 100644 turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs diff --git a/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs b/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs new file mode 100644 index 0000000..63448a6 --- /dev/null +++ b/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs @@ -0,0 +1,34 @@ +// packages/core-eslint/rules/no-direct-socket-io.js +const ALLOWED = [ + /\/packages\/core-realtime\/src\//, + /\/apps\/[^/]+\/server\.ts$/, + /\/apps\/[^/]+\/src\/.*\.test\.ts$/, +]; + +export default { + meta: { + type: "problem", + docs: { description: "Block direct socket.io imports outside core-realtime + app servers" }, + messages: { + noDirectSocketIO: 'Import from "@repo/core-realtime" instead of "socket.io". Direct imports allowed only in packages/core-realtime/src/ and apps/*/server.ts.', + noDirectSocketIOClient: 'Use the realtime helpers from "@repo/core-realtime" / "@repo/core-testing/instrumentation" instead of "socket.io-client".', + }, + schema: [], + }, + create(context) { + const filename = context.filename ?? context.getFilename(); + const allowed = ALLOWED.some((re) => re.test(filename)); + if (allowed) return {}; + + return { + ImportDeclaration(node) { + const source = node.source.value; + if (source === "socket.io") { + context.report({ node, messageId: "noDirectSocketIO" }); + } else if (source === "socket.io-client") { + context.report({ node, messageId: "noDirectSocketIOClient" }); + } + }, + }; + }, +}; diff --git a/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs b/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs new file mode 100644 index 0000000..bf74953 --- /dev/null +++ b/turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs @@ -0,0 +1,33 @@ +// packages/core-eslint/rules/no-direct-socket-io.test.js +import { RuleTester } from "eslint"; +import rule from "./no-direct-socket-io.js"; + +const tester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "module" }, +}); + +tester.run("no-direct-socket-io", rule, { + valid: [ + // Allowed inside core-realtime + { code: 'import { Server } from "socket.io";', filename: "/repo/packages/core-realtime/src/socket-io-realtime-server.ts" }, + // Allowed in app servers + { code: 'import { Server } from "socket.io";', filename: "/repo/apps/web-next/server.ts" }, + // Allowed in app integration tests (e.g. realtime-ping e2e) + { code: 'import { Server } from "socket.io";', filename: "/repo/apps/web-next/src/__tests__/realtime-ping.test.ts" }, + { code: 'import { io } from "socket.io-client";', filename: "/repo/apps/web-next/src/__tests__/realtime-ping.test.ts" }, + // Allowed elsewhere when not importing socket.io + { code: 'import { foo } from "bar";', filename: "/repo/packages/blog/src/foo.ts" }, + ], + invalid: [ + { + code: 'import { Server } from "socket.io";', + filename: "/repo/packages/blog/src/foo.ts", + errors: [{ messageId: "noDirectSocketIO" }], + }, + { + code: 'import { io } from "socket.io-client";', + filename: "/repo/packages/blog/src/ui/Component.tsx", + errors: [{ messageId: "noDirectSocketIOClient" }], + }, + ], +}); diff --git a/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs b/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs new file mode 100644 index 0000000..b428360 --- /dev/null +++ b/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs @@ -0,0 +1,47 @@ +// packages/core-eslint/rules/no-realtime-handler-reexport.js +// R1 — Realtime handlers are private. A feature's realtime/handlers/*.handler.ts +// must only be wired in the feature's own bind-production / bind-dev-seed files. +// They must never be re-exported from barrel files or other public surfaces. + +const BIND_FILE = /\bdi\/bind-(?:production|dev-seed)\b/; +const REALTIME_HANDLERS_IN_SOURCE = /\/realtime\/handlers\//; +const HANDLERS_IN_SOURCE = /\/handlers\//; +const REALTIME_IN_FILENAME = /\/realtime\//; + +export default { + meta: { + type: "problem", + docs: { + description: + "Block re-exports of realtime/handlers/** outside feature bind-* files (ADR-016 R1)", + }, + messages: { + noRealtimeHandlerReexport: + "Realtime handlers (realtime/handlers/*.handler.ts) must not be re-exported (ADR-016 R1). " + + "Wire them only inside the feature's own bind-production / bind-dev-seed files.", + }, + schema: [], + }, + create(context) { + const filename = context.filename ?? context.getFilename(); + + // Bind-* files are the only allowed place for these exports/imports + if (BIND_FILE.test(filename)) return {}; + + function checkExportSource(node) { + if (!node.source) return; + const source = node.source.value; + const isRealtimeHandler = + REALTIME_HANDLERS_IN_SOURCE.test(source) || + (HANDLERS_IN_SOURCE.test(source) && REALTIME_IN_FILENAME.test(filename)); + if (isRealtimeHandler) { + context.report({ node, messageId: "noRealtimeHandlerReexport" }); + } + } + + return { + ExportNamedDeclaration: checkExportSource, + ExportAllDeclaration: checkExportSource, + }; + }, +}; diff --git a/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs b/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs new file mode 100644 index 0000000..b85f161 --- /dev/null +++ b/turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs @@ -0,0 +1,35 @@ +// packages/core-eslint/rules/no-realtime-handler-reexport.test.js +import { RuleTester } from "eslint"; +import rule from "./no-realtime-handler-reexport.js"; + +const tester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "module" }, +}); + +tester.run("no-realtime-handler-reexport", rule, { + valid: [ + // Importing a handler from inside a feature's bind-* file is allowed. + { + code: 'import { onPingHandler } from "../realtime/handlers/on-ping.handler";', + filename: "/repo/packages/blog/src/di/bind-production.ts", + }, + // Re-exporting a channel descriptor is allowed. + { + code: 'export { presenceChannel } from "./realtime/presence.channel";', + filename: "/repo/packages/blog/src/index.ts", + }, + ], + invalid: [ + // Re-exporting a handler from any non-bind file is forbidden. + { + code: 'export { onPingHandler } from "./realtime/handlers/on-ping.handler";', + filename: "/repo/packages/blog/src/index.ts", + errors: [{ messageId: "noRealtimeHandlerReexport" }], + }, + { + code: 'export * from "./handlers/on-ping.handler";', + filename: "/repo/packages/blog/src/realtime/index.ts", + errors: [{ messageId: "noRealtimeHandlerReexport" }], + }, + ], +});