From 122a81b2ca173a1a1870dd31db33811197dac5fb Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 22:01:03 +0200 Subject: [PATCH] feat(core-eslint): rule no-realtime-handler-reexport Co-Authored-By: Claude Sonnet 4.6 --- packages/core-eslint/base.js | 12 ++++- .../rules/no-realtime-handler-reexport.js | 47 +++++++++++++++++++ .../no-realtime-handler-reexport.test.js | 35 ++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 packages/core-eslint/rules/no-realtime-handler-reexport.js create mode 100644 packages/core-eslint/rules/no-realtime-handler-reexport.test.js diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index 3323a7e..b4c90b3 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -5,6 +5,7 @@ import turboPlugin from "eslint-plugin-turbo"; import boundaries from "eslint-plugin-boundaries"; import globals from "globals"; import noDirectSocketIO from "./rules/no-direct-socket-io.js"; +import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js"; export default [ { ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] }, @@ -172,11 +173,20 @@ export default [ }, // R2 — `socket.io` and `socket.io-client` must not be imported outside // core-realtime/src/ and apps/*/server.ts. Use @repo/core-realtime helpers. + // R1 (ADR-016) — Realtime handlers must not be re-exported outside bind-* files. { files: ["**/*.{ts,tsx,mjs,cjs,js}"], - plugins: { "repo-rules": { rules: { "no-direct-socket-io": noDirectSocketIO } } }, + 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", }, }, ]; diff --git a/packages/core-eslint/rules/no-realtime-handler-reexport.js b/packages/core-eslint/rules/no-realtime-handler-reexport.js new file mode 100644 index 0000000..b428360 --- /dev/null +++ b/packages/core-eslint/rules/no-realtime-handler-reexport.js @@ -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/packages/core-eslint/rules/no-realtime-handler-reexport.test.js b/packages/core-eslint/rules/no-realtime-handler-reexport.test.js new file mode 100644 index 0000000..b85f161 --- /dev/null +++ b/packages/core-eslint/rules/no-realtime-handler-reexport.test.js @@ -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" }], + }, + ], +});