esm-env

Zero-dependency export-conditions library that resolves BROWSER, DEV, and NODE constants at build time instead of runtime.

Library
npm
v1.2.2
234stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
33/100Needs Attention
Development Activity4
Maintenance20
Community28
Maturity52
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
55/100Fair
Architecture75
Code Quality35
Innovation65
Learning Curve45

esm-env is a tiny, zero-dependency package that exposes three boolean constants — BROWSER, DEV, and NODE — describing the environment code is running in. Instead of checking typeof window or process.env.NODE_ENV at runtime, it relies on package.json export conditions so bundlers like Vite, Rollup, and webpack (and runtimes like Node.js and Bun) resolve the correct value at build or import time, letting dead code elimination strip the unused branch entirely.

It ships with fallback files for bundlers that don’t set matching conditions, so consumers get a sane default (checking for a browser-like global for BROWSER, inspecting NODE_ENV for DEV) rather than an error. The library backs environment detection inside tools including Vite and SvelteKit, where correctly discriminating between server and client, or dev and prod, at build time avoids bundling unnecessary code paths.

What You Get

  • BROWSER constant - a boolean resolved via the browser export condition, true when bundled for a browser environment.
  • DEV constant - a boolean resolved via the development export condition, true unless the bundler or runtime explicitly sets a production condition.
  • NODE constant - a boolean resolved via the node export condition, true when running under Node.js’s own conditional-exports resolution.
  • Runtime fallbacks - browser-fallback.js and dev-fallback.js infer a reasonable value for bundlers that don’t set the matching export condition, instead of erroring.
  • Zero runtime dependencies - the entire package is a handful of one-line ESM files with no dependency tree to audit or bloat a bundle.

Common Use Cases

  • Stripping dev-only code from production bundles - libraries wrap debug logging or prop-type checks in if (DEV) so bundlers dead-code-eliminate them from production builds.
  • Universal libraries branching on browser vs. server - packages that behave differently in SSR vs. the browser import BROWSER instead of feature-detecting window at runtime, which fails at build-time static analysis.
  • Framework internals like Vite and SvelteKit - build tools use esm-env to select environment-specific behavior at bundle time, keeping the decision static and tree-shakeable.
  • Verifying bundler configuration across tools - the repo’s own playgrounds/rollup and playgrounds/webpack projects demonstrate configuring exportConditions/conditionNames so consumers can check their own bundler resolves the intended constant.

Under The Hood

Architecture esm-env’s architecture is entirely declarative, living in the package.json exports map for packages/esm-env rather than in runtime logic: index.js re-exports three named bindings (BROWSER, DEV, NODE) from three synthetic subpaths (esm-env/browser, esm-env/development, esm-env/node), and each subpath’s exports entry lists condition keys (browser, development, production, node, default) pointing at one-line files (true.js, false.js, browser-fallback.js, dev-fallback.js) that a bundler or runtime resolves at import/build time based on which conditions it declares. There is no dependency injection, shared state, or execution path beyond module resolution — the fallback files exist purely to give a sane default when the consumer’s bundler doesn’t set a matching condition. Extending the core abstraction would mean adding a new condition key and a corresponding fallback file, a well-contained, low-risk change given how few files the whole package comprises.

Tech Stack The package is plain ESM JavaScript ("type": "module") with zero runtime dependencies, hand-written index.d.ts type declarations and no TypeScript build step, developed inside a pnpm workspace monorepo (esm-env-monorepo) alongside two throwaway test packages, playgrounds/rollup and playgrounds/webpack, which pin @rollup/plugin-node-resolve/rollup and webpack/webpack-cli purely to exercise export-condition resolution against real bundlers. Releases are automated via Changesets and a GitHub Actions workflow that runs pnpm changeset:publish with npm provenance on pushes to main, so there is no custom build pipeline for the published artifact — the source files are the published files.

Code Quality There is no automated test suite in the repository — no test runner config and no test files were found; correctness of export-condition resolution is instead exercised manually through the two playground packages, which each import DEV and build a bundle to confirm the intended condition resolves. Error handling is minimal by necessity: the dev fallback uses optional chaining to avoid throwing when process is undefined, addressing a real bug noted in the 1.2.2 changelog (a fix for a warning when the environment couldn’t be determined). No linter or formatter configuration is present, and CI is limited to the release workflow rather than a lint or test gate.

API Design The public API is about as low-friction as it gets: three self-explanatory named exports (BROWSER, DEV, NODE) with zero configuration objects, hooks, or setup calls — consumers import the constant they need and get a boolean. Type declarations ship inline so there’s no separate types package to install, and the README is a single concise page walking through exactly which conditions flag to add for Node, Bun, Vite/Vitest, and webpack. The one added-friction point for a first-time consumer is conceptual rather than syntactic: getting the intended value requires understanding and correctly configuring export conditions in one’s own bundler, a concept the library depends on but doesn’t teach from scratch.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search