import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import pluginVue from 'eslint-plugin-vue'; import vueTsEslintConfig from '@vue/eslint-config-typescript'; import tsParser from '@typescript-eslint/parser'; import stylistic from '@stylistic/eslint-plugin'; import prettier from 'eslint-config-prettier'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export default [ { name: 'app/files-to-lint', files: ['**/*.{ts,mts,tsx,vue}'], languageOptions: { parser: tsParser, parserOptions: { projectService: { defaultProject: resolve(__dirname, 'tsconfig.app.json'), }, tsconfigRootDir: __dirname, sourceType: 'module', ecmaVersion: 'latest', allowDefaultProject: true, }, }, plugins: { '@stylistic': stylistic, }, }, { name: 'app/files-to-ignore', ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/node_modules/**', '**/.ponder/**', '**/__tests__/**'], }, ...pluginVue.configs['flat/essential'], ...vueTsEslintConfig(), { name: 'app/custom-rules', rules: { // TypeScript rules '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', }, ], // General code quality 'no-console': 'error', '@stylistic/indent': ['error', 2, { SwitchCase: 1 }], 'max-len': ['error', { code: 140 }], // Vue specific rules 'vue/multi-word-component-names': 'error', 'vue/component-name-in-template-casing': ['error', 'PascalCase'], 'vue/require-prop-types': 'error', 'vue/require-default-prop': 'error', 'vue/html-indent': ['error', 2], 'vue/no-unused-vars': 'error', // Disable rules that conflict with Prettier 'vue/max-attributes-per-line': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/html-self-closing': 'off', // Naming conventions camelcase: ['error', { properties: 'never', ignoreDestructuring: true, allow: ['^UNSAFE_'] }], // Complexity rules (disabled as per requirements) complexity: 'off', 'max-depth': 'off', 'max-lines': 'off', 'max-params': 'off', }, }, { name: 'arch/graphql-no-interpolation', rules: { 'no-restricted-syntax': [ 'error', { selector: "Property[key.name='query'] > TemplateLiteral[expressions.length>0], Property[key.name='mutation'] > TemplateLiteral[expressions.length>0]", message: 'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.', }, { selector: "CallExpression[callee.property.name='waitForTimeout']", message: '[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.', }, { selector: "NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']", message: '[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.', }, ], }, }, { name: 'app/tests-override', files: ['src/**/__tests__/**/*.ts', 'src/**/__tests__/**/*.tsx'], languageOptions: { parserOptions: { projectService: false, project: undefined, }, }, }, prettier, ];