npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

envconform

v1.2.0

Published

Detect and fix drift between your .env, .env.example, and the env vars your code actually uses.

Readme

envconform

CI npm version license

Detect drift between your .env, .env.example, and the env vars your code actually uses — before it breaks a teammate's setup or your CI pipeline.

The problem

Environment variables live in three places that constantly fall out of sync:

  • Template files (.env.example) — the documented contract of what a project needs.
  • Actual value files (.env, .env.local, ...) — the real values on a given machine, usually gitignored.
  • Source code — where variables are actually read.

A developer adds a new process.env.STRIPE_KEY in code but forgets to add it to .env.example. The next person who clones the repo gets a runtime crash instead of a clear error. .env.example also tends to accumulate dead keys nobody uses anymore, and nothing catches either problem until it breaks something.

What envconform does

envconform doesn't diff files against each other — it builds three sets of keys and reports where they disagree:

| Set | Built from | Meaning | |------------|--------------------------------------|-----------------------------------| | DECLARED | all template files (.env.example) | what the project says it needs | | PRESENT | all actual-value files (.env, ...) | what values actually exist | | USED | env reads found in source code | what the code actually uses |

Every finding keeps its provenance — the exact file and line it came from — so the report always says where, not just what.

$ envconform

envconform — 3 issues found (comparing against: .env.example)

🔴 Used in code, missing from template
   STRIPE_SECRET_KEY        src/payments.js:1

🟡 Has a value but missing from template
   SHARED_SECRET            .env:2

🔵 Documented in template but missing locally
   OLD_TOKEN                .env.example:2

Env vars used in code but never documented (🔴) is the headline check — the class of bug that crashes a teammate's first npm run dev after a clone.

"Template" always means an actual file — the (comparing against: ...) line names it, and lists every one if you have more than one (e.g. a monorepo with .env.example at the root and another under apps/web/). If no template file is found, every key gets flagged, but envconform says so explicitly instead of leaving you to guess why — and tells you what to do about it, whether that's generating one or pointing envconform at one you already have:

$ npx envconform

⚠️  No template file found (e.g. .env.example) — 'template' below refers to
   that missing file. Every key is flagged because there is nothing to
   compare against yet.
   Run `npx envconform init` to generate one from what you already have, or —
   if you already have one under a different name (not ending in .example/.sample/
   .template) — run `npx envconform config init` to create envconform.config.json,
   then add its exact filename to "templates" there.

envconform — 12 issues found
...

"Not found" and "doesn't exist" aren't the same thing. envconform only recognizes a template by filename pattern (below) — it never reads file contents to guess. If your team names its template something that doesn't match the pattern (env.rules, vars.example.txt, whatever), envconform will report zero template files, even though you have one. There's usually no envconform.config.json yet either at that point — npx envconform config init creates one with every field already set to its real default, so you're editing a working file, not writing JSON from scratch — see Configuration.

Install

npx envconform

No install needed for a one-off run. To use it regularly:

npm install --save-dev envconform

Usage

npx envconform              # human-readable report
npx envconform --json        # machine-readable report
npx envconform --ci           # non-zero exit code if a var is used in code but undocumented

