// ESLint 10 flat config. Scope is deliberately small: this project targets zero
// client JavaScript (CLAUDE.md, AGENTS.md §7), so the only JS/TS here is build
// configuration, site data, and the occasional island. Rules exist to catch
// mistakes, not to impose style — Prettier owns formatting.
//
// `typescript-eslint` is here because .astro frontmatter IS TypeScript, so the
// plugin cannot parse a single component without it. It runs unconfigured for
// type-awareness on purpose: `astro check` already does the type checking, and
// duplicating it here would be slower and would disagree at the edges.
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import astro from 'eslint-plugin-astro';
export default [
{ ignores: ['dist/**', 'node_modules/**', '.astro/**', 'docs/reference/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
...astro.configs.recommended,
...astro.configs['flat/jsx-a11y-recommended'],
// `no-undef` off for TYPESCRIPT ONLY, on typescript-eslint's own advice: it
// has no type information, so every ambient global is a false positive —
// Astro declares `ImageMetadata`, `astroHTML.JSX` and friends globally, and
// .astro frontmatter IS TypeScript. tsc catches a real undefined reference,
// which is what `npm run check` is for.
//
// NOT applied to .js/.mjs. `tsconfig.json` sets `allowJs` without `checkJs`,
// so plain JS is not type-checked by anything — turning the rule off there
// meant a typo like `procss.env.X` in astro.config.mjs passed lint silently.
{
files: ['**/*.ts', '**/*.astro'],
rules: { 'no-undef': 'off' },
},
{
files: ['**/*.{js,mjs,ts}', '**/*.astro'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: { ...globals.browser, ...globals.node },
},
rules: {
// A stray console.log in a static build is dead weight shipped to nobody.
'no-console': ['warn', { allow: ['warn', 'error'] }],
// `role="list"` on a
is redundant to a spec reader and load-bearing
// in a browser: Safari drops list semantics from any list styled
// `list-style: none`, so VoiceOver stops announcing "list, 6 items".
// src/styles/global.css keys its own reset off `ul[role='list']` for
// exactly this reason. The rule is right in general; this is the one
// documented exception, and it is scoped to that single pairing.
'astro/jsx-a11y/no-redundant-roles': [
'error',
{ ul: ['list'], ol: ['list'] },
],
eqeqeq: ['error', 'always'],
'prefer-const': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
},
},
// `scripts/` ARE CLI TOOLS, AND PRINTING IS THEIR OUTPUT. The `no-console`
// rule above is justified in this config as "a stray console.log in a static
// build is dead weight shipped to nobody" — which is a statement about the
// shipped bundle, and nothing in `scripts/` reaches it. `check-claims.mjs`
// exists to print what it matched: CLAUDE.md's rule is that a grep is not a
// finding until you read what it matched, so suppressing its output would
// defeat the tool. Scoped to this directory rather than disabled globally.
//
// ⚠️ IT MUST SIT AFTER THE BLOCK IT OVERRIDES. Flat config applies matching
// blocks in order, last one wins — placed above, this had no effect at all
// and `npm run lint` still reported all six warnings. Measured, not assumed.
{
files: ['scripts/**/*.{js,mjs}'],
rules: { 'no-console': 'off' },
},
/* `infra/cloudfront/` IS NOT A NODE MODULE AND NOT A BROWSER SCRIPT. A
CloudFront Function's entry point is a bare `function handler(event)` that
the runtime calls **by name** — it has no `export` (the runtime rejects
module syntax) and nothing in the file references it, so
`no-unused-vars` fires on the one declaration that is the whole point of
the file. `argsIgnorePattern` cannot reach a function declaration, so the
rule is scoped off here rather than silenced with a comment at the
declaration, which would read as though the name were incidental.
The test beside it is a CLI tool and prints, exactly as `scripts/` does.
⚠️ LIKE THE BLOCK ABOVE, THIS MUST STAY LAST. Flat config applies matching
blocks in order and the last one wins. */
{
files: ['infra/cloudfront/**/*.{js,mjs}'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'off',
},
},
];