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

@verifyhash/env-diff

v0.1.0

Published

KEY-aware .env differ and .env.example generator: a zero-dependency, pure dotenv-subset parser + semantic key-level diff that reports unparsed lines and duplicate keys honestly and never leaks a value.

Readme

env-diff

A KEY-aware .env differ and .env.example generator — the semantic counterpart to a plain line-based text diff. It compares two dotenv files by variable, not by line: it tells you which keys are missing, which are extra, and which changed — regardless of ordering, comments, or quoting style — and it turns a real .env (which may hold live secrets) into a safe-to-commit .env.example template.

Zero dependencies, zero network, zero filesystem. Three pure functions over strings that never leak an env value.

This is not a duplicate of polyatic's line-based /diff-checker/. That tool compares raw text lines; env-diff understands KEY=value structure, so re-ordering keys or adding a comment produces no false diff.

Who it's for

Developers eyeballing two .env files — for example checking a local .env against the committed .env.example, or reconciling .env.staging with .env.production — who want to know which variables differ without hand- scanning line by line, and who want a committable .env.example so teammates stop hitting "works on my machine" boot failures.

Install

# The final scoped name is the owner's call (see "Publishing" below).
npm install @verifyhash/env-diff   # placeholder name — subject to change
const { parseEnv, diffKeys, toExample } = require('@verifyhash/env-diff');

Zero runtime dependencies, zero devDependencies — nothing else to install.

API

| Function | Signature | Returns | | --- | --- | --- | | parseEnv | parseEnv(text) | { entries, unparsed, duplicates } — parse the common dotenv subset | | diffKeys | diffKeys(leftText, rightText) | { onlyLeft, onlyRight, valueDiffers, same, duplicates, unparsed } — a KEY-level semantic diff | | toExample | toExample(text, opts?) | a ready-to-commit .env.example string, every value blanked |

opts for toExample is { stripComments?: boolean } — see below. Full, precise TypeScript types ship in index.d.ts.

Runnable example

const { parseEnv, diffKeys, toExample } = require('@verifyhash/env-diff');

// 1) parseEnv — structured view of a .env
parseEnv('export API_KEY="abc 123"\n# comment\nPORT=8080\n');
// {
//   entries: [
//     { key: 'API_KEY', value: 'abc 123', line: 1, quoted: 'double', exported: true },
//     { key: 'PORT',    value: '8080',    line: 3, quoted: false,    exported: false }
//   ],
//   unparsed: [],
//   duplicates: []
// }

// 2) diffKeys — which VARIABLES differ (never the values themselves)
diffKeys('A=1\nB=2\nSHARED=x\n', 'B=9\nC=3\nSHARED=x\n');
// {
//   onlyLeft:     ['A'],
//   onlyRight:    ['C'],
//   valueDiffers: [{ key: 'B' }],   // <-- key only; the values 2 vs 9 are NOT emitted
//   same:         ['SHARED'],
//   duplicates:   { left: [], right: [] },
//   unparsed:     { left: [], right: [] }
// }

// 3) toExample — a committable .env.example (values blanked)
toExample('export API_KEY="abc"\n# note\nPORT=8080\n');
// export API_KEY=
// # note
// PORT=

parseEnv(text)

Returns { entries, unparsed, duplicates }:

  • entries[{ key, value, line, quoted, exported }], one per assignment in source order.
    • value is the unquoted value (surrounding matching quotes stripped); '' for KEY=.
    • quoted is 'single', 'double', or false. A value quoted single vs double but otherwise identical yields the same value.
    • exported is true when the line used the export prefix.
  • unparsed[{ line, text, reason }] for lines it could not parse: lines with no =, invalid keys, ${...}/$VAR interpolation (kept literal, not resolved), and multiline continuations (trailing \).
  • duplicates[{ key, lines }], one row per key that appears more than once, listing all of its line numbers. Every occurrence still appears in entries; the last occurrence is the effective (last-wins) value.

diffKeys(leftText, rightText)

