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>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-07-04 10:06:45 +02:00
1 parent 0b526b119a
commit e14b8ae8d9
11 files changed
+455 -12

No files matched your search

+33
View File
@@ -10,6 +10,33 @@ on:
workflow_dispatch:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint all projects
run: pnpm nx run-many --target=lint --all --parallel=3 --output-style=static
env:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
unit-and-typecheck:
name: Unit Tests and Typechecks
runs-on: ubuntu-latest
@@ -43,6 +70,12 @@ jobs:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
- name: Run Tier B/C validation commands
run: node tools/coverage/check-coverage-policy.mjs --run-non-tier-a
env:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
- name: Upload unit coverage artifact
if: always()
uses: actions/upload-artifact@v4
+11 -3
View File
@@ -198,13 +198,21 @@ Before finishing behavior changes or bug fixes, follow `Regression Prevention An
### Linting
```bash
# Lint frontend
nx lint web
# Lint all projects (what CI enforces on every PR)
pnpm run lint
# Lint backend
# Lint a single project
nx lint web
nx lint electron-backend
```
CI runs lint for every project (`.github/workflows/ci.yml`). This enforces the
Nx module-boundary tags, the legacy bare-alias ban, and a `max-lines` ESLint
rule (hard maximum 400 lines per TypeScript file). Pre-existing oversized files
are baselined in `tools/eslint/max-lines-baseline.mjs`; regenerate the baseline
with `node tools/eslint/generate-max-lines-baseline.mjs` after splitting a file.
Never add new files to the baseline.
### Documentation And Wiki Export
```bash
@@ -82,3 +82,16 @@ playback, or EPG data belongs in `libs/portal/shared/data-access`, not
`libs/portal/shared/util`. That keeps pure collection helpers importable by
Xtream/Stalker data-access libraries while allowing shared UI to use
provider-specific collection services without creating cycles.
Note: `workspace-shell-util` (`libs/workspace/shell/util`) is tagged
`type:data-access` despite its path. It exports injectable services such as
`WorkspaceStartupPreferencesService` that depend on `@iptvnator/services`, and
it must stay eagerly importable from `apps/web/src/app/app.routes.ts` without
pulling the lazy-loaded workspace shell feature bundle into the initial chunk.
## CI Enforcement
The `Lint` job in `.github/workflows/ci.yml` runs
`pnpm nx run-many --target=lint --all` on every PR, so
`@nx/enforce-module-boundaries` violations, legacy bare-alias imports, and
`max-lines` violations fail CI. Run `pnpm run lint` locally before pushing.
+24 -2
View File
@@ -17,16 +17,38 @@ pnpm nx show projects --withTarget e2e
| ---------------------------------- | ----------------------------------- |
| Angular renderer entry points | `pnpm run typecheck:web` |
| Electron main process entry points | `pnpm run typecheck:backend` |
| Curated CI unit suite | `pnpm run test:unit:ci` |
| Full unit suite (all projects) | `pnpm run test:unit:ci` |
| EPG data access | `pnpm nx test epg-data-access` |
| Workspace shell utilities | `pnpm nx test workspace-shell-util` |
| Shared SQLite schema/connection | `pnpm nx test database` |
| Packaging metadata | `pnpm nx test packaging` |
## Lint
```bash
pnpm run lint # nx run-many --target=lint --all
pnpm nx lint <project> # single project
```
The CI workflow (`.github/workflows/ci.yml`) runs lint for every project on
each PR. This enforces `@nx/enforce-module-boundaries` (scope/domain/type tag
constraints), the legacy bare-alias ban, and the `max-lines` file-size rule
(hard maximum 400 lines per TypeScript file). Files that predate the
`max-lines` rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after
splitting a baselined file below the limit, regenerate the list with
`node tools/eslint/generate-max-lines-baseline.mjs`. Never add new files to
the baseline.
## Coverage Tiers
Use `tools/coverage/coverage-policy.json` as the source of truth for coverage
ownership.
ownership. Every project with a `test` target must be classified in a tier;
`pnpm run coverage:policy:check` (part of `coverage:ci`) fails CI when a new
project is missing from the policy, a listed project no longer exists, or a
Tier A entry has no test target. CI runs Tier A with coverage (uploaded to
Codecov) and each Tier B/C project's `validationCommand` (falling back to
`nx test`) without coverage; projects with an `e2e` target are skipped there
because the E2E workflow already runs them on every PR.
| Tier | Rule | Validation |
| --- | --- | --- |
+18
View File
@@ -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',
@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@@ -25,8 +25,7 @@ export interface RemoteControlStatus {
})
export class RemoteControlService {
private readonly apiBaseUrl = '/api/remote-control';
constructor(private http: HttpClient) {}
private readonly http = inject(HttpClient);
/**
* Changes to the next channel
+1 -1
View File
@@ -4,7 +4,7 @@
"sourceRoot": "libs/workspace/shell/util/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:workspace", "domain:workspace", "type:util"],
"tags": ["scope:workspace", "domain:workspace", "type:data-access"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
+4 -3
View File
@@ -32,11 +32,12 @@
"test:frontend": "nx test web",
"test:backend": "nx test electron-backend",
"test:unit:all": "nx run-many --target=test --all --parallel=3",
"test:unit:ci": "nx run-many --target=test --projects=electron-backend,web,services,m3u-state,portal-stalker-data-access,portal-shared-util,playlist-shared-ui,portal-xtream-data-access,portal-xtream-feature,workspace-dashboard-data-access,components,@iptvnator/pipes,packaging,epg-data-access,workspace-shell-util,database --parallel=3 --output-style=static",
"test:unit:ci": "nx run-many --target=test --all --parallel=3 --output-style=static",
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
"coverage:health": "node tools/coverage/coverage-health.mjs",
"coverage:ci": "pnpm run coverage:unit:ci && pnpm run coverage:merge && node tools/coverage/coverage-health.mjs --require-report",
"coverage:policy:check": "node tools/coverage/check-coverage-policy.mjs",
"coverage:ci": "pnpm run coverage:policy:check && pnpm run coverage:unit:ci && pnpm run coverage:merge && node tools/coverage/coverage-health.mjs --require-report",
"coverage:e2e:summary": "node tools/coverage/e2e-semantic-summary.mjs",
"coverage:e2e:v8:web": "node tools/coverage/e2e-v8-web.mjs",
"typecheck:web": "tsc -p apps/web/tsconfig.app.json --noEmit",
@@ -59,7 +60,7 @@
"release:artwork:manifest": "tsx tools/release/generate-marketing-artwork.ts --manifest",
"release:artwork:generate": "tsx tools/release/generate-marketing-artwork.ts --generate",
"release:artwork:validate": "tsx tools/release/generate-marketing-artwork.ts --validate",
"lint": "nx lint electron-backend",
"lint": "nx run-many --target=lint --all",
"build": "nx build electron-backend",
"wiki:export": "node tools/wiki/export-wiki-context.mjs",
"test:wiki-export": "node --test tools/wiki/export-wiki-context.test.mjs"
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Guards tools/coverage/coverage-policy.json against drift.
*
* Every Nx project with a `test` target must be classified in a coverage
* tier (A, B, or C). This makes CI fail loudly when a new library is added
* without deciding how it is tested, instead of silently skipping it.
*
* Usage:
* node tools/coverage/check-coverage-policy.mjs
* Validate the policy. Exits non-zero on unclassified projects, stale
* entries, or Tier A entries without a test target.
* node tools/coverage/check-coverage-policy.mjs --run-non-tier-a
* Run the validation command for each Tier B/C project. Uses the
* entry's `validationCommand` when present, falling back to
* `pnpm nx test <project>`. Projects with an `e2e` target are skipped
* because the E2E workflow already runs them on every PR.
*/
import { execFileSync, spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
const workspaceRoot = process.cwd();
const policyPath = path.join(workspaceRoot, 'tools/coverage/coverage-policy.json');
const policy = JSON.parse(readFileSync(policyPath, 'utf8'));
const tiers = {
tierA: policy.unitCoverage.tierA ?? [],
tierB: policy.unitCoverage.tierB ?? [],
tierC: policy.unitCoverage.tierC ?? [],
};
function showProjects(extraArgs = []) {
const output = execFileSync(
'pnpm',
['nx', 'show', 'projects', '--json', ...extraArgs],
{ cwd: workspaceRoot, encoding: 'utf8' }
);
const start = output.indexOf('[');
if (start === -1) {
throw new Error(
`nx show projects --json produced no JSON array.\nRaw output:\n${output}`
);
}
return new Set(JSON.parse(output.slice(start)));
}
const testProjects = showProjects(['--withTarget', 'test']);
const allProjects = showProjects();
const e2eProjects = showProjects(['--withTarget', 'e2e']);
function runNonTierA() {
let failed = false;
for (const project of [...tiers.tierB, ...tiers.tierC]) {
if (e2eProjects.has(project.name)) {
console.log(
`==> Skipping ${project.name}: covered by the E2E workflow.`
);
continue;
}
const command =
project.validationCommand ??
(testProjects.has(project.name)
? `pnpm nx test ${project.name}`
: null);
if (!command) {
console.log(
`==> Skipping ${project.name}: no validation command or test target.`
);
continue;
}
console.log(`\n==> Validating ${project.name}: ${command}`);
const result = spawnSync(command, {
cwd: workspaceRoot,
shell: true,
stdio: 'inherit',
});
if (result.status !== 0) {
failed = true;
}
}
process.exit(failed ? 1 : 0);
}
if (process.argv.includes('--run-non-tier-a')) {
runNonTierA();
}
const classified = new Map(
Object.entries(tiers).flatMap(([tier, projects]) =>
projects.map((project) => [project.name, tier])
)
);
const unclassified = [...testProjects].filter((name) => !classified.has(name));
const stale = [...classified.keys()].filter((name) => !allProjects.has(name));
const tierAWithoutTest = tiers.tierA
.map((project) => project.name)
.filter((name) => !testProjects.has(name));
if (unclassified.length > 0) {
console.error(
'Coverage policy is missing projects that have a test target:\n' +
unclassified.map((name) => ` - ${name}`).join('\n') +
'\n\nAdd each project to a tier in tools/coverage/coverage-policy.json:' +
'\n tierA: runs in CI with coverage (Codecov)' +
'\n tierB/tierC: runs in CI without coverage'
);
process.exit(1);
}
if (stale.length > 0) {
console.error(
'Coverage policy lists projects that do not exist in the workspace:\n' +
stale.map((name) => ` - ${name}`).join('\n') +
'\n\nRemove them from tools/coverage/coverage-policy.json.'
);
process.exit(1);
}
if (tierAWithoutTest.length > 0) {
console.error(
'Tier A projects must have a test target, but these do not:\n' +
tierAWithoutTest.map((name) => ` - ${name}`).join('\n') +
'\n\nRestore the test target or move the project to Tier B/C in ' +
'tools/coverage/coverage-policy.json.'
);
process.exit(1);
}
console.log(
`Coverage policy OK: ${testProjects.size} test projects classified ` +
`(${tiers.tierA.length} Tier A, ${tiers.tierB.length} Tier B, ${tiers.tierC.length} Tier C).`
);
@@ -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('.tsx')) &&
!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.`);
+140
View File
@@ -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',
];