Как мы внедрили Knip в Angular-проект: от настройки до CI/CD
- суббота, 5 сентября 2026 г. в 00:00:13

Привет! Я Незар, фронтенд-разработчик в Т-Банке. Наш Angular-проект достаточно крупный, и со временем в нем накопился неиспользуемый код. Это замедляет разработку, усложняет рефакторинг и вводит в заблуждение новых членов команды. А ручная очистка такого «мертвого» кода требует огромных трудозатрат и быстро перестает быть эффективной.
Поэтому мы решили автоматизировать этот процесс с помощью Knip — статического анализатора для TypeScript. В статье я расскажу, как мы его настроили, интегрировали в pre‑commit и CI/CD и какие результаты получили.
Наш проект — крупное Angular-приложение с архитектурой Nx workspace: несколько приложений и более 100 библиотек. С ростом кодовой базы мы заметили, что в проекте накапливается неиспользуемый код: зависимости, экспорты, целые файлы.
Попытки отслеживать неиспользуемый код вручную оказались нереальными: в таком объеме разработчики просто не успевают проверять каждый файл, а при рефакторинге старые сущности часто остаются нетронутыми. Тогда мы обратились к инструменту Knip — статическому анализатору для TypeScript-проектов.
Что он делает:
Находит неиспользуемые зависимости в package.json.
Обнаруживает неиспользуемые файлы (.ts, .less, .html).
Выявляет неиспользуемые экспорты в модулях.
Показывает отсутствующие зависимости (unlisted) и неразрешенные импорты (unresolved).
Разберем нашу конфигурацию Knip, автоматическую генерацию workspaces, интеграцию в pre-commit хук и CI/CD и результаты, которые мы получили.
Вначале необходимо установить Knip как dev-зависимости:
npm install --save-dev knip
А далее — настроить базовую конфигурацию в knip.config.ts:
import type { KnipConfig } from "knip"; export default { nx: true, workspaces: { "libs/core": { entry: ["src/index.ts"] }, "projects/project1": { angular: true, entry: ["src/main.ts", "src/app/app.module.ts"], }, }, ignore: [ "**/*.d.ts", "**/polyfills*.ts", "**/environments/environment*.ts", "**/*.spec.ts", "**/*.test.ts", ], ignoreDependencies: ["package-name-1"], rules: { files: "error", dependencies: "error", devDependencies: "error", unresolved: "error", exports: "error", types: "error", }, } satisfies KnipConfig;
Пройдемся по ключевым полям конфигурации:
nx: true — включает интеграцию с Nx workspace. Knip автоматически распознает структуру проекта.
workspaces — определяет точки входа для каждой библиотеки. Для библиотек указываем src/index.ts, для Angular-приложений — src/main.ts и src/app/app.module.ts.
ignore — массив глобальных паттернов для игнорирования файлов. Мы игнорируем файлы деклараций (**/*.d.ts), полифилы, файлы окружения, тесты и артефакты сборки.
ignoreDependencies — зависимости, которые Knip должен игнорировать, даже если они не используются напрямую в коде.
rules — определяет строгость проверки: 'error' (блокирующая ошибка), 'warn' (предупреждение), 'off' (проверка отключена).
В крупном проекте ручное поддержание актуального списка workspaces в knip.config.ts становится проблемой: разработчики забывают добавить новую библиотеку, при удалении библиотеки запись остается в конфиге, конфигурация быстро устаревает.
Решение: скрипт автоматической генерации. Мы создали скрипт scripts/knip/generate-workspaces.ts, который сканирует директорию libs/ рекурсивно, находит все папки с src/index.ts, сканирует projects/ для Angular-приложений с src/main.ts и генерирует отсортированную конфигурацию workspaces.
Ключевые функции скрипта, который рекурсивно сканирует директорию и собирает workspace конфигурации:
function scanDirectory( dir: string, basePath: string, ignoreFolders: Set<string> = new Set([ "node_modules", ".git", "dist", ".nx", ".angular", ]), ): Record<string, { entry: string[] }> { const result: Record<string, { entry: string[] }> = {}; const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith(".") || ignoreFolders.has(entry.name)) continue; const fullPath = join(dir, entry.name); const workspacePath = basePath ? ${basePath}/${entry.name} : entry.name; if (entry.isDirectory()) { if (hasIndexTs(fullPath)) { result[libs/${workspacePath}] = { entry: ["src/index.ts"] }; } else { const nested = scanDirectory(fullPath, workspacePath, ignoreFolders); Object.assign(result, nested); } } } return result; }
Второй скрипт scripts/knip/validate-workspaces.ts проверяет, что все библиотеки присутствуют в конфиге:
function validate(): void { console.log("🔍 Валидация knip.config.ts..."); const knipConfig = readFileSync(KNIP_CONFIG_PATH, "utf-8"); const workspacesInConfig = extractWorkspacesFromConfig(knipConfig); const actualWorkspaces: string[] = []; // Сканируем projects/ if (existsSync(PROJECTS_DIR)) { const projects = readdirSync(PROJECTS_DIR, { withFileTypes: true }); for (const project of projects) { if (project.isDirectory() && !project.name.startsWith(".")) { const projectPath = join(PROJECTS_DIR, project.name); if (hasMainTs(projectPath)) { actualWorkspaces.push(projects/${project.name}); } } } } // Сканируем libs/ const libsWorkspaces = scanDirectory(LIBS_DIR, ""); actualWorkspaces.push(...libsWorkspaces); const missingWorkspaces = actualWorkspaces.filter( (workspace) => !workspacesInConfig.has(workspace), ); if (missingWorkspaces.length > 0) { console.error("❌ Следующие workspace отсутствуют в knip.config.ts:"); missingWorkspaces.forEach((workspace) => console.error( - ${workspace}), ); console.error("\n💡 Запустите для автогенерации: npm run knip:generate"); process.exit(1); } console.log( ✅ Все ${actualWorkspaces.length} workspace(s) присутствуют в knip.config.ts, ); }
В package.json добавлены скрипты для работы с Knip:
{ "scripts": { "knip": "knip --tsConfig tsconfig.base.json 2>&1 | tee knip-report.txt", "knip:fix": "knip --tsConfig tsconfig.base.json --fix", "knip:generate": "ts-node scripts/knip/generate-workspaces.ts", "knip:validate": "ts-node scripts/knip/validate-workspaces.ts" } }
Скрипт npm run knip запускает анализ с сохранением отчета, knip:fix автоматически исправляет проблемы, knip:generate автогенерирует конфигурацию workspaces, knip:validate проверяет актуальность конфигурации.
Валидацию Knip можно явно прописать в pre-commit хук через Husky:
# .husky/pre-commit npm run knip:validate
Если обнаружены библиотеки, отсутствующие в конфиге, коммит блокируется:
❌ Следующие workspace отсутствуют в knip.config.ts: - libs/new-feature 💡 Запустите для автогенерации: npm run knip:generate
Пойдем дальше и добавим CI-джобу, которая запустит эти проверки в пайплайне:
knip-check: stage: test script: - npm run knip:generate - git diff --exit-code knip.config.ts || (echo "❌ knip.config.ts устарел" && exit 1) - npm run knip -- --tsConfig tsconfig.base.json rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
Скрипт следит, чтобы конфигурация всегда была в актуальном состоянии, и не дает мертвому коду проникнуть в основную ветку. Благодаря этому мы сокращаем технический долг еще до того, как он успевает накопиться.
С появлением Knip мы перестали игнорировать мертвый код. Теперь проверка неиспользуемых сущностей запускается автоматически — на каждом коммите и в каждом MR.
Цифры и ощущения команды:
Удаление неиспользуемых зависимостей. Мы сократили общее число пакетов с 158 до 148 (−6,3%). Это ускорило установку node_modules примерно на 10% и уменьшило размер кэша в CI на несколько сотен мегабайт.
Очистка кода от мертвых сущностей — Knip помог обнаружить и удалить сотни файлов, которые давно потеряли актуальность: старые моки, сервисы и компоненты из экспериментов, не прошедших в продакшен, утилитарные и прочие классы. Хотя эти файлы не влияли на итоговый бандл (tree-shaking их исключал), они серьезно мешали разработке. Разработчики тратили время на изучение неработающего кода, при рефакторинге случайно использовали устаревшие функции, а новые члены команды терялись в захламленной структуре.
Исключение человеческого фактора. Автоматическая генерация workspaces и pre-commit-валидация гарантируют, что конфигурация Knip всегда синхронизирована с файловой системой. Никто больше не забывает добавить новую библиотеку в настройки.
Мгновенная обратная связь. Pre-commit-хук и CI/CD-джоба не дают мертвому коду попасть в основную ветку. Разработчик видит проблему еще до того, как отправит код на ревью, и может сразу ее исправить или в крайнем случае добавить исключение с комментарием.
Уверенность в коде. Сегодня мы точно знаем, что каждый файл, каждая зависимость в проекте либо используется, либо явно помечена как игнорируемая. Это повышает качество кода и ускоряет код-ревью — ревьюверы не отвлекаются на подозрительные, но мертвые фрагменты.
Хотя Knip кардинально не повлиял на размер итоговой сборки, нам удалось сделать саму разработку быстрее, а код — понятнее и надежнее. Мы перестали бороться с последствиями небрежности и начали автоматизированно предотвращать их на ранних этапах.
Внедрение Knip показало нам, что статический анализ эффективно решает проблему захламления кодовой базы. Инструмент позволяет выявлять неиспользуемые файлы, экспорты и зависимости на этапе разработки, а интеграция с pre‑commit-хуками и CI/CD гарантирует, что мертвый код не попадает в основную ветку.
Knip стал стандартным инструментом в нашем процессе разработки. Он не требует ручного вмешательства и дает быструю обратную связь, позволяя команде сосредоточиться на создании новой функциональности, а не на разборе старого кода.