eslint.config.mjs
Rama main · réplica de lectura del repositorio original.
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'
import nextTypescript from 'eslint-config-next/typescript'
import prettier from 'eslint-config-prettier'
/**
* Imported directly, not through `FlatCompat`.
*
* eslint-config-next ships flat config from v16 on, and the compatibility
* bridge chokes on it -- it tries to validate a flat array against the old
* schema and dies inside its own error formatter, which is a stack trace rather
* than a message. One import each is also simply what the config now is.
*/
const GERMAN_IN_COMPONENT =
'German text belongs in src/messages, not in a component. ' +
'Use useTranslations() / getTranslations(). See docs/ui-conventions.md'
const config = [
{
ignores: [
// A git worktree is a checkout of its own, at its own commit, with its own
// lint run -- and this project puts them INSIDE the repository, under
// .claude/worktrees/. Linting one from here reports findings about code
// that is not on this branch and cannot be fixed from this directory.
//
// The symptom was the build output rather than the source: once a worktree
// had been built, `pnpm lint` in the main checkout drowned in hundreds of
// no-require-imports errors from minified vendor chunks -- exactly what
// the patterns below exist to prevent, walked around by one directory
// level. CI never saw it, because CI has no worktrees.
'.claude/worktrees/**',
// Prefixed with **/ rather than anchored at the root: an artefact
// directory one level down is the same artefact directory.
'**/.next/**',
'**/node_modules/**',
'**/coverage/**',
'next-env.d.ts',
// Test and build artefacts: minified vendor bundles that would otherwise
// drown the report in thousands of irrelevant findings.
'**/playwright-report/**',
'**/blob-report/**',
'**/test-results/**',
// The bundled collaboration server: generated output, not source.
'**/dist/**',
// The help site is a package of its own (Astro, its own lockfile) whose
// code is two config scripts; the Next rules here do not apply to it.
'help/**',
],
},
...nextCoreWebVitals,
...nextTypescript,
prettier,
{
rules: {
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
// --- goodworkshop-ui + i18n: what may not appear in UI code ----------------
//
// Both guardrails live in ONE `no-restricted-syntax` entry on purpose. Flat
// config REPLACES a rule's options when a later block matches the same file,
// so splitting them into two blocks over the same glob silently switched the
// first one off -- which is exactly the kind of guardrail failure that looks
// like everything is fine.
//
// No raw hex colours: category colours go through the .cat-* OKLCH tokens and
// accents through --brand-*, or dark mode, the print stylesheet and tenant
// branding end up disagreeing about a colour.
//
// No German prose: umlauts and ß are the cheap, reliable tell. It will not
// catch "Save me" -- nothing lint-shaped would -- but it does catch the
// realistic case, which is somebody adding a German label next to fifteen
// translated ones because that is what the file used to look like.
{
files: ['src/components/**/*.{ts,tsx}', 'src/features/**/*.{ts,tsx}', 'src/app/**/*.{ts,tsx}'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?\\b/]',
message:
'No raw hex colours in UI code. Category colours go through the .cat-* tokens, accents through --brand-*. See docs/ui-conventions.md',
},
{
selector: 'Literal[value=/[äöüÄÖÜß]/]',
message: GERMAN_IN_COMPONENT,
},
{
selector: 'TemplateElement[value.raw=/[äöüÄÖÜß]/]',
message: GERMAN_IN_COMPONENT,
},
{
selector: 'JSXText[value=/[äöüÄÖÜß]/]',
message: GERMAN_IN_COMPONENT,
},
],
},
},
// German is the SOURCE text, so these three keep the colour rule and lose the
// language one: the reference agenda and the assertions that read it are
// written in German, and translating a fixture would mean the export
// snapshots stop showing what a German facilitator actually sees. The export
// route's umlauts ARE the characters being transliterated, not prose.
//
// The hex selector is restated rather than inherited -- see the note above
// about replacement.
{
// Scoped to the SAME directories as the block above. Listing bare
// `src/**/*.test.ts` here would hand the colour rule to files it never
// applied to -- src/lib/color's own tests are full of hex values on
// purpose, because hex is what they convert.
files: [
'src/components/**/*.test.{ts,tsx}',
'src/features/**/*.test.{ts,tsx}',
'src/app/**/*.test.{ts,tsx}',
'src/components/**/fixtures/**/*.{ts,tsx}',
'src/features/**/fixtures/**/*.{ts,tsx}',
'src/app/api/w/**/export/route.ts',
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?\\b/]',
message:
'No raw hex colours in UI code. Category colours go through the .cat-* tokens, accents through --brand-*. See docs/ui-conventions.md',
},
],
},
},
// --- RLS seam: the raw db handle must never escape src/server/db -----------
// Every query has to run inside withTenant() so that app.tenant_id is set.
// An import of the raw client anywhere else is a tenant-isolation bug.
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/server/db/**'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/server/db/client', '@/server/db/client'],
message:
'Das rohe db-Handle ist modul-privat. Nutze withTenant() / withAuth() / withOps() aus @/server/db. Siehe Plan §RLS.',
},
],
},
],
},
},
]
export default config