mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
<!-- ccr-slack-attribution --> _Requested by **Charis Lam** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1790599888647059?thread_ts=1790599888.647059&cid=C0161K73J1J)_ ## Problem The Studio ESLint "ratchet" (`apps/studio/scripts/ratchet-eslint-rules.ts`, baseline in `apps/studio/.github/eslint-rule-baselines.json`, tracked rules in `apps/studio/scripts/ratchet-rules.json`) lets certain rules stay at `warn` severity while CI blocks the *count* of violations from increasing. Several of those tracked rules had already reached a baseline of 0 allowed violations, meaning there's nothing left to ratchet — they should be enforced directly instead of tracked indirectly. ## Solution **Before:** `no-restricted-imports`, `jsx-a11y/aria-props`, `jsx-a11y/aria-proptypes`, `jsx-a11y/role-supports-aria-props`, `jsx-a11y/anchor-has-content`, `jsx-a11y/aria-role`, `jsx-a11y/no-aria-hidden-on-focusable`, `jsx-a11y/tabindex-no-positive`, `jsx-a11y/no-distracting-elements`, and `react-hook-form/no-use-watch` were all tracked in the ratchet baseline with a count of 0, and (apart from `no-restricted-imports`, see below) configured as ESLint `warn` in `apps/studio/eslint.config.cjs`. **After:** each of those rules is removed from `apps/studio/.github/eslint-rule-baselines.json` (both the `rules` count and the now-empty `ruleFiles` entry) and from `apps/studio/scripts/ratchet-rules.json`. Their severity in `apps/studio/eslint.config.cjs` is bumped from `warn` to `error` so they're enforced directly by lint going forward instead of being tracked via the ratchet. `no-restricted-imports` was a special case: a later config block in `apps/studio/eslint.config.cjs` already overrides the shared `warn` default with `error` (confirmed via `eslint --print-config`), so only the ratchet bookkeeping needed removing for that rule — no severity change was needed. Promoting `jsx-a11y/role-supports-aria-props` to `error` surfaced one real violation that the ratchet's non-test-file filter had been hiding: a mock `<button>` in `LocalDropdown.test.tsx` set `aria-checked`, which that role doesn't support. Removed the unused `aria-checked` attribute from the mock (it wasn't asserted on by any test). Every other rule still tracked by the ratchet (e.g. `@typescript-eslint/no-explicit-any`, `react-hooks/exhaustive-deps`, `no-restricted-exports`, …) has a baseline above 0 and was left untouched. ### How verified - `pnpm --filter studio run lint:ratchet` → `Stable: No regressions for selected rules.` - `pnpm --filter studio run lint` → `0 errors, 2430 warnings` (no new errors from the severity bumps) - `npx vitest run components/interfaces/LocalDropdown.test.tsx` → 3/3 passing after the mock fix - `npx prettier --check` on all touched files → clean - `npx tsc --noEmit` shows one pre-existing, unrelated error in `packages/ui-patterns` (reproduced identically on `master` before this change) ## Review instructions 1. Confirm `apps/studio/.github/eslint-rule-baselines.json` and `apps/studio/scripts/ratchet-rules.json` no longer list the 10 rules named above. 2. Confirm those same rules (except `no-restricted-imports`, already `error`) are now `'error'` in `apps/studio/eslint.config.cjs`. 3. Run `pnpm --filter studio run lint:ratchet` and `pnpm --filter studio run lint` locally to confirm both pass. ## Checklist Check all before review: - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [ ] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LZThbcWV5U1r5cvUDKPVQP --- _Generated by [Claude Code](https://claude.ai/code/session_01LZThbcWV5U1r5cvUDKPVQP)_ Co-authored-by: Claude <noreply@anthropic.com>
129 lines
5.5 KiB
JavaScript
129 lines
5.5 KiB
JavaScript
const { defineConfig } = require('eslint/config')
|
|
const { fixupPluginRules } = require('@eslint/compat')
|
|
const barrelFiles = require('eslint-plugin-barrel-files')
|
|
const valtio = require('eslint-plugin-valtio')
|
|
// eslint-plugin-react-hook-form@0.3.1 (latest) still calls the ESLint 8
|
|
// `context.getScope()`, which ESLint 9 removed. fixupPluginRules shims the
|
|
// deprecated context methods so the rules run under flat config.
|
|
const reactHookForm = require('eslint-plugin-react-hook-form')
|
|
const supabaseConfig = require('eslint-config-supabase/next')
|
|
|
|
// Analytics SQL wire boundary — see the block below for context. Shared so the
|
|
// API/route block can re-include it (flat config replaces, not merges, a rule's
|
|
// options when blocks overlap, so the later block must carry these forward).
|
|
const ANALYTICS_SQL_RESTRICTED_SYNTAX = [
|
|
{
|
|
selector:
|
|
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all']",
|
|
message:
|
|
'Do not call the analytics logs.all endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
|
},
|
|
{
|
|
selector:
|
|
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all.otel']",
|
|
message:
|
|
'Do not call the analytics logs.all.otel endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
|
},
|
|
]
|
|
|
|
// Ban constructing a Supabase client at module scope in API route files. The
|
|
// TanStack server imports the entire route tree at boot (loadEntries), so a
|
|
// module-scope createClient with an env var that's unset in that environment
|
|
// (e.g. SUPABASE_URL on platform) throws on import and 500s every route — a
|
|
// runtime-only failure that's painful to catch. Construct it lazily inside the
|
|
// handler instead (see lib/api/self-hosted-admin.ts).
|
|
const NO_MODULE_SCOPE_CREATE_CLIENT = {
|
|
selector:
|
|
":matches(Program, ExportNamedDeclaration) > VariableDeclaration > VariableDeclarator > CallExpression[callee.name='createClient']",
|
|
message:
|
|
'Do not construct a Supabase client at module scope in API route files — the TanStack server evaluates every route module at boot, so a missing env var (e.g. SUPABASE_URL on platform) crashes every route. Construct it lazily inside the handler (see lib/api/self-hosted-admin.ts).',
|
|
}
|
|
|
|
module.exports = defineConfig([
|
|
{ files: ['**/*.ts', '**/*.tsx'] },
|
|
supabaseConfig,
|
|
{
|
|
files: ['components/ui/PartnerIcon.tsx'],
|
|
rules: { 'shadcn/no-raw-colors': 'off' },
|
|
},
|
|
{
|
|
files: ['**/*.{js,jsx,mjs,ts,tsx,mts,cts}'],
|
|
plugins: {
|
|
'barrel-files': barrelFiles,
|
|
valtio,
|
|
'react-hook-form': fixupPluginRules(reactHookForm),
|
|
},
|
|
rules: {
|
|
'@next/next/no-img-element': 'off',
|
|
'react/no-unescaped-entities': 'off',
|
|
'react/display-name': 'warn',
|
|
'react/no-unstable-nested-components': 'warn',
|
|
'react/jsx-key': 'error',
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: 'components/ui/DataTable/DataTableColumn/DataTableColumnHeader',
|
|
message: 'Use TanStackTableHeadSort from ui-patterns/Table instead.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'barrel-files/avoid-re-export-all': 'error',
|
|
'jsx-a11y/alt-text': 'warn',
|
|
'jsx-a11y/role-has-required-aria-props': 'error',
|
|
'jsx-a11y/aria-props': 'error',
|
|
'jsx-a11y/aria-proptypes': 'error',
|
|
'jsx-a11y/role-supports-aria-props': 'error',
|
|
'jsx-a11y/anchor-has-content': 'error',
|
|
'jsx-a11y/control-has-associated-label': [
|
|
'warn',
|
|
{ controlComponents: ['Button', 'Switch'] },
|
|
],
|
|
'jsx-a11y/label-has-associated-control': [
|
|
'warn',
|
|
{ labelComponents: ['Label'], controlComponents: ['Input', 'Switch'] },
|
|
],
|
|
'jsx-a11y/aria-role': 'error',
|
|
'jsx-a11y/no-redundant-roles': 'warn',
|
|
'jsx-a11y/no-aria-hidden-on-focusable': 'error',
|
|
'jsx-a11y/tabindex-no-positive': 'error',
|
|
'jsx-a11y/anchor-is-valid': 'warn',
|
|
'jsx-a11y/heading-has-content': 'warn',
|
|
'jsx-a11y/no-distracting-elements': 'error',
|
|
'valtio/state-snapshot-rule': 'warn',
|
|
'valtio/avoid-this-in-proxy': 'error',
|
|
'react-hook-form/destructuring-formstate': 'error',
|
|
'react-hook-form/no-access-control': 'error',
|
|
'react-hook-form/no-nested-object-setvalue': 'error',
|
|
'react-hook-form/no-use-watch': 'error',
|
|
},
|
|
},
|
|
// Analytics SQL wire boundary: every call to a SQL-bearing analytics
|
|
// endpoint (`logs.all` / `logs.all.otel`) must go through
|
|
// `executeAnalyticsSql` so the `SafeLogSqlFragment` brand is enforced at the
|
|
// type level. See .agents/skills/safe-sql-execution/SKILL.md.
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx'],
|
|
ignores: ['data/logs/execute-analytics-sql.ts'],
|
|
rules: {
|
|
'no-restricted-syntax': ['error', ...ANALYTICS_SQL_RESTRICTED_SYNTAX],
|
|
},
|
|
},
|
|
// API route modules are eagerly imported by the TanStack server at boot, so
|
|
// module-scope side effects there are especially dangerous. This block also
|
|
// re-includes the analytics selectors because flat config replaces (not
|
|
// merges) a rule's options for overlapping files.
|
|
{
|
|
files: ['pages/api/**/*.ts', 'pages/api/**/*.tsx', 'routes/**/*.ts', 'routes/**/*.tsx'],
|
|
rules: {
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
...ANALYTICS_SQL_RESTRICTED_SYNTAX,
|
|
NO_MODULE_SCOPE_CREATE_CLIENT,
|
|
],
|
|
},
|
|
},
|
|
])
|