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

zodenvy

v1.1.0

Published

Generate type-safe, Zod-validated TypeScript schemas from your .env. CLI that produces autocomplete, runtime validation, and a synced .env.template.

Readme

zodenvy

Generate type-safe, Zod-validated TypeScript schemas from your .env file. One command — autocomplete, runtime validation, and a .env.template kept automatically in sync.

Install

npm install --save-dev zodenvy zod
# or
pnpm add -D zodenvy zod
# or
yarn add -D zodenvy zod

zod is a peer dependency (≥ 3.22).

Quick start

Given a .env like this:

API_URL=https://api.example.com
DB_PORT=5432
DEBUG=false
# @default production
NODE_ENV=

Run:

npx zodenvy

You get src/constants/env.generated.ts:

// Auto-generated by zodenvy — DO NOT EDIT
import { z } from "zod";

export const ENV_NAMES = ["API_URL", "DB_PORT", "DEBUG", "NODE_ENV"] as const;

export const envSchema = z.object({
	API_URL: z.string().min(1),
	DB_PORT: z.coerce.number(),
	DEBUG: z.enum(["true", "false"]).transform((v) => v === "true"),
	NODE_ENV: z.string().default("production"),
});

export type EnvironmentVariables = z.infer<typeof envSchema>;

export const getEnvs = (): EnvironmentVariables => {
	return envSchema.parse({
		API_URL: process.env.API_URL,
		DB_PORT: process.env.DB_PORT,
		DEBUG: process.env.DEBUG,
		NODE_ENV: process.env.NODE_ENV,
	});
};

The accessor uses static literal process.env.FOO reads (not a process.env[name] loop) so it works correctly with every bundler that statically inlines env vars — Webpack/Next.js DefinePlugin, Vite, esbuild, Rollup.

…and a .env.template next to your .env, ready to commit.

CLI options

| Flag | Default | Description | | -------------------- | ---------------------- | ---------------------------------------------------------------------------------- | | --out-dir <dir> | src/constants | Output directory (relative to the .env file, or absolute) | | --out-file <file> | env.generated.ts | Output filename | | --framework <name> | auto | auto | node | next | vite | astro | cloudflare | deno | bun | | --env-source <s> | auto | Legacy: auto | process.env | import.meta.env (overrides framework) | | --type-name <name> | EnvironmentVariables | Exported TypeScript type name | | --env-file <path> | ./.env | Path to the .env file | | -r, --recursive | false | Walk the current directory tree and process every package's .env (monorepo) | | -h, --help | — | Show usage | | -v, --version | — | Show installed version |

Framework support

--framework auto (the default) inspects the neighboring package.json and picks an adapter:

| Adapter | Detected by | Accessor | Server/client split | | ------------ | -------------------------------------------- | -------------------------- | ------------------------------ | | next | next dependency | process.env.FOO | NEXT_PUBLIC_* → public split | | astro | astro dependency | import.meta.env.FOO | PUBLIC_* → public split | | vite | vite / @vitejs/* dependency | import.meta.env.FOO | VITE_* → public split | | cloudflare | wrangler / @cloudflare/workers-types dep | env.FOO (request-scoped) | factory: createGetEnvs(env) | | bun | bun / bun-types / @types/bun dep | Bun.env.FOO | — | | deno | (explicit --framework deno) | Deno.env.get("FOO") | — | | node | fallback | process.env.FOO | — |

When an adapter has a server/client split (Next.js, Vite, Astro), the generated file additionally exports publicEnvSchema, PublicEnvironmentVariables, and getPublicEnvs() — a subset containing only variables whose name starts with the framework's public prefix. Import getPublicEnvs() in client code, getEnvs() on the server.

Override the framework explicitly with --framework next (or any name above). The legacy --env-source flag still works and takes precedence — handy if you have an old config you don't want to touch.

Annotations

Comments directly above a variable override the inferred type:

# @type number
SOME_STRING_THAT_IS_ACTUALLY_A_PORT=8080

# @optional
FEATURE_FLAG=

# @default production
NODE_ENV=

| Annotation | Effect | | --------------------------------- | ----------------------------- | | # @type number\|boolean\|string | Override the inferred type | | # @optional | Mark as optional | | # @default <value> | Set default, implies optional |

Annotations are cleared by blank lines — they apply only to the immediately following variable.

Type inference defaults:

| Value | Inferred type | | ---------------- | ------------------ | | true / false | boolean | | 3000, 0.5 | number | | anything else | string | | (empty) | string, optional |

Runtime usage

Option A — use the generated getEnvs

The generated file already exports getEnvs:

import { getEnvs } from "./constants/env.generated";

const env = getEnvs();
console.log(env.DB_PORT); // number

Next.js: server vs client

With --framework next the generated file exports both getEnvs (full schema, server-only) and getPublicEnvs (only NEXT_PUBLIC_* vars, safe to call in the browser):

// app/page.tsx (Server Component) — full env
import { getEnvs } from "@/constants/env.generated";
const env = getEnvs();
fetch(`${env.API_URL}/data`, { headers: { Authorization: env.DB_PASSWORD } });

// app/header.tsx ("use client") — only public env
("use client");
import { getPublicEnvs } from "@/constants/env.generated";
const publicEnv = getPublicEnvs();
console.log(publicEnv.NEXT_PUBLIC_API_URL);

Because the generated accessor uses static process.env.NEXT_PUBLIC_FOO reads (not a dynamic loop), Next.js's webpack DefinePlugin inlines the values at build time and the client bundle gets real strings — no more undefined.

Cloudflare Workers — request-scoped env

With --framework cloudflare the generated file exports a createGetEnvs factory instead, because Workers receive env from the request context:

// src/index.ts
import { createGetEnvs } from "./constants/env.generated";

export default {
	async fetch(req: Request, env: Env) {
		const envs = createGetEnvs(env as Record<string, string | undefined>)();
		return new Response(envs.API_URL);
	},
};

Option B — createGetEnvs factory (any runtime)

For any runtime where the built-in adapters don't fit (Deno without --framework deno, an embedded engine, a test harness), import the runtime factory and pass your own env source:

import { createGetEnvs } from "zodenvy";
import { envSchema } from "./constants/env.generated";

export const getEnvs = createGetEnvs(envSchema, import.meta.env);
// or: createGetEnvs(envSchema, Deno.env.toObject())
// or: createGetEnvs(envSchema, request.env)

createGetEnvs parses and validates on the first call, then returns the cached result. It accepts any Record<string, string | undefined> as the source — fully framework-agnostic.

.env.template sync

Every run automatically syncs .env.template next to your .env:

  • New variables are appended with an empty value
  • Variables removed from .env are removed from the template

Commit .env.template to version control so teammates know which variables are required.

Monorepo mode (--recursive)

For monorepos, run from the repository root:

npx zodenvy --recursive

The CLI walks the current directory, finds every .env whose folder contains a src/ directory, and for each one:

  • writes <out-dir>/<out-file> next to the .env
  • syncs .env.template next to the .env

Ignored: node_modules, dist, build, .git, .next, .turbo, coverage, and any directory whose name starts with ..

Programmatic API

zodenvy also exports the building blocks if you want to script your own pipeline or build a custom framework adapter:

import {
	EnvParser,
	CodeGenerator,
	TemplateSync,
	createGetEnvs,
	nodeAdapter,
	nextAdapter,
	cloudflareAdapter,
	type Adapter,
} from "zodenvy";
  • EnvParser — parses .env content into typed entries.
  • CodeGenerator — turns entries into the generated TypeScript source (accepts an adapter option).
  • TemplateSync — keeps .env.template in sync with the parsed entries.
  • createGetEnvs — runtime factory that parses, validates and caches.
  • nodeAdapter, nextAdapter, viteAdapter, astroAdapter, cloudflareAdapter, denoAdapter, bunAdapter — built-in framework adapters.
  • Adapter — type for custom adapters; pass one to CodeGenerator to emit any accessor pattern.

License

MIT © Patryk Barć