feat(core-realtime): scaffold realtime optional core

Generator-emitted scaffold (pnpm turbo gen core-package realtime) plus
the story-00-precedent coverage repairs (coverage provider devDep,
symbols.ts exclude + tested allowlist mirror) and three minimal tests
covering generator-emitted realtime code the template suite misses.
Squash of 31d85e0 + review-fix cf11b38.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 20:35:09 +02:00
parent fc13424e9d
commit e50306cbf6
41 changed files with 1913 additions and 3 deletions

View File

@@ -8,6 +8,8 @@ import conformancePlugin from "./plugin.js";
import path from "node:path";
import { fileURLToPath } from "node:url";
// <gen:realtime-rules-imports>
import noDirectSocketIO from "./rules/no-direct-socket-io.js";
import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..", "..");
@@ -82,6 +84,8 @@ export default [
{ type: "tooling", pattern: "packages/core-testing" },
{ type: "core-composition", pattern: "packages/core-api" },
{ type: "core-composition", pattern: "packages/core-cms" },
{ type: "core", pattern: "packages/core-realtime", mode: "folder" },
{ type: "core", pattern: "packages/core-*" },
{ type: "feature", pattern: "packages/!(core-*)" },
],
@@ -245,4 +249,19 @@ export default [
// no-realtime-handler-reexport) are added here when @repo/core-realtime is
// scaffolded via `pnpm turbo gen core-package realtime`.
// <gen:realtime-rules>
{
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",
},
},
];

View File

@@ -0,0 +1,39 @@
// 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" });
}
},
};
},
};

View File

@@ -0,0 +1,48 @@
// 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" }],
},
],
});

View File

@@ -0,0 +1,48 @@
// packages/core-eslint/rules/no-realtime-handler-reexport.js
// 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" }],
},
],
});