Files
iptvnator/eslint.config.mjs
T
4grayandClaude Fable 5 e14b8ae8d9 feat(ci): enforce lint, guard coverage policy, add max-lines rule (#1117)
* fix(lint): resolve module-boundary and prefer-inject errors

Retag workspace-shell-util as type:data-access to match its injectable
services that depend on @iptvnator/services, and convert
RemoteControlService to inject(HttpClient).

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

* feat(lint): enforce max-lines 400 with generated baseline

Add a max-lines ESLint error (hard cap 400 raw lines per TypeScript
file) per the repo file-size rule. The 134 pre-existing offenders are
baselined in tools/eslint/max-lines-baseline.mjs, regenerable via
generate-max-lines-baseline.mjs; the list should only shrink.

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

* feat(ci): enforce lint on PRs and guard coverage policy drift

- Add a Lint job to ci.yml running nx run-many -t lint --all, so
  module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
  with a test target is missing from coverage-policy.json; wired into
  coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
  policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.

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

* docs: document CI lint enforcement and coverage policy guard

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

* fix(ci): address bot review feedback on policy guard and baseline generator

- Drive Tier B/C validation from each policy entry's validationCommand
  (falling back to nx test), skipping projects with an e2e target since
  the E2E workflow already runs them (Codex).
- Fail when a Tier A entry has no test target (Greptile, adapted:
  checking all entries against test targets would false-positive on the
  intentionally spec-less e2e/mock-server tiers).
- Guard against missing JSON array in nx show projects output (Greptile).
- Scan .tsx files in the max-lines baseline generator to match the
  ESLint rule's file patterns (Greptile).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 10:06:45 +02:00

279 lines
11 KiB
JavaScript

import nx from '@nx/eslint-plugin';
import { maxLinesBaseline } from './tools/eslint/max-lines-baseline.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: keep TypeScript files under 300 lines,
// hard maximum 400. Files that predate the rule are baselined below.
files: ['**/*.ts', '**/*.tsx'],
rules: {
'max-lines': [
'error',
{ max: 400, skipBlankLines: false, skipComments: false },
],
},
},
{
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'] }],
},
},
];