Skip to content

Граф зависимостей ​

Две команды, которые смотрят на один и тот же граф импортов с разных сторон: unused-deps сверяет его с тем, что заявлено в package.json, circular-imports ищет в нём циклы. Обе — только для чтения, ничего не пишут и не выполняют исходный код.

unused-deps ​

Находит зависимости в package.json, на которые нет ни одного импорта в коде — и зеркально, «фантомные» зависимости: пакет реально импортируется, но нигде не объявлен.

bash
devtoolz unused-deps [dir] [options]

Команда читает один package.json в указанной директории (по умолчанию — текущая) — тот же самый путь служит и корнем скана файлов, и точкой поиска package.json, специально без возможности рассинхрона между «что сканируем» и «чей package.json сверяем».

Реальный вывод — пакет с одной по-настоящему неиспользуемой зависимостью и одной фантомной:

🧰 devtoolz, reporting for duty

Scanned 2 files.

2 problems found:
  phantom  dotenv    resolves from C:\tmp\unused-deps-doc\node_modules\dotenv — not declared in package.json
  unused   left-pad  (dependencies, not imported anywhere)

dotenv реально импортируется в исходниках, но нигде не значится в package.json — работает только потому, что кто-то другой затянул его транзитивно. left-pad объявлена, но на неё нет ни одного импорта.

Что считается использованием, кроме прямого импорта ​

Наивная проверка «просто нет импорта → неиспользуемая» дала бы огромное количество ложных срабатываний — тот же пакет, но с vitest/happy-dom/@types/node в devDependencies, при обычном запуске:

🧰 devtoolz — chores, automated

Scanned 2 files.

2 problems found:
  phantom  dotenv    resolves from C:\tmp\unused-deps-doc\node_modules\dotenv — not declared in package.json
  unused   left-pad  (dependencies, not imported anywhere)

vitest и happy-dom в отчёт не попадают — они используются, просто не через import:

  • вызов бинарником из scripts (или из lint-staged в package.json, если он там) — vitest вызывается через "test": "vitest run"; проверка учитывает и bin-ключи из собственного package.json пакета в node_modules, не только имя самого пакета — так ловится и typescript, вызываемая как tsc;
  • упоминание в конфиг-файле — happy-dom нигде не импортируется, но vitest.config.ts содержит environment: 'happy-dom'. Проверяется фиксированный список файлов (vitest.config.*, .eslintrc*, .stylelintrc*, postcss.config.*, tsconfig.json и другие) на присутствие имени пакета — и в кавычках, и голым идентификатором (postcss.config.js's plugins: { autoprefixer: {} }), с проверкой границ слова, чтобы autoprefixer не совпал с autoprefixer-wrapper как часть другого имени.

Структурно необнаружимые пакеты ​

Отдельная, явная категория — пакеты, которые в принципе никогда не появятся текстом ни в импорте, ни в scripts, ни в каком-либо конфиге: @types/* (ambient, подхватывается TypeScript по одному факту наличия), typescript (почти всегда легитимна, вызывается транзитивно другими инструментами), @vitest/coverage-*/@vitest/ui (vitest сам достраивает имя пакета из значения coverage.provider/ui — полное имя пакета никогда не появляется текстом), @nuxt/schema (ambient-аугментация типов Nuxt-модуля), postcss/sass/less/stylus (Vite подхватывает по расширению файла и факту установки).

По умолчанию эта категория не проверяется вообще. --strict включает её тоже — на том же примере выше, @types/node теперь виден, а vitest/happy-dom по-прежнему нет (это реальное использование, а не исключение из проверки):

🧰 devtoolz

Scanned 2 files.

3 problems found:
  phantom  dotenv       resolves from C:\tmp\unused-deps-doc\node_modules\dotenv — not declared in package.json
  unused   @types/node  (devDependencies, not imported anywhere)
  unused   left-pad     (dependencies, not imported anywhere)

Опции ​

[dir] ​

Директория пакета для проверки — она же корень скана файлов (позиционный аргумент, по умолчанию .).

--strict ​

Также проверять структурно необнаружимую категорию (см. выше) — для ручного аудита.

--ignore-package <name> ​

Исключить конкретную зависимость из проверки в обе стороны (повторяемый) — для случаев, которые не покрывает ни одна эвристика, например пакет, вызываемый только из внешнего git-хука.

--ext <list> ​

Через запятую, какие расширения обрабатывать. По умолчанию: .ts,.tsx,.js,.jsx,.cjs,.mjs.

--ignore <glob> ​

Дополнительный паттерн игнорирования (повторяемый) поверх встроенных дефолтов.

--no-respect-gitignore ​

Не учитывать .gitignore проекта.

Намеренно без --fix — удаление зависимости из package.json слишком рискованно для автоматики, решение — за человеком.

Пример:

bash
devtoolz unused-deps                       # package.json в текущей директории
devtoolz unused-deps path/to/package        # конкретная директория
devtoolz unused-deps --strict               # и структурно неявную категорию тоже
devtoolz unused-deps --ignore-package foo    # исключить конкретный пакет

circular-imports ​

Находит циклы импортов (A → B → … → A) в собственном коде проекта.

bash
devtoolz circular-imports [paths...] [options]

В ESM цикл импортов иногда даёт undefined в рантайме в самый неожиданный момент, а находится обычно только когда уже сломалось. Проверяются только локальные (./) спецификаторы — цикл через сторонний пакет не виден и не важен. Находятся все циклы сразу, не только первый; каждая находка — полная цепочка, не просто пара файлов, которые входят в один цикл.

Реальный вывод — два файла, импортирующие друг друга напрямую:

🧰 devtoolz, reporting for duty

Scanned 2 files.

1 circular import found:

  src/a.ts
  → src/b.ts
  → src/a.ts

import type-циклы скрыты по умолчанию ​

Цикл, полностью состоящий из import type, не является рантайм-багом — типы стираются при компиляции. Раз это идиоматичный современный стиль, показывать такие циклы по умолчанию означало бы шумную находку почти на любом типизированном проекте:

🧰 devtoolz

Scanned 2 files.

Nothing found. Somebody already did their homework.

--include-types показывает и их тоже, с явной пометкой:

🧰 devtoolz — chores, automated

Scanned 2 files.

1 circular import found:

  src/a.ts
  → src/b.ts
  → src/a.ts
  (type-only — harmless at runtime, types are erased)

Если в цикле есть хотя бы один обычный (не типовый) импорт — цикл считается value-level целиком и виден по умолчанию, даже если остальные импорты в нём типовые.

Инструмент не пытается доказать, что конкретный цикл реально сломан в рантайме — это в общем случае неразрешимая статически задача (обращение может быть отложено внутрь функции, а не стоять на верхнем уровне модуля). Находка сообщает «вот цепочка, оцените сами», не «это точно баг».

Опции ​

--include-types ​

Также показывать циклы, полностью состоящие из import type (по умолчанию скрыты).

--cwd <path> ​

Корень, от которого резолвятся относительные пути.

--ext <list> ​

Через запятую, какие расширения обрабатывать. По умолчанию: .ts,.tsx,.js,.jsx,.cjs,.mjs.

--ignore <glob> ​

Дополнительный паттерн игнорирования (повторяемый) поверх встроенных дефолтов.

--no-respect-gitignore ​

Не учитывать .gitignore проекта.

Пример:

bash
devtoolz circular-imports src                  # только value-level циклы
devtoolz circular-imports src --include-types  # и чисто типовые тоже