feat(core-eslint): rule no-realtime-handler-reexport

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 22:01:03 +02:00
parent 77467de702
commit 122a81b2ca
3 changed files with 93 additions and 1 deletions

View File

@@ -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",
},
},
];

View File

@@ -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,
};
},
};

View File

@@ -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" }],
},
],
});