Skip to content

Repository files navigation

aliascheck

tsc compiles it. Node throws. aliascheck is the step in between.

CI npm license zero dependencies


The problem

You add path aliases, because relative imports six levels deep are miserable:

// tsconfig.json
"paths": { "@/*": ["src/*"] }
import { greet } from '@/lib/greet';

Your editor is happy. tsc is happy. Then:

$ node dist/index.js
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@' imported from dist/index.js

TypeScript does not rewrite paths when it emits. That is deliberate — the team's position is that module resolution belongs to the host, not the compiler — and it means the alias is copied into the output verbatim. tsc will never warn you. There is no diagnostic for it, because as far as the type system is concerned nothing is wrong.

So you find out somewhere else. In a container. In a Lambda. Or from someone who installed your package, because the same thing happens to your published dist — and then it is their tsc reporting Cannot find module '@/lib/greet' against a path that only ever existed on your machine.

There are plenty of fixes: tsc-alias, a bundler, tsconfig-paths, subpath imports. Every one of them is a remedy you have to already know you need.

aliascheck is the part nobody built: the thing that tells you.

What it does

It reads your build output and looks for aliases that are still there. That is not a heuristic — if dist/index.js contains from '@/lib/greet', that file throws, and no reasoning about your configuration is required to know it.

$ npm run build && npx aliascheck

3 aliases declared · 1 runtime import (1 type-only, erased by tsc)
  ~ a TypeScript-aware dev runtime             dev only — leaves the output as written
  read 3 built file(s) — this verdict is confirmed, not inferred

error   alias-in-published-output dist/index.js:1
  1 unresolved alias import remains in JavaScript this package publishes, starting with `@/lib/greet`.
  so: Anyone who installs this package gets a module that throws ERR_MODULE_NOT_FOUND
      on import. It will not fail in this repository, because the alias resolves
      here — it fails on their machine.
  fix: Rewrite aliases at build time (tsc-alias, or a bundler), or move to
       package.json "imports" subpaths.
    dist/index.js:1 → @/lib/greet

error   alias-in-published-types dist/index.d.ts:1
  1 unresolved alias import remains in the declaration files this package publishes.
  so: The code runs. What breaks is the consumer's typecheck.

If you have not built yet, it falls back to reading your configuration and says so — useful, weaker, and it tells you to build for a definitive answer.

Install

npx aliascheck

Or as a dev dependency:

npm install --save-dev aliascheck

Requires Node 20.10 or newer. Zero dependencies.

Use it

aliascheck                       # current directory
aliascheck ./packages/api        # somewhere else
aliascheck --verbose             # list every alias import and how it is classified
aliascheck --json                # machine-readable
aliascheck --fail-on warning     # stricter gate

In CI, after the build:

- run: npm ci
- run: npm run build
- run: npx aliascheck

Exit codes: 0 clean, 1 findings at or above the threshold, 2 bad usage.

Three things it gets right

Type-only imports are not reported. import type { User } from '@/models' is erased by the compiler and can never fail at runtime. In a codebase that uses aliases mostly for types, nearly every hit is harmless — and saying so is the useful answer. Inline markers are handled too: import { type A, b } still emits, because b is a value.

Declaration files are a separate finding. An alias in dist/index.js throws ERR_MODULE_NOT_FOUND at runtime. The same alias in dist/index.d.ts runs perfectly and breaks the consumer's typecheck instead. Same cause, different symptom, different place to look — so they are reported separately. Bundlers often rewrite the JavaScript and leave the .d.ts alone, which is exactly how this one slips through.

A dev runtime is not a fix. tsx, ts-node and tsconfig-paths/register patch resolution inside the running process. Development works flawlessly and dist stays exactly as broken as it was. aliascheck marks those ~ rather than , and reports it, because it is the case that costs the most: everything passes locally right up until someone else installs the package.

Rules

Rule Severity Meaning
alias-in-published-output error an alias survives in JavaScript you publish — consumers get ERR_MODULE_NOT_FOUND
alias-in-published-types error an alias survives in a .d.ts you publish — consumers' typecheck fails
alias-survives-build error an alias survives in the build output of a private package
no-rewriter-configured warning aliases will be emitted as written and nothing appears to rewrite them
dev-only-resolution warning resolution is patched at runtime; the emitted files are untouched
alias-target-missing info a paths entry points at a directory that no longer exists
alias-unused info a declared alias nothing imports
prefer-subpath-imports info the standard-library answer, which needs no tool at all

Details and worked examples: docs/rules.md.

The fix that makes the tool unnecessary

Node has had a first-class answer since v16, and hardly anyone uses it:

// package.json
"imports": { "#lib/*": "./dist/lib/*.js" }
import { greet } from '#lib/greet';

Node resolves #-prefixed specifiers itself. No build step, no plugin, no extra dependency — and, unlike paths, it works for anyone who installs your package, because the mapping ships in package.json rather than living in a tsconfig they never see.

aliascheck suggests this, reports nothing when you adopt it, and the bundled examples/uses-subpath-imports fixture exists to prove it stays silent. A tool that flagged the correct answer would be worse than no tool.

Try it

git clone https://github.com/hamodywe/aliascheck && cd aliascheck
npm install
node src/cli.ts examples/ships-broken        # every rule fires
node src/cli.ts examples/uses-subpath-imports # silence

Limitations

Stated plainly.

  • Source analysis cannot see types. Without a type checker, import { Foo } from '@/types' cannot be known to be type-only — TypeScript elides it if Foo turns out to be a type. Explicit import type is handled exactly. This is why the built-output check exists, and why the tool is far more useful after npm run build.
  • Specifiers must be literal. import(someVariable) cannot be resolved statically, by this or anything else.
  • files globs are treated as prefixes. "files": ["dist"] is handled precisely; an exotic glob may misjudge whether a file is published. It errs toward reporting.
  • Monorepos are scanned one package at a time. Point it at each package, or run it per workspace in CI.
  • It does not check that the alias target exists in the output. It reports aliases that were not rewritten, not rewrites that pointed somewhere wrong.

FAQ

Isn't this just tsc-alias? tsc-alias is one of the fixes. aliascheck tells you that you need one, and afterwards confirms it worked — including in the .d.ts files, which several fixes quietly skip.

Why not just always use a bundler? Many people do, and then aliascheck reports nothing. It is for the projects that ship plain tsc output, which is most libraries.

Does it work with jsconfig.json? Yes.

Does it modify anything? No. It only reads.

Contributing

The most useful issue is a false verdict in either direction — a project flagged that is genuinely fine, or one that is broken and got a clean report. See CONTRIBUTING.md.

Licence

MIT © hamodywe

References

About

Finds TypeScript path aliases that survive the build and throw MODULE_NOT_FOUND at runtime — the error tsc will never give you.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages