Привет! Я Незар, фронтенд-разработчик в Т-Банке. Наш 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 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 стал стандартным инструментом в нашем процессе разработки. Он не требует ручного вмешательства и дает быструю обратную связь, позволяя команде сосредоточиться на создании новой функциональности, а не на разборе старого кода.