Parses both sides with parseEnv and returns a six-field, KEY-level diff:

  • onlyLeft / onlyRight[key, ...] bare key strings present on only one side (in that side's first-seen order).
  • valueDiffers[{ key }, ...] for keys present on both sides whose effective (last-wins) values differ. Keys only, by design — the differing values are never emitted (they are secrets; the caller decides how, if at all, to display them).
  • same[key, ...] for keys on both sides with equal effective values.
  • duplicates / unparsed{ left, right }, each side's parseEnv report passed through unchanged.

Because the diff is computed on the parsed key sets, it is insensitive to line order, to comments, and to quote style: KEY="1" and KEY=1 compare as same.

toExample(text, opts).env.example generator

Returns a ready-to-commit .env.example string with every recognized value blanked to KEY=. It walks the raw input line by line (so it does not depend on the parser to preserve comments), and:

  • blanks each recognized value while preserving the export prefix, the key, and original line/key order;
  • keeps comments and blank lines by default; pass { stripComments: true } to drop full-line comments (blank lines are always kept);
  • collapses a duplicate key to a single blanked line at its first occurrence (later duplicate lines are dropped);
  • carries any unparsed line (e.g. ${VAR} interpolation, trailing-\ multiline, invalid keys) through verbatim, each preceded by a # env-diff: unparsed marker so nothing is silently lost;
  • is deterministic: the same input yields byte-identical output. Empty input yields ''.
  • is idempotent: toExample(toExample(x)) === toExample(x). Re-processing an already-generated template is a fixed point — values are already blank, duplicates already collapsed, and an unparsed line whose marker survived from the previous pass is not re-marked (two consecutive unparsed lines in the original input still each get their own marker).

What is parsed vs reported

| Input | Result | | ---------------------------- | -------------------------------------------------- | | KEY=value | entry | | export KEY=value | entry, exported: true | | KEY="a b" / KEY='a b' | entry, quotes stripped, quoted records the style | | KEY= | entry, value: '' | | blank line / # comment | skipped (not an entry, not unparsed) | | NO_EQUALS | unparsed — no = | | KEY=${OTHER} | unparsed — interpolation kept literal, not resolved | | KEY=value\ (trailing \) | unparsed — multiline continuation unsupported |

Honest limits

The .env "format" is loose and has no single standard. This library parses the common dotenv subset and is deliberately conservative — it never guesses:

  • It is not a shell parser. It understands KEY=value, export KEY=value, and single/double surrounding quotes. That's it. Anything else is reported in unparsed, never silently reinterpreted.
  • It does NOT resolve ${FOO} / $FOO interpolation. Such values are kept as literal text and flagged as unparsed so you are never misled into thinking a variable was expanded.
  • No multiline / heredoc values. A line ending in a trailing backslash is reported as an unsupported multiline continuation rather than joined.
  • No escape-sequence decoding inside quotes. A double-quoted \n stays the two characters \ and n; the value text between the quotes is taken literally.
  • No inline # comment stripping. A # is a comment only when it is the first non-blank character of a line (a full-line comment). A # after a value — KEY=val # note or KEY="a # b" — is part of the value, quoted or not. The parser does not guess where an unquoted value ends, so it never truncates one at a #.
  • Diffing is by effective (last-wins) value, matching dotenv runtime semantics for duplicated keys. Duplicates are still surfaced separately so you can see the drift.

When in doubt, a line lands in unparsed with an honest reason string — the library would rather tell you it didn't understand a line than pretend it did.

Redaction & trust

Env files routinely contain live secrets — API keys, DB passwords, tokens. This library is built so a value can never escape:

  • diffKeys never emits a value. valueDiffers carries { key } objects only; same / onlyLeft / onlyRight are bare key strings. Whether to show any value at all is the caller's decision, not the library's.
  • toExample blanks every recognized value to KEY= — that is its whole job: produce a template that is safe to commit.
  • Pure by construction. These modules have no network and no filesystem access — they are functions from string to data. They cannot exfiltrate anything, and the accompanying browser UI runs entirely client-side, so nothing you paste ever leaves your device.

This redaction discipline is a standing rule for the project, not a convenience default — see GO-LIVE.md.

Package layout & publishing

The npm entry point is a small index.js that re-exports the three core modules, with main and types pointing at index.js / index.d.ts. We chose the re-export approach (over keeping parse.js as main with an ambient multi-module .d.ts) because it gives consumers one obvious importrequire('@verifyhash/env-diff') yields parseEnv, diffKeys, and toExample together — matches the sibling incubator libraries' layout, and still leaves each module independently require-able (e.g. require('@verifyhash/env-diff/parse.js')) and browser-loadable via the globalThis.EnvDiff namespace the staged UI uses.

npm pack ships only the runtime surface — the core modules, the entry, the types, this README, the LICENSE, and package.json. The UI files (app.js, index.html, style.css) and test/ are intentionally excluded via the files whitelist.

The scoped package name @verifyhash/env-diff above is a placeholder — the final published name is the owner's call (see GO-LIVE.md).

Running the tests

To run the tests, from the package directory:

cd env-diff
npm test

npm test runs the hand-verified golden vectors in test/parse.test.js, test/diff.test.js, and test/example.test.js (14 parse + 6 diff + 6 example blocks). Each assertion fails the process (non-zero exit) on mismatch.

License

MIT — see LICENSE. Copyright (c) 2026 verifyhash.