Проверка пакета
Две команды, которые смотрят на пакет так же, как смотрел бы настоящий потребитель, а не как смотрит его собственная сборка: exports-doctor сверяет пути, заявленные в package.json, с тем, что реально лежит на диске, readme-check тайпчекает код-примеры из README/доков против собственной сборки пакета. Обе — только для чтения, ничего не пишут и не выполняют исходный код примеров, только компилируют/резолвят пути.
exports-doctor
Резолвит каждый путь, который package.json заявляет (main/module/types/typings/bin/exports, включая вложенные condition-объекты, subpath-карты и fallback-массивы), против того, что реально лежит на диске.
devtoolz exports-doctor [dir] [options]Команда читает один package.json в указанной директории (по умолчанию — текущая), а не обходит дерево файлов — поэтому у неё нет --cwd/--ext/--ignore, только [dir].
Реальный вывод — пакет, где bin-файл забыл про shebang, а ./cdn-subpath резолвится в рантайме, но у него нигде не объявлены типы:
🧰 devtoolz — chores, automated
Checked widget-kit's declared exports.
2 problems found:
bin.widget-kit ./dist/cli.js missing '#!/usr/bin/env node' — won't run as a bin
exports["./cdn"] ./dist/cdn/index.js runtime resolves fine, but no types declared for this entry at allВторой случай — самый коварный из всех: import из pkg/cdn реально сработает у любого потребителя, сборка пакета пройдёт без единой ошибки, а TypeScript-подсказок для этого пути не будет ни у кого — ни ошибки, ни предупреждения, просто тихая пустота вместо типов. Ловится только сверкой с тем, что реально объявлено под тем же ключом exports.
Несовпадение регистра — та же механика, что у case-check, только для полей package.json, а не для импортов исходников:
🧰 devtoolz, reporting for duty
Checked widget-kit's declared exports.
1 problem found:
main ./dist/index.js wrong case — real path is ./dist/Index.jsЧто проверяется
missing— путь не резолвится вообще, ни при каком регистре.case-mismatch— резолвится, но не тем регистром — показывается реальный путь на диске.missing-shebang—bin-файл существует, но не начинается с#!/usr/bin/env node, так что не запустится как исполняемый.types-missing— у subpath'а изexportsесть хотя бы одно рантайм-условие (import/require/...), которое резолвится нормально, но среди условий того же subpath'а нигде не объявленtypes. Не срабатывает на простом строковом значении без condition-объекта вовсе — для такой формы TypeScript и так подхватывает соседний.d.tsпо конвенции, как уmain.
Опции
[dir]
Директория пакета для проверки (позиционный аргумент, по умолчанию — .).
Пример:
devtoolz exports-doctor # package.json в текущей директории
devtoolz exports-doctor path/to/package # конкретная директория
devtoolz exports-doctor --json # машиночитаемый вывод, для CIreadme-check
Вытаскивает ts-блоки кода из README/доков и реально тайпчекает их через TypeScript Compiler API против собственного tsconfig.json пакета.
devtoolz readme-check [dir] [options]Виртуальный файл, в который подставляется код блока, кладётся прямо в корень пакета (рядом с package.json) — поэтому резолвятся и относительные импорты, и self-reference импорт по собственному опубликованному имени пакета (import { x } from 'my-package'), в точности как у настоящего потребителя, если moduleResolution пакета это поддерживает (bundler/node16/nodenext). Ничего не выполняется — только компилируется.
Реальный вывод — README с двумя примерами: рабочим и намеренно неверным, оба импортируют пакет по его собственному опубликованному имени:
🧰 devtoolz
Typechecked 2 code blocks across 1 file.
1 problem found:
README.md:13:7 ts Type 'string' is not assignable to type 'number'.Первый блок (const widget = createWidget('main'); console.log(widget.label)) в отчёте не появляется — он реально компилируется. Второй, const count: number = createWidget('main').label, компилируется тоже, но label — строка, а не число, поэтому и находка.
Каждый блок проверяется изолированно
Блок проверяется сам по себе, без общего контекста с соседними блоками того же файла. Это осознанный выбор, а не упрощение: док-сниппет часто представляет собой фрагмент, который опирается на переменную, заданную где-то в окружающей прозе или в другом, несвязанном примере («внутри вашего компонента:», «имея экземпляр router, ...») — в изоляции это выглядит как неопределённое имя, но не обязательно является реальной ошибкой в доке. Поэтому следующие классы диагностик TypeScript намеренно не показываются как находки:
Cannot find nameи его вариант «did you mean» — неопределённое имя, скорее всего унаследованное из внешнего по отношению к блоку контекста;Cannot find module './x'для ОТНОСИТЕЛЬНОГО пути — почти всегда гипотетический файл в проекте читателя («ваш App.vue»), а не то, что могло бы существовать в самом пакете;- «X is declared but its value is never read» — док-фрагмент регулярно объявляет обработчик, вызываемый не в этом же блоке (из шаблона, из другого примера);
- ошибка резолва self-reference-импорта именно из-за того, что СОБСТВЕННЫЙ
tsconfig.jsonпакета не включаетmoduleResolution: bundler/node16/nodenext— это ограничение окружения самого пакета, а не ошибка в доке.
Cannot find module 'pkg-name' для ГОЛОГО имени пакета (self-reference) при этом остаётся находкой — это и есть тот самый исторически ценный случай, ради которого команда существует.
Опции
--file <path>
Markdown-файл для проверки, относительно [dir] (можно указать несколько раз). По умолчанию — README.md.
--lang <list>
Через запятую, языки фенс-блоков для проверки. По умолчанию — только ts. tsx намеренно не включён по умолчанию — Vue-пакеты обычно не настраивают JSX/React-типы, так что tsx-блок там почти всегда иллюстративный псевдокод, который сам пакет не смог бы скомпилировать в принципе; добавляйте явно через --lang ts,tsx для пакета, который реально настраивает JSX.
--tsconfig <path>
tsconfig.json, из которого читать compiler options (по умолчанию автоопределяется в [dir]).
Пример:
devtoolz readme-check # ts-блоки в ./README.md
devtoolz readme-check --file docs/guide.md # ещё один документ вместо/вместе с README
devtoolz readme-check --lang ts,tsx # включить и tsx-блоки тоже