diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index 9d4ffb3..3323a7e 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -4,6 +4,7 @@ import tseslint from "typescript-eslint"; 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"; export default [ { ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] }, @@ -169,4 +170,13 @@ 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. + { + files: ["**/*.{ts,tsx,mjs,cjs,js}"], + plugins: { "repo-rules": { rules: { "no-direct-socket-io": noDirectSocketIO } } }, + rules: { + "repo-rules/no-direct-socket-io": "error", + }, + }, ]; diff --git a/packages/core-eslint/rules/no-direct-socket-io.js b/packages/core-eslint/rules/no-direct-socket-io.js new file mode 100644 index 0000000..6e854f5 --- /dev/null +++ b/packages/core-eslint/rules/no-direct-socket-io.js @@ -0,0 +1,33 @@ +// packages/core-eslint/rules/no-direct-socket-io.js +const ALLOWED = [ + /\/packages\/core-realtime\/src\//, + /\/apps\/[^/]+\/server\.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/packages/core-eslint/rules/no-direct-socket-io.test.js b/packages/core-eslint/rules/no-direct-socket-io.test.js new file mode 100644 index 0000000..2837fce --- /dev/null +++ b/packages/core-eslint/rules/no-direct-socket-io.test.js @@ -0,0 +1,30 @@ +// 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 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/packages/core-eslint/vitest.config.js b/packages/core-eslint/vitest.config.js index f04b5e5..dfc2a86 100644 --- a/packages/core-eslint/vitest.config.js +++ b/packages/core-eslint/vitest.config.js @@ -4,6 +4,6 @@ export default defineConfig({ test: { globals: true, environment: "node", - include: ["*.test.js"], + include: ["*.test.js", "rules/*.test.js"], }, });