// ESLint v9 flat config. Mirrors the rule set from // docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md (Task 4). import js from "@eslint/js"; import tseslint from "typescript-eslint"; import unusedImports from "eslint-plugin-unused-imports"; import boundaries from "eslint-plugin-boundaries"; export default [ { ignores: [ "dist/**", "node_modules/**", "ClientApp/**", "wwwroot/**", "**/*.cjs", "pnpm-lock.yaml", ], }, js.configs.recommended, ...tseslint.configs.recommended, { files: ["src/**/*.{ts,tsx}"], languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: 2023, sourceType: "module", project: "./tsconfig.json", ecmaFeatures: { jsx: true }, }, }, plugins: { "unused-imports": unusedImports, }, rules: { "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ "warn", { vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_", }, ], "@typescript-eslint/consistent-type-imports": [ "error", { prefer: "type-imports" }, ], "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-non-null-assertion": "warn", "no-console": ["warn", { allow: ["warn", "error"] }], }, }, { files: ["src/**/*.{ts,tsx}"], plugins: { boundaries, }, settings: { "import/resolver": { node: { extensions: [".ts", ".tsx", ".js", ".jsx"], }, }, "boundaries/elements": [ { type: "routes", pattern: "src/routes/*" }, { type: "mf", pattern: "src/mf/*" }, { type: "features", pattern: "src/features/*", capture: ["feature"] }, { type: "ui", pattern: "src/ui/*" }, { type: "shared", pattern: "src/shared/*" }, { type: "observability", pattern: "src/observability/*" }, { type: "i18n", pattern: "src/i18n/*" }, { type: "env", pattern: "src/env/*" }, ], }, rules: { // Design spec §1.2 layered dependency direction: // features/ cannot import routes/ or mf/ // ui/ cannot import features/ // shared/ cannot import features/, routes/, mf/, observability/ // observability/ cannot import features/, routes/, mf/ "boundaries/element-types": [ "error", { default: "allow", rules: [ { from: "features", disallow: ["routes", "mf"], message: "Features must not import from routes/ or mf/. Use the HostContract or a shared module instead.", }, { from: "ui", disallow: ["features", "routes", "mf"], message: "UI layer must not import from features/, routes/, or mf/. UI is consumed by features, not the other way around.", }, { from: "shared", disallow: ["features", "routes", "mf", "observability"], message: "Shared modules must not import from features/, routes/, mf/, or observability/.", }, { from: "observability", disallow: ["features", "routes", "mf"], message: "Observability modules must not import from features/, routes/, or mf/.", }, ], }, ], }, }, // --- Restricted imports (master plan §1A-3) --- // OTel SDK internals + react-i18next: merged into one block so ESLint 9 // flat config doesn't override one rule with the other. // otel.ts is allowed to import OTel SDK; provider.tsx is allowed to import react-i18next. // Neither file needs the other's exemption, so combining ignores is safe. { files: ["src/**/*.{ts,tsx}"], ignores: ["src/observability/metrics/otel.ts", "src/observability/metrics/otel.test.ts", "src/i18n/provider.tsx"], rules: { "no-restricted-imports": [ "error", { paths: [ { name: "@opentelemetry/sdk-metrics", message: "Import from @opentelemetry/api or use getMeter() from @/observability/metrics/otel instead. Direct SDK access is restricted to src/observability/metrics/otel.ts.", }, { name: "@opentelemetry/sdk-node", message: "Import from @opentelemetry/api or use getMeter()/getTracer() from @/observability/metrics/otel instead. Direct SDK access is restricted to src/observability/metrics/otel.ts.", }, { name: "react-i18next", message: "Import useTranslation from @/i18n/provider instead. Direct react-i18next imports are restricted to src/i18n/provider.tsx.", }, ], }, ], }, }, // @microsoft/signalr: forbidden in files that run during SSR. // SSR-bundle = routes/ and server/ directories. Features + shared/hooks are client-side. { files: ["src/routes/**/*.{ts,tsx}", "src/server/**/*.{ts,tsx}"], rules: { "no-restricted-imports": [ "error", { paths: [ { name: "@microsoft/signalr", message: "SignalR must not be imported in SSR-bundle files (routes/, server/). Use dynamic import in a useEffect or a client-only wrapper.", }, ], }, ], }, }, // window.localStorage / window.sessionStorage: only src/shared/storage.ts { files: ["src/**/*.{ts,tsx}"], ignores: ["src/shared/storage.ts", "src/shared/storage.test.ts"], rules: { "no-restricted-globals": [ "error", { name: "localStorage", message: "Use the storage module from @/shared/storage instead. Direct localStorage access is restricted to src/shared/storage.ts.", }, { name: "sessionStorage", message: "Use the storage module from @/shared/storage instead. Direct sessionStorage access is restricted to src/shared/storage.ts.", }, ], }, }, // Test files get a looser ruleset: non-null assertions are idiomatic // when fixtures guarantee presence (`arr[0]!` after an explicit length // check). Production code keeps the warning; tests don't. { files: [ "src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}", ], rules: { "@typescript-eslint/no-non-null-assertion": "off", }, }, ];