diff --git a/eslint.config.mjs b/eslint.config.mjs index 41093fbd1..57bef608a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,5 @@ import nx from '@nx/eslint-plugin'; +import { maxLinesBaseline } from './tools/eslint/max-lines-baseline.mjs'; const legacyBareAliases = [ 'components', @@ -246,6 +247,23 @@ export default [ '@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', diff --git a/tools/eslint/generate-max-lines-baseline.mjs b/tools/eslint/generate-max-lines-baseline.mjs new file mode 100644 index 000000000..bf288ac49 --- /dev/null +++ b/tools/eslint/generate-max-lines-baseline.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +/** + * Regenerates tools/eslint/max-lines-baseline.mjs: the list of TypeScript + * files that already exceed the max-lines limit enforced in eslint.config.mjs. + * Baselined files are exempt from the rule; the list should only shrink. + * + * Usage: node tools/eslint/generate-max-lines-baseline.mjs + */ + +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +export const MAX_LINES = 400; + +const workspaceRoot = process.cwd(); +const scanRoots = ['apps', 'libs', 'tools']; +const skipDirs = new Set(['node_modules', 'dist', '.nx', 'coverage']); + +function collectTsFiles(dir, results) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!skipDirs.has(entry.name)) { + collectTsFiles(path.join(dir, entry.name), results); + } + } else if ( + entry.name.endsWith('.ts') && + !entry.name.endsWith('.d.ts') + ) { + results.push(path.join(dir, entry.name)); + } + } + return results; +} + +function countLines(filePath) { + const content = readFileSync(filePath, 'utf8'); + if (content.length === 0) { + return 0; + } + const lines = content.split('\n').length; + return content.endsWith('\n') ? lines - 1 : lines; +} + +const offenders = scanRoots + .flatMap((root) => collectTsFiles(path.join(workspaceRoot, root), [])) + .map((filePath) => ({ + file: path.relative(workspaceRoot, filePath).split(path.sep).join('/'), + lines: countLines(filePath), + })) + .filter(({ lines }) => lines > MAX_LINES) + .sort((a, b) => a.file.localeCompare(b.file)); + +const banner = `// Generated by tools/eslint/generate-max-lines-baseline.mjs — do not edit by hand. +// TypeScript files that predate the max-lines (${MAX_LINES}) ESLint rule. +// This list should only shrink: split a file below the limit, rerun the +// generator, and commit the result. Never add new files here. +`; + +const body = offenders.map(({ file }) => ` '${file}',`).join('\n'); + +writeFileSync( + path.join(workspaceRoot, 'tools/eslint/max-lines-baseline.mjs'), + `${banner}export const maxLinesBaseline = [\n${body}\n];\n` +); + +console.log(`Baselined ${offenders.length} files over ${MAX_LINES} lines.`); diff --git a/tools/eslint/max-lines-baseline.mjs b/tools/eslint/max-lines-baseline.mjs new file mode 100644 index 000000000..2e79ddc2c --- /dev/null +++ b/tools/eslint/max-lines-baseline.mjs @@ -0,0 +1,140 @@ +// Generated by tools/eslint/generate-max-lines-baseline.mjs — do not edit by hand. +// TypeScript files that predate the max-lines (400) ESLint rule. +// This list should only shrink: split a file below the limit, rerun the +// generator, and commit the result. Never add new files here. +export const maxLinesBaseline = [ + 'apps/electron-backend-e2e/src/catalog-sorting.e2e.ts', + 'apps/electron-backend-e2e/src/category-management.e2e.ts', + 'apps/electron-backend-e2e/src/electron-test-fixtures.ts', + 'apps/electron-backend-e2e/src/favorites.e2e.ts', + 'apps/electron-backend-e2e/src/playlist-switcher.e2e.ts', + 'apps/electron-backend-e2e/src/portal-mock-fixtures.ts', + 'apps/electron-backend-e2e/src/recent.e2e.ts', + 'apps/electron-backend-e2e/src/search.e2e.ts', + 'apps/electron-backend-e2e/src/settings.e2e.ts', + 'apps/electron-backend-e2e/src/sources.e2e.ts', + 'apps/electron-backend/src/app/api/main.preload.spec-data.ts', + 'apps/electron-backend/src/app/api/main.preload.ts', + 'apps/electron-backend/src/app/app.ts', + 'apps/electron-backend/src/app/database/operations/content.operations.spec.ts', + 'apps/electron-backend/src/app/database/operations/content.operations.ts', + 'apps/electron-backend/src/app/database/operations/playlist.operations.ts', + 'apps/electron-backend/src/app/events/epg-query.service.spec.ts', + 'apps/electron-backend/src/app/events/epg-query.service.ts', + 'apps/electron-backend/src/app/events/epg-worker.service.ts', + 'apps/electron-backend/src/app/events/epg.events.spec.ts', + 'apps/electron-backend/src/app/events/mpv-session.service.ts', + 'apps/electron-backend/src/app/events/player.events.spec.ts', + 'apps/electron-backend/src/app/events/playlist.events.spec.ts', + 'apps/electron-backend/src/app/events/vlc-session.service.ts', + 'apps/electron-backend/src/app/services/app-update.service.spec.ts', + 'apps/electron-backend/src/app/services/app-update.service.ts', + 'apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts', + 'apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts', + 'apps/electron-backend/src/app/services/embedded-mpv-native.service.ts', + 'apps/electron-backend/src/app/util/validated-axios.spec.ts', + 'apps/electron-backend/src/app/workers/database.worker.ts', + 'apps/electron-backend/src/app/workers/epg-parser.worker.ts', + 'apps/stalker-mock-server/src/app/data-generator.ts', + 'apps/web-backend/src/app/web-backend-app.spec.ts', + 'apps/web-backend/src/app/web-backend-app.ts', + 'apps/web-e2e/src/stalker.e2e.ts', + 'apps/web-e2e/src/xtream.e2e.ts', + 'apps/web/src/app/services/electron.service.ts', + 'apps/web/src/app/services/pwa.service.ts', + 'apps/web/src/app/settings/settings.component.spec.ts', + 'apps/web/src/app/settings/settings.component.ts', + 'apps/xtream-mock-server/src/app/generators/marketing.generator.ts', + 'libs/epg/data-access/src/lib/epg.service.spec.ts', + 'libs/epg/data-access/src/lib/epg.service.ts', + 'libs/m3u-state/src/lib/effects.ts', + 'libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts', + 'libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts', + 'libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.spec.ts', + 'libs/playlist/shared/ui/src/lib/playlist-refresh-action.service.ts', + 'libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts', + 'libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.spec.ts', + 'libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts', + 'libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.spec.ts', + 'libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts', + 'libs/playlist/shared/util/src/lib/playlist-context.facade.spec.ts', + 'libs/playlist/shared/util/src/lib/playlist-context.facade.ts', + 'libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.spec.ts', + 'libs/portal/downloads/feature/src/lib/downloads.component.ts', + 'libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.spec.ts', + 'libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts', + 'libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.spec.ts', + 'libs/portal/shared/data-access/src/lib/collection/unified-favorites-data.service.ts', + 'libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.spec.ts', + 'libs/portal/shared/data-access/src/lib/collection/unified-recent-data.service.ts', + 'libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.spec.ts', + 'libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts', + 'libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.spec.ts', + 'libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts', + 'libs/portal/shared/util/src/lib/navigation/workspace-portal-navigation.ts', + 'libs/portal/stalker/data-access/src/lib/stalker-session.service.ts', + 'libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.spec.ts', + 'libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.ts', + 'libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-epg.feature.ts', + 'libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-player.feature.ts', + 'libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.spec.ts', + 'libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts', + 'libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts', + 'libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts', + 'libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts', + 'libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts', + 'libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts', + 'libs/portal/xtream/data-access/src/lib/data-sources/electron-xtream-data-source.ts', + 'libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.spec.ts', + 'libs/portal/xtream/data-access/src/lib/data-sources/pwa-xtream-data-source.ts', + 'libs/portal/xtream/data-access/src/lib/data-sources/xtream-data-source.interface.ts', + 'libs/portal/xtream/data-access/src/lib/services/xtream-api.service.ts', + 'libs/portal/xtream/data-access/src/lib/services/xtream-url.service.ts', + 'libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.spec.ts', + 'libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts', + 'libs/portal/xtream/data-access/src/lib/stores/features/with-selection.feature.ts', + 'libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts', + 'libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts', + 'libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.spec.ts', + 'libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts', + 'libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts', + 'libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts', + 'libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.spec.ts', + 'libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts', + 'libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts', + 'libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.spec.ts', + 'libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.ts', + 'libs/services/src/lib/database-electron.service.ts', + 'libs/services/src/lib/downloads.service.ts', + 'libs/services/src/lib/playlist-backup.service.ts', + 'libs/services/src/lib/playlists.service.spec.ts', + 'libs/services/src/lib/playlists.service.ts', + 'libs/services/src/lib/runtime-capabilities.service.spec.ts', + 'libs/shared/database/src/lib/connection.spec.ts', + 'libs/shared/database/src/lib/connection.ts', + 'libs/shared/interfaces/src/lib/electron-api.interface.ts', + 'libs/shared/m3u-utils/src/lib/playlist.utils.ts', + 'libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts', + 'libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts', + 'libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.ts', + 'libs/ui/components/src/lib/season-container/season-container.component.ts', + 'libs/ui/epg/src/lib/epg-list/epg-list.component.spec.ts', + 'libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts', + 'libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts', + 'libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts', + 'libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts', + 'libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts', + 'libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts', + 'libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts', + 'libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts', + 'libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts', + 'libs/workspace/dashboard/feature/src/lib/rails/workspace-dashboard-rails.component.spec.ts', + 'libs/workspace/dashboard/feature/src/lib/rails/workspace-dashboard-rails.component.ts', + 'libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.spec.ts', + 'libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts', + 'libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-command-builders.ts', + 'libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts', + 'libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts', + 'tools/release/capture-v020-screenshots.ts', + 'tools/release/generate-marketing-artwork.ts', +];