(Drop the npx prefix only if you've installed it globally with npm install -g envconform — otherwise a bare envconform won't be on your PATH and the shell will tell you the command doesn't exist.)

Run it from the root of a project. It walks the whole tree (skipping node_modules, .git, dist, build, .next, coverage) looking for template files and actual-value files — env files are read even if .gitignore excludes them, since that's normal for .env/.env.local. It also scans JS/TS source files (respecting .gitignore this time) for env var reads, and prints any drift between what's declared, what's present, and what's used.

How it decides what's a template, by default: any filename ending in .example, .sample, or .template (case-insensitive) — so .env.example, .env.sample, .env.template all just work, and so would something like config.sample. This is a filename pattern, not a content check — envconform never opens a file to guess whether it "looks like" a template.

How it decides what's an actual-value file: .env itself, or anything starting with .env. that isn't a template (.env.local, .env.development, .env.production.local, etc.).

If your naming doesn't match either pattern, envconform won't find your template on its own — it'll report zero template files, not an error, so the failure mode is silent unless you know to look for it. Add the exact filename to envconform.config.json's templates (or actual) array and it's treated as one from then on, regardless of what it's called — see Configuration.

More flags (--env, --example, --src, --ignore) are on the roadmap — use a config file (below) to set these for now.

NODE_ENV is ignored by default — it's set by the runtime/framework itself, never something you declare in .env.example. When a key is used in a lot of places, the table view shows the first 4 locations and +N more to keep the report scannable (--json always includes every location in full).

envconform init / envconform --fix

envconform can also write your template for you — it never copies a real value into it, only key names and provenance comments.

npx envconform init                    # create .env.example when none exists
npx envconform --fix                    # add only the keys used in code but missing from an existing template
npx envconform init --placeholders       # same as init, but infer non-secret placeholders from value shape

init builds the file from the union of every key used in code and every key present in an actual-value file. It refuses to run (exit 2) if a template already exists — use --fix instead. --fix only appends the 🔴 used-but-undocumented keys to the end of the existing file; it never reorders, edits, or removes anything already there.

# Generated by envconform — fill in real values in your local .env
# Used in: src/payments.ts
STRIPE_SECRET_KEY=

# Used in: src/cache.ts  (also found in .env.local)
REDIS_URL=

DATABASE_URL=

--placeholders (off by default) fills in a placeholder inferred from a present value's shape only — never the value itself: a URL-shaped value becomes http://localhost:PORT, true/false becomes false, a number becomes 0, anything else stays blank. This is enforced structurally: the function that renders a template entry never receives the real value at all, only one of those four fixed strings.

Configuration

Everything works with zero config. To override defaults, add envconform.config.json at the project root (or an "envconform" key in package.jsonenvconform.config.json wins if both exist). Don't want to write the JSON by hand? Generate a starter file with every field already set to its real default:

npx envconform config init
{
  "templates": [".env.staging"],       // force-classify extra filenames as templates
  "actual": [],                          // force-classify extra filenames as actual-value files
  "src": ["src", "app"],                // restrict JS/TS scanning to these dirs (default: whole tree)
  "ignore": ["DEBUG_PORT", "CI"],       // keys to never report (default: ["NODE_ENV"] — this replaces it, not adds to it)
  "failOn": ["used-undocumented"],      // finding categories that make --ci exit 1 (this is the default)
  "placeholders": false                  // default for --placeholders on init/--fix
}

(That's annotated for reference — real JSON can't have comments, so the file config init actually writes has no // lines, just the six fields at their real defaults, ready to edit.)

Every field is optional and only overrides its own default — templates and actual add exceptions on top of the built-in name patterns (they don't replace them), while ignore, failOn, src, and placeholders replace their default outright when present. That means if you set your own ignore list, it replaces the default ["NODE_ENV"] rather than adding to it — include "NODE_ENV" yourself if you still want it filtered.

Exit codes

| Code | Meaning | |------|------------------------------------------------------------------| | 0 | No blocking findings (or --ci wasn't passed) | | 1 | --ci was passed and a var is used in code but undocumented (🔴) | | 2 | Usage error — unknown flag or unreadable file |

CI

# .github/workflows/envconform.yml
name: envconform
on: [pull_request]

jobs:
  check-env:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npx envconform --ci

How it works

envconform is a four-stage pipeline, kept strictly separated:

  1. Parsers read .env-format files into { key, value, file, line } records — no evaluation, no expansion.
  2. Classifier decides whether a file is a template or an actual-value file, by name pattern.
  3. Scanner walks the source tree (respecting .gitignore) and delegates to a per-language detector behind a stable Detector interface — the JS/TS detector parses each file into a real AST (@babel/parser, with the typescript/jsx plugins as appropriate) rather than pattern-matching raw text, so process.env.X inside a comment or a string literal is correctly ignored instead of producing a false positive. A file that fails to parse is skipped rather than crashing the scan.
  4. Reconciler is pure set logic — no I/O — that takes the DECLARED, PRESENT, and USED sets and returns a list of findings.
  5. Reporter formats a Report into human-readable table output or --json; renderers never compute findings, only format them.
  6. Generator (init/--fix) is also pure — it builds template text from key names, file provenance, and (optionally) a shape-inferred placeholder, never from the real EnvRecord.value. The CLI is the only layer that touches the filesystem for it.
  7. Config (envconform.config.json) is loaded once at the top of the CLI and threaded through as plain data — classification, scanning, and reconciliation all stay pure and config-agnostic in isolation; only the CLI knows a config file exists.

Roadmap

  • [x] v0.1 — env-to-env diff
  • [x] v0.2 — scan JS/TS source for process.env.* reads, flag undocumented usage
  • [x] v0.3 — --json output, --ci mode with exit codes
  • [x] v0.4 — multiple env files, full provenance, .env.local severity nuance
  • [x] v0.5 — envconform init / envconform --fix (generate and patch templates, never leaking real values)
  • [x] v0.6 — config file, shape-based placeholders
  • [x] v0.7 — AST-based JS/TS detector
  • [x] v1.0 — polish, docs, publish to npm
  • [x] v1.1 — real-world polish from dogfooding: explicit no-template notice, names the actual template file(s) instead of the abstract word "template", default-ignores NODE_ENV, truncates long location lists in the table view
  • [x] v1.1.1 — a declared key that's both unused and missing locally no longer shows up as two separate findings — it's one fact ("safe to remove"), not a duplicate
  • [x] v1.1.2 — the no-template notice now proactively points to envconform.config.json for teams with a non-standard template filename, instead of only documenting it
  • [x] v1.1.3 — every self-referential command the tool prints (in error messages and the no-template notice) now says npx envconform ... explicitly, since a bare envconform only works if it's installed globally
  • [x] v1.2.0 — envconform config init scaffolds envconform.config.json with the real defaults already filled in, since telling someone to "add it to config" isn't useful advice when they don't have a config file yet and don't know its shape. The no-template notice now points to this command directly.

Development

npm install
npm test         # vitest (builds first via pretest)
npm run coverage   # vitest with coverage — reconcile.ts is held to a 90%+ threshold
npm run typecheck
npm run build      # bundles src/cli.ts to dist/cli.js via tsup

License

MIT — see LICENSE.