import eslint from '@eslint/js'; import angular from 'angular-eslint'; import globals from 'globals'; import tseslint from 'typescript-eslint'; import kanera from './apps/web/eslint/kanera-plugin.mjs'; export default tseslint.config( { linterOptions: { reportUnusedDisableDirectives: 'warn', }, }, { ignores: [ '**/dist/**', '**/coverage/**', 'e2e/artifacts/**', '**/.angular/**', '**/node_modules/**', '**/out-tsc/**', 'apps/api/drizzle/**', 'apps/api/drizzle.config.ts', ], }, { files: ['**/*.ts'], extends: [ eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked, ], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, rules: { '@typescript-eslint/array-type': 'off', '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/no-confusing-void-expression': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-object-type': [ 'error', { allowInterfaces: 'with-single-extends' }, ], '@typescript-eslint/no-extraneous-class': [ 'error', { allowConstructorOnly: true }, ], '@typescript-eslint/no-floating-promises': [ 'warn', { ignoreVoid: true }, ], '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: false }, ], '@typescript-eslint/consistent-type-imports': [ 'warn', { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, ], '@typescript-eslint/no-deprecated': 'warn', '@typescript-eslint/no-import-type-side-effects': 'warn', '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/no-unsafe-argument': 'warn', '@typescript-eslint/no-unsafe-assignment': 'warn', '@typescript-eslint/no-unsafe-call': 'warn', '@typescript-eslint/no-unsafe-member-access': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-unused-vars': [ 'warn', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', varsIgnorePattern: '^_', }, ], '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/require-await': 'off', }, }, { files: ['apps/api/**/*.ts', 'packages/shared/**/*.ts'], languageOptions: { globals: globals.node, }, rules: { '@typescript-eslint/await-thenable': 'warn', }, }, { files: ['apps/web/**/*.ts'], extends: [...angular.configs.tsRecommended], processor: angular.processInlineTemplates, languageOptions: { globals: globals.browser, }, plugins: { kanera }, rules: { 'kanera/localize-description-colon': 'error', '@angular-eslint/component-class-suffix': [ 'error', { suffixes: ['Component', 'Page', 'Popover'] }, ], '@angular-eslint/component-selector': [ 'error', { type: 'element', prefix: 'k', style: 'kebab-case', }, ], '@angular-eslint/directive-selector': [ 'warn', { type: 'attribute', prefix: 'k', style: 'camelCase', }, ], '@angular-eslint/no-output-native': 'warn', '@angular-eslint/prefer-on-push-component-change-detection': 'error', '@angular-eslint/prefer-output-readonly': 'error', // Dates render through the named styles in apps/web/src/app/shared/date-format.ts so the same // instant reads identically on the board, in activity, in notes, and on home. 'no-restricted-syntax': [ 'error', { selector: "MemberExpression[object.name='Intl'][property.name='DateTimeFormat']", message: 'Use formatDate/formatDateTime/formatTime from shared/date-format (or viewerTimeZone from shared/day-key.util).', }, { selector: "CallExpression[callee.property.name=/^toLocale(Date|Time)?String$/]", message: 'Use formatDate/formatDateTime/formatTime from shared/date-format.', }, ], }, }, { // The formatter itself, the zone-arithmetic helper beside due dates, and the specs that compute // locale-independent expectations are the only places allowed to call Intl directly. files: [ 'apps/web/src/app/shared/date-format.ts', 'apps/web/src/app/shared/day-key.util.ts', 'apps/web/src/app/features/board/due-date.util.ts', 'apps/web/**/*.spec.ts', ], rules: { 'no-restricted-syntax': 'off' }, }, { files: ['apps/admin-web/**/*.ts'], extends: [...angular.configs.tsRecommended], processor: angular.processInlineTemplates, languageOptions: { globals: globals.browser, }, rules: { '@angular-eslint/component-class-suffix': [ 'error', { suffixes: ['Component', 'Page'] }, ], '@angular-eslint/component-selector': [ 'error', { type: 'element', prefix: 'a', style: 'kebab-case', }, ], '@angular-eslint/directive-selector': [ 'warn', { type: 'attribute', prefix: 'a', style: 'camelCase', }, ], '@angular-eslint/no-output-native': 'warn', '@angular-eslint/prefer-on-push-component-change-detection': 'error', '@angular-eslint/prefer-output-readonly': 'error', }, }, { files: ['apps/api/**/*.test.ts', 'apps/api/**/*.itest.ts'], rules: { // Injected HTTP responses and test doubles intentionally expose untyped payloads. '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', // node:test owns the promises returned by test registrations. '@typescript-eslint/no-floating-promises': 'off', }, }, { files: ['apps/web/**/*.spec.ts', 'apps/web/**/*.test.ts'], languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/unbound-method': 'off', }, }, { // Covers both .html templates and the inline templates processInlineTemplates extracts from .ts. files: ['apps/web/**/*.html'], plugins: { kanera }, rules: { 'kanera/tabler-icon-aria-hidden': 'error', }, }, { // Playwright fixtures must destructure their dependencies, so `async ({}, use) =>` is the // required signature for a fixture that depends on none. files: ['e2e/**/*.ts'], rules: { 'no-empty-pattern': 'off', }, }, { files: ['**/*.html'], extends: [...angular.configs.templateRecommended], rules: { '@angular-eslint/template/alt-text': 'off', '@angular-eslint/template/click-events-have-key-events': 'off', '@angular-eslint/template/interactive-supports-focus': 'off', '@angular-eslint/template/label-has-associated-control': 'off', '@angular-eslint/template/mouse-events-have-key-events': 'off', '@angular-eslint/template/no-autofocus': 'off', }, }, );