Привет! Я Незар, фронтенд-разработчик в Т-Банке. Наш 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 в Angular-проекте
Вначале необходимо установить 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
В крупном проекте ручное поддержание актуального списка workspaces в knip.config.ts становится проблемой: разработчики забывают добавить новую библиотеку, при удалении библиотеки запись остается в конфиге, конфигурация быстро устаревает.
Решение: скрипт автоматической генерации. Мы создали скрипт scripts/knip/generate-workspaces.ts, который сканирует директорию libs/ рекурсивно, находит все папки с src/index.ts, сканирует projects/ для Angular-приложений с src/main.ts и генерирует отсортированную конфигурацию workspaces.
Ключевые функции скрипта, который рекурсивно сканирует директорию и собирает workspace конфигурации:
function generateConfig() { const workspaces = scanDirectory('libs', ''); // Аналогично для Angular-проектов const newConfig = `import type {KnipConfig} from 'knip'; ${formatWorkspacesObject(workspaces)} export default { nx: true, workspaces: workspacesConfig, ...DEFAULT_KNIP_CONFIG (базовая конфигурация правил, которая описана выше) } satisfies KnipConfig; `; writeFileSync('knip.config.ts', newConfig, 'utf-8'); } function scanDirectory( dir: string, basePath: string, ) { const result = {}; const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { 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); Object.assign(result, nested); } } } return result; } function formatWorkspacesObject( workspaces: Record<string, any>, ) { const lines: string[] = []; lines.push("const workspacesConfig: KnipConfig['workspaces'] = {"); for (const key of Object.keys(workspaces)) { const config = workspaces[key]; const entryStr = `entry: ['src/index.ts']`; lines.push(`'${key}': {${entryStr}},`); } lines.push('};'); return lines.join('\n'); }
Второй скрипт scripts/knip/validate-workspaces.ts проверяет, что все библиотеки присутствуют в конфиге:
function validate() { const knipConfig = readFileSync('knip.config.ts', "utf-8"); const workspacesInConfig = extractWorkspacesByRegex(knipConfig); const actualWorkspaces = scanDirectory('libs', ""); // Аналогично для Angular-проектов const missingWorkspaces = actualWorkspaces.filter( (workspace) => !workspacesInConfig.has(workspace), ); if (missingWorkspaces.length > 0) { console.error("❌ Следующие workspace отсутствуют в knip.config.ts:"); missingWorkspaces.forEach((workspace) => console.error( - ${workspace}), ); process.exit(1); } } function scanDirectory(dir: string, basePath: string) { const result: string[] = []; if (!existsSync(dir)) return result; const entries = readdirSync(dir, {withFileTypes: true}); for (const entry of entries) { const fullPath = join(dir, entry.name); const workspacePath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { if (hasIndexTs(fullPath)) { result.push(`libs/${workspacePath}`); } else { result.push(...scanDirectory(fullPath, workspacePath)); } } } return result; }
В 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-validate: stage: affected script: - npm run knip:validate knip-report: stage: affected script: - npm run knip:report - | if [ -s knip-report.txt ]; then echo "⚠️ Есть рекомендации от Knip" exit 1 fi needs: - knip-validate
Скрипт следит, чтобы конфигурация всегда была в актуальном состоянии, и не дает мертвому коду проникнуть в основную ветку. Благодаря этому мы сокращаем технический долг еще до того, как он успевает накопиться.
Результаты
С появлением 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 стал стандартным инструментом в нашем процессе разработки. Он не требует ручного вмешательства и дает быструю обратную связь, позволяя команде сосредоточиться на создании новой функциональности, а не на разборе старого кода.