fix(lint): read the config the way ESLint 10 reads one

ESLint was raised to 10, which reads eslint.config.* and nothing else. This
repo still said it all in .eslintrc.cjs and .eslintignore, so every lint run
ended at "couldn't find an eslint.config file" — and the pre-commit hook runs
eslint over staged sources, so no commit touching a .ts file could be made.

The config is the same config in the flat spelling: every rule, every ignore
and the __tests__ override carried over, and the two legacy files are gone
rather than left beside it to disagree.

The transaction rule (inside $transaction, only `tx`) keeps both its callers.
transaction.cjs is a whole flat config, so `lint:tx` still hands it to eslint
alone — with --no-config-lookup, since --no-eslintrc and --ext no longer
exist — and the shared config spreads it in. Its test now gives the linter
the array AS SHIPPED instead of re-stating the parser beside a copy of the
rules: a copy keeps passing after the original stops selecting files. 10 of
10, and `lint:tx` is clean over packages and apps.

eslint-plugin-react finds the React version by asking the rule context for a
filename through an API ESLint 10 removed, so detection threw on the first
file. The version is stated, which never takes that path.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-18 17:58:11 -07:00
co-authored by Hanzo Dev
parent 4880a6f2b5
commit 1c7d2f6fbb
6 changed files with 162 additions and 133 deletions
-8
View File
@@ -1,8 +0,0 @@
# Config files
*.config.js
*.config.cjs
# Statically hosted javascript files
apps/*/public/*.js
apps/*/public/*.cjs
scripts/
-16
View File
@@ -1,16 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@hanzo/esign-eslint-config')],
rules: {
'@next/next/no-img-element': 'off',
'no-unreachable': 'error',
'react-hooks/exhaustive-deps': 'off',
},
settings: {
next: {
rootDir: ['apps/*/'],
},
},
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
};
+40
View File
@@ -0,0 +1,40 @@
const shared = require('@hanzo/esign-eslint-config');
/** @type {import('eslint').Linter.Config[]} */
module.exports = [
{
// What is never linted. These were `.eslintignore` and `ignorePatterns`; a
// flat config keeps them in the one file that holds everything else.
ignores: [
// Config files
'**/*.config.js',
'**/*.config.cjs',
'**/*.config.mjs',
// Statically hosted javascript files
'apps/*/public/*.js',
'apps/*/public/*.cjs',
'scripts/**',
'**/lingui.config.ts',
'packages/lib/translations/**/*.js',
// Generated
'packages/prisma/generated/**',
],
},
...shared,
{
settings: {
next: { rootDir: ['apps/*/'] },
// Stated, not detected. eslint-plugin-react finds the version by asking the
// rule context for a filename through an API ESLint 10 removed, so detection
// throws on the first file; a declared version never takes that path.
react: { version: '19.2.8' },
},
rules: {
'@next/next/no-img-element': 'off',
'no-unreachable': 'error',
'react-hooks/exhaustive-deps': 'off',
},
},
];
@@ -2,22 +2,16 @@ const assert = require('node:assert/strict');
const { test } = require('node:test');
const { Linter } = require('eslint');
const parser = require('@typescript-eslint/parser');
const config = require('../transaction.cjs');
const linter = new Linter();
linter.defineParser('ts', parser);
// The config AS SHIPPED, handed to the linter whole: the same array `lint:tx`
// gives eslint. Reading the rules out of it and re-stating the parser here would
// test a copy, and a copy keeps passing after the original stops selecting files.
const linter = new Linter({ configType: 'flat' });
/** @param {string} code */
const check = (code) =>
linter
.verify(code, {
parser: 'ts',
parserOptions: config.parserOptions,
rules: config.rules,
})
.map((m) => m.message);
const check = (code) => linter.verify(code, config, { filename: 'probe.ts' }).map((m) => m.message);
test('a transaction that only touches tx is clean', () => {
assert.deepEqual(
+95 -85
View File
@@ -1,99 +1,109 @@
module.exports = {
extends: [
require.resolve('./transaction.cjs'),
'next',
'turbo',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
const js = require('@eslint/js');
const globals = require('globals');
const next = require('eslint-config-next');
const turbo = require('eslint-config-turbo/flat');
const unusedImports = require('eslint-plugin-unused-imports');
const tseslint = require('typescript-eslint');
plugins: ['unused-imports'],
const transaction = require('./transaction.cjs');
env: {
es2022: true,
node: true,
browser: true,
},
const source = ['**/*.ts', '**/*.tsx'];
parser: '@typescript-eslint/parser',
// The shared lint config, as ESLint 10 reads one: an array, in order, later
// entries winning. Everything the eslintrc form said is here — it changed
// spelling, not meaning.
module.exports = [
...transaction,
...next,
...(turbo.default ?? turbo),
js.configs.recommended,
...tseslint.configs.recommended,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['../../tsconfig.eslint.json'],
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
},
{
name: 'esign/source',
files: source,
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react/no-unescaped-entities': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
languageOptions: {
parser: tseslint.parser,
parserOptions: {
tsconfigRootDir: __dirname,
project: ['../../tsconfig.eslint.json'],
ecmaVersion: 2022,
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
],
globals: { ...globals.es2022, ...globals.node, ...globals.browser },
},
'no-multi-spaces': [
'error',
{
ignoreEOLComments: false,
exceptions: {
BinaryExpression: false,
VariableDeclarator: false,
ImportDeclaration: false,
Property: false,
plugins: { 'unused-imports': unusedImports },
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react/no-unescaped-entities': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
},
},
],
],
// Safety with promises so we aren't running with scissors
'no-promise-executor-return': 'error',
'prefer-promise-reject-errors': 'error',
'require-atomic-updates': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-await': 'error',
'no-multi-spaces': [
'error',
{
ignoreEOLComments: false,
exceptions: {
BinaryExpression: false,
VariableDeclarator: false,
ImportDeclaration: false,
Property: false,
},
},
],
// We never want to use `as` but are required to on occasion to handle
// shortcomings in third-party and generated types.
//
// To handle this we want this rule to catch usages and highlight them as
// warnings so we can write appropriate interfaces and guards later.
'@typescript-eslint/consistent-type-assertions': ['warn', { assertionStyle: 'never' }],
// Safety with promises so we aren't running with scissors
'no-promise-executor-return': 'error',
'prefer-promise-reject-errors': 'error',
'require-atomic-updates': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
disallowTypeAnnotations: false,
},
],
// We never want to use `as` but are required to on occasion to handle
// shortcomings in third-party and generated types.
//
// To handle this we want this rule to catch usages and highlight them as
// warnings so we can write appropriate interfaces and guards later.
'@typescript-eslint/consistent-type-assertions': ['warn', { assertionStyle: 'never' }],
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
disallowTypeAnnotations: false,
},
],
},
},
overrides: [
{
// `node:test` awaits the promise a describe/test call returns; a test
// file states its cases rather than chaining them.
files: ['**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-floating-promises': 'off',
},
{
// `node:test` awaits the promise a describe/test call returns; a test
// file states its cases rather than chaining them.
name: 'esign/tests',
files: ['**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-floating-promises': 'off',
},
],
};
},
];
+22 -13
View File
@@ -68,18 +68,27 @@ const restricted = [
},
];
module.exports = {
parser: '@typescript-eslint/parser',
const parser = require('@typescript-eslint/parser');
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: { jsx: true },
// A flat config, and a whole one: `lint:tx` hands this file to eslint alone, so
// it says for itself which files it reads and which it never does.
module.exports = [
{ ignores: ['**/build/**', '**/dist/**', '**/.react-router/**', '**/node_modules/**'] },
{
name: 'esign/transaction',
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
},
rules: {
'no-restricted-syntax': ['error', ...restricted],
},
},
ignorePatterns: ['build', 'dist', '.react-router'],
rules: {
'no-restricted-syntax': ['error', ...restricted],
},
};
];