Files
iptvnator/eslint.config.mjs
T
4grayandClaude Opus 5 9b7776a901 chore(lint): hold tests to their own max-lines ceiling (#1306)
* chore(lint): hold tests to their own max-lines ceiling

The flat 400-line cap treated a spec like a component. A spec is a flat
list of independent cases, so hitting the cap there produces arbitrary
`-2.spec.ts` splits and hides coverage instead of surfacing design debt —
65 of the 138 files over the limit were tests.

Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and
everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no
longer count, so a docblock can't be the reason a file must be split.

Both limits now live in tools/eslint/max-lines-config.mjs, imported by
eslint.config.mjs and the baseline generator alike. The generator decides
who belongs on the list by running ESLint's own max-lines rule instead of
counting lines itself — a private reimplementation would disagree with the
rule the moment either side changed (a `//` inside a template literal is
enough) and yield a baseline that turns CI red while looking correct.

The baseline drops 126 -> 68 entries with nothing added, and six now-dead
`eslint-disable max-lines` directives are removed. A new eslint-tools test
asserts the committed baseline still matches what the generator produces,
so a stale entry or a forgotten regeneration fails CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(lint): classify eslint-tools in the coverage policy

A project with a `test` target must be assigned a coverage tier, so
adding eslint-tools broke `coverage:policy:check` before the unit suite
even ran. Tier B alongside packaging and release-tools: these are Node
tests over lint tooling, and a coverage percentage across a generated
list would not mean anything.

CI runs Tier B/C through its own `--run-non-tier-a` step, so the
baseline-consistency test executes there rather than being skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 08:08:04 +02:00

297 lines
11 KiB
JavaScript

import nx from '@nx/eslint-plugin';
import { maxLinesBaseline } from './tools/eslint/max-lines-baseline.mjs';
import {
MAX_LINES_OPTIONS,
MAX_LINES_PROD,
MAX_LINES_TEST,
TEST_FILE_GLOBS,
} from './tools/eslint/max-lines-config.mjs';
const legacyBareAliases = [
'components',
'm3u-state',
'm3u-utils',
'services',
'shared-interfaces',
'shared-portals',
'remote-control',
'database',
'database-schema',
'database-path-utils',
'workspace-dashboard-feature',
'workspace-dashboard-data-access',
];
export default [
...nx.configs['flat/base'],
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist'],
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
depConstraints: [
{
sourceTag: 'type:app',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:e2e',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:dev-app',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:website',
onlyDependOnLibsWithTags: ['type:ui', 'type:util'],
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: [
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:data-access',
onlyDependOnLibsWithTags: [
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:util',
onlyDependOnLibsWithTags: ['type:util'],
},
{
sourceTag: 'domain:portal-shared',
onlyDependOnLibsWithTags: [
'domain:portal-shared',
'domain:m3u',
'domain:playback',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
'scope:portal',
],
},
{
sourceTag: 'domain:m3u',
onlyDependOnLibsWithTags: [
'domain:m3u',
'domain:playback',
'domain:portal-shared',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
{
sourceTag: 'domain:playlist-import',
onlyDependOnLibsWithTags: [
'domain:m3u',
'domain:playlist-import',
'domain:playback',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'domain:stalker',
'scope:shared',
],
},
{
sourceTag: 'domain:playback',
onlyDependOnLibsWithTags: [
'domain:m3u',
'domain:playback',
'domain:portal-shared',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
{
sourceTag: 'domain:shared-contracts',
onlyDependOnLibsWithTags: [
'domain:shared-contracts',
'scope:shared',
],
},
{
sourceTag: 'domain:shared-runtime',
onlyDependOnLibsWithTags: [
'domain:m3u',
'domain:shared-contracts',
'domain:shared-runtime',
'scope:shared',
],
},
{
sourceTag: 'domain:shared-ui',
onlyDependOnLibsWithTags: [
'domain:m3u',
'domain:playback',
'domain:portal-shared',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
{
sourceTag: 'domain:xtream',
onlyDependOnLibsWithTags: [
'domain:xtream',
'domain:portal-shared',
'domain:m3u',
'domain:playback',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
{
sourceTag: 'domain:stalker',
onlyDependOnLibsWithTags: [
'domain:stalker',
'domain:portal-shared',
'domain:m3u',
'domain:playback',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
{
sourceTag: 'domain:workspace',
onlyDependOnLibsWithTags: [
'domain:workspace',
'domain:portal-shared',
'domain:xtream',
'domain:stalker',
'domain:m3u',
'domain:playback',
'domain:shared-contracts',
'domain:shared-runtime',
'domain:shared-ui',
'scope:shared',
],
},
],
},
],
'no-restricted-imports': [
'error',
{
paths: legacyBareAliases.map((name) => ({
name,
message:
'Use the scoped @iptvnator/* path alias instead of the legacy bare alias.',
})),
patterns: legacyBareAliases.map((name) => ({
group: [`${name}/*`],
message:
'Use the scoped @iptvnator/* path alias instead of the legacy bare alias.',
})),
},
],
},
},
{
files: [
'**/*.ts',
'**/*.tsx',
'**/*.cts',
'**/*.mts',
'**/*.js',
'**/*.jsx',
'**/*.cjs',
'**/*.mjs',
],
// Override or add rules here
rules: {
'@angular-eslint/template/click-events-have-key-events': 'off',
'@angular-eslint/template/interactive-supports-focus': 'off',
},
},
{
// CLAUDE.md file-size rule for production code: target under 300 lines,
// hard maximum 400. Files that predate the rule are baselined below.
files: ['**/*.ts', '**/*.tsx'],
ignores: TEST_FILE_GLOBS,
rules: {
'max-lines': [
'error',
{ max: MAX_LINES_PROD, ...MAX_LINES_OPTIONS },
],
},
},
{
// Tests get a much higher ceiling — see tools/eslint/max-lines-config.mjs
// for why a long spec is not the same signal as a long component.
files: TEST_FILE_GLOBS,
rules: {
'max-lines': [
'error',
{ max: MAX_LINES_TEST, ...MAX_LINES_OPTIONS },
],
},
},
{
files: maxLinesBaseline,
rules: {
'max-lines': 'off',
},
},
{
files: [
'apps/web/src/app/app.component.ts',
'apps/electron-backend/src/app/events/player.events.ts',
'libs/epg/data-access/**/*.ts',
'libs/ui/playback/**/*.ts',
],
rules: {
'no-console': ['warn', { allow: ['debug', 'warn', 'error'] }],
},
},
];