Граф зависимостей
Две команды, которые смотрят на один и тот же граф импортов с разных сторон: unused-deps сверяет его с тем, что заявлено в package.json, circular-imports ищет в нём циклы. Обе — только для чтения, ничего не пишут и не выполняют исходный код.
unused-deps
Находит зависимости в package.json, на которые нет ни одного импорта в коде — и зеркально, «фантомные» зависимости: пакет реально импортируется, но нигде не объявлен.
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'splugins: { 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 слишком рискованно для автоматики, решение — за человеком.
Пример:
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) в собственном коде проекта.
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.tsimport 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 проекта.
Пример:
devtoolz circular-imports src # только value-level циклы
devtoolz circular-imports src --include-types # и чисто типовые тоже