import js from "@eslint/js"; import eslintConfigPrettier from "eslint-config-prettier"; 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"; import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js"; export default [ { ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] }, js.configs.recommended, { files: ["**/*.{mjs,cjs,js}", "**/*.config.{ts,tsx}"], languageOptions: { globals: { ...globals.node }, }, }, ...tseslint.configs.recommended, eslintConfigPrettier, { plugins: { turbo: turboPlugin }, rules: { "turbo/no-undeclared-env-vars": "warn", }, }, { rules: { // Honour the leading-underscore convention for intentionally-unused params/vars. "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", }, ], }, }, { plugins: { boundaries }, settings: { "boundaries/elements": [ { type: "app", pattern: "apps/*" }, { type: "tooling", pattern: "packages/core-eslint" }, { type: "tooling", pattern: "packages/core-typescript" }, { type: "tooling", pattern: "packages/core-testing" }, { type: "core-composition", pattern: "packages/core-api" }, { type: "core-composition", pattern: "packages/core-cms" }, // Explicit entry placed before the catch-all so its `mode: "folder"` // is preferred — needed for boundaries-plugin to resolve element root // by directory rather than by package.json name. { type: "core", pattern: "packages/core-realtime", mode: "folder" }, { type: "core", pattern: "packages/core-*" }, { type: "feature", pattern: "packages/!(core-*)" }, ], }, rules: { "boundaries/element-types": [ 2, { default: "disallow", rules: [ { from: "app", allow: ["app", "core", "core-composition", "feature", "tooling"] }, { from: "feature", allow: ["core", "tooling"] }, { from: "core", allow: ["core", "tooling"] }, { from: "core-composition", allow: ["core", "feature", "tooling"] }, { from: "tooling", allow: ["tooling"] }, ], }, ], }, }, // R40 — block direct @sentry/* imports outside the allowlisted instrumentation paths { files: ["**/*.{ts,tsx,mjs,cjs,js}"], rules: { "no-restricted-imports": [ "error", { patterns: [ { group: ["@sentry/*"], message: "Import from @repo/core-shared/instrumentation instead — feature packages must not depend on Sentry directly (R40).", }, ], }, ], }, }, // R40 allowlist — the only paths permitted to import @sentry/*. // Patterns are double-star prefixed so they match whether eslint runs from // the repo root or from inside a sub-package. { files: [ "**/instrumentation/sentry/**", "**/instrumentation/di/bind-sentry-instrumentation.{ts,js}", "**/instrumentation/di/bind-sentry-instrumentation.test.{ts,js}", "**/setup/no-sentry.{ts,js}", "**/setup/no-sentry.test.{ts,js}", "**/instrumentation.{ts,js,mjs}", "**/instrumentation-client.{ts,js,mjs}", "**/next.config.{mjs,ts,js}", "**/vite.config.{ts,mjs,js}", "**/sentry.*.config.{ts,mjs,js}", ], rules: { "no-restricted-imports": "off", }, }, // E1 — Event handlers must not be re-exported. Wire them only inside the // consumer feature's bind-production / bind-dev-seed (spec § 2.2 Rule E1). // J — Direct `payload.jobs.*` access is forbidden outside the integration // layer. Use IJobQueue (from @repo/core-shared/jobs) instead. { files: ["**/*.{ts,tsx,mjs,cjs,js}"], rules: { "no-restricted-syntax": [ "error", { selector: "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]", message: "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", }, { selector: "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]", message: "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", }, { selector: "MemberExpression[object.type='MemberExpression'][object.object.type='Identifier'][object.object.name='payload'][object.property.type='Identifier'][object.property.name='jobs']", message: "Direct `payload.jobs.*` access is not allowed here. Use IJobQueue (from @repo/core-shared/jobs) instead. Allowed only in **/integrations/cms/jobs/** and **/core-shared/src/jobs/**.", }, ], }, }, // J — `payload.jobs.*` is allowed only in the integration layer. // In these paths, no-restricted-syntax is narrowed to keep E1 active but // drop the payload.jobs check. // Note: "**/core-shared/src/jobs/**" does not match from within a package-local // ESLint run because ESLint resolves globs relative to the config file location. // The pattern is kept for documentation; in practice, the PayloadJobQueue class // uses `this.payload.jobs.*` which the selector already ignores (it only catches // bare `payload.jobs.*`). Any new file added there that does use bare `payload.jobs.*` // would need this allowlist to be expressed as "**/jobs/payload-*" or similar. { files: [ "**/integrations/cms/jobs/**", "**/core-shared/src/jobs/**", ], rules: { "no-restricted-syntax": [ "error", { selector: "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]", message: "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", }, { selector: "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]", message: "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", }, ], }, }, // 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, "no-realtime-handler-reexport": noRealtimeHandlerReexport, }, }, }, rules: { "repo-rules/no-direct-socket-io": "error", "repo-rules/no-realtime-handler-reexport": "error", }, }, ];