Package Checks
Two commands that look at a package the way a real consumer would, not the way its own build does: exports-doctor checks the paths declared in package.json against what's really on disk, readme-check typechecks code examples from README/docs against the package's own build. Both are read-only — neither writes anything, and neither executes an example's source code, only compiles it / resolves paths.
exports-doctor
Resolves every path package.json declares (main/module/types/typings/bin/exports, including nested condition objects, subpath maps, and fallback arrays) against what's actually on disk.
devtoolz exports-doctor [dir] [options]The command reads a single package.json in the given directory (current directory by default) rather than walking a file tree — so it has no --cwd/--ext/--ignore, only [dir].
Real output — a package whose bin file forgot its shebang, and whose ./cdn subpath resolves fine at runtime but never declares types anywhere:
🧰 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 allThe second case is the sneakiest of them all: import from pkg/cdn genuinely works for any consumer, the package's own build passes without a single error, and yet nobody gets TypeScript hints for that path — no error, no warning, just silent nothing instead of types. Only checking it against what's actually declared under the same exports key catches it.
A case mismatch uses the same mechanics as case-check, just for package.json fields instead of source imports:
🧰 devtoolz, reporting for duty
Checked widget-kit's declared exports.
1 problem found:
main ./dist/index.js wrong case — real path is ./dist/Index.jsWhat's checked
missing— the path doesn't resolve at all, under any case.case-mismatch— it resolves, but under a different case — the real on-disk path is shown.missing-shebang— abinfile exists but doesn't start with#!/usr/bin/env node, so it won't run as an executable.types-missing— a subpath inexportshas at least one runtime condition (import/require/…) that resolves fine, but notypesis declared among that same subpath's conditions anywhere. Doesn't fire for a plain string value with no conditions object at all — for that shape TypeScript already falls back to a colocated.d.tsfile by convention, the same waymaindoes.
Options
[dir]
Package directory to check (positional argument, default: .).
Example:
devtoolz exports-doctor # package.json in the current directory
devtoolz exports-doctor path/to/package # a specific directory
devtoolz exports-doctor --json # machine-readable output, for CIreadme-check
Extracts fenced ts code blocks from README/docs and actually typechecks them with the TypeScript Compiler API against the package's own tsconfig.json.
devtoolz readme-check [dir] [options]The virtual file a block's code gets dropped into lives right at the package root (next to package.json) — so both relative imports and a self-referencing import of the package's own published name (import { x } from 'my-package') resolve exactly as they would for a real consumer, as long as the package's moduleResolution supports it (bundler/node16/nodenext). Nothing ever runs — only compiles.
Real output — a README with two examples, one working and one deliberately wrong, both importing the package by its own published name:
🧰 devtoolz
Typechecked 2 code blocks across 1 file.
1 problem found:
README.md:13:7 ts Type 'string' is not assignable to type 'number'.The first block (const widget = createWidget('main'); console.log(widget.label)) doesn't show up in the report — it genuinely compiles. The second, const count: number = createWidget('main').label, compiles too, but label is a string, not a number — hence the finding.
Each block is checked in isolation
A block is checked on its own, with no shared context from other blocks in the same file. That's a deliberate choice, not a shortcut: a doc snippet is routinely a fragment that assumes a variable set up somewhere in the surrounding prose or in a different, unrelated example ("inside your component:", "given a router instance, …") — in isolation that reads as an undefined name, without necessarily being a real doc bug. So the following classes of TypeScript diagnostics are deliberately dropped rather than shown as findings:
Cannot find nameand its "did you mean" variant — an undefined name, most likely inherited from context outside the block;Cannot find module './x'for a RELATIVE path — almost always a hypothetical file in the reader's own project ("your App.vue"), not something that could exist in the package itself;- "X is declared but its value is never read" — a doc fragment routinely declares a handler that's called somewhere else (a template, a different example), not in this same block;
- a self-referencing import failing to resolve specifically because the package's OWN
tsconfig.jsondoesn't setmoduleResolution: bundler/node16/nodenext— that's a limitation of the package's own environment, not a doc bug.
Cannot find module 'pkg-name' for a BARE package name (self-reference), by contrast, remains a finding — that's the one historically valuable case this command exists for.
Options
--file <path>
Markdown file to check, relative to [dir] (repeatable). Default: README.md.
--lang <list>
Comma-separated fenced-block languages to check. Default: ts only. tsx is deliberately not in the default set — Vue packages typically don't configure JSX/React types, so a tsx block there is almost always illustrative pseudocode the package itself could never compile; opt in explicitly with --lang ts,tsx for a package that actually configures JSX.
--tsconfig <path>
tsconfig.json to read compiler options from (auto-detected in [dir] by default).
Example:
devtoolz readme-check # ts blocks in ./README.md
devtoolz readme-check --file docs/guide.md # another doc instead of/alongside README
devtoolz readme-check --lang ts,tsx # include tsx blocks too