unenv: polyfilling Node.js builtins so a library can run on an edge runtime
🕊️ Node.js compatibility for any JavaScript runtime, including browsers and edge workers.
At a glance
- What is it?
- unenv turns a list of Node.js builtins and globals into bundler configuration, so code written for Node can be aliased, injected and shimmed for browsers and edge workers. It is a build-time compatibility layer, not a Node implementation, and its main branch is still a v2 release candidate.
- Who is it for?
- Adopt unenv when you are publishing a library that has to run in a browser or an edge worker and you want one shared list of aliases and shims instead of a hand written set per bundler. Do not expect it to give you Node.js itself, since modules such as node:fs and node:child_process can only be approximated outside a server.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem unenv solves is a library that assumes Node exists
Most server libraries import Node builtins without asking where the code will run. A package that pulls in node:fs or node:buffer works fine in a Node server and breaks the moment it is bundled for a browser tab or a worker at the edge, because the module simply does not exist in that runtime. The usual fix is a pile of alias rules, one entry per builtin, repeated in every project that bundles the library.
unenv removes that repetition. It provides polyfills that add Node.js compatibility for any JavaScript runtime, including browsers and edge workers, and it does so at build time rather than at run time. The runtime is not changed, no global is patched in a browser, and no code is shipped to the client beyond what the bundler decides to keep. Everything happens while the bundle is assembled.
The audience is therefore library authors and framework teams rather than application developers. The README names Nitro, Nuxt, Cloudflare and ESM.sh as projects that use it, which tells you the shape of the intended use: a shared layer of runtime differences, configured once and applied by whatever bundler the consumer happens to use.
defineEnv returns a bundle of bundler shaped configuration
The entry point is a single utility. Calling defineEnv with a set of options produces an env object holding four collections, and each one maps onto a different bundler feature: alias for redirecting imports, inject for injecting globals as module imports, external for keeping a dependency outside the bundle, and polyfill for the global polyfill set.
The README gives this example.
import { defineEnv } from "unenv";
const { env } = defineEnv({
nodeCompat: true,
npmShims: true,
resolve: true,
overrides: {},
presets: [],
});
const { alias, inject, external, polyfill } = env;The options are what decide the content. nodeCompat, which defaults to true, adds alias entries for Node.js builtins under both the bare id and the node: prefixed form, and adds inject entries for the Node.js globals global, Buffer and process. npmShims, off by default, adds alias entries that replace npm packages such as node-fetch with lighter shims. resolve, also off by default, resolves config values to absolute paths. overrides and presets are the extension points, and the README points at @cloudflare/unenv-preset as an example of a preset.
npmShims is where a package stops being the package the author wrote
The npmShims option deserves its own attention because it is the most aggressive thing in the toolkit. Turning it on does not merely add missing modules, it replaces working ones. A dependency on node-fetch can be aliased to a lighter shim, which changes the implementation of network calls in the bundle rather than the interface.
That is a deliberate trade. In a browser, a full node-fetch implementation pulls in a lot of code to do what fetch already does, and the edge case is similar. The saving is bundle size and sometimes startup behaviour, and the price is behavioural difference that the library author may not have anticipated, particularly around headers, streaming bodies and error shapes.
The default is off, and that default is the right starting position. Enable npmShims when you have measured a bundle and know which packages are inflating it, not as a blanket setting. The README does not document which packages are covered by the shim set, so the list of substitutions is something you confirm by inspecting the alias entries that defineEnv returns for your own configuration.
The unenv/ export paths let you import a single polyfill directly
defineEnv is the bundled path, but the package also exposes the polyfills themselves through subpath exports, so a project can pull in exactly one thing when the whole configuration is overkill. The export map in package.json exposes the root entry, the package.json file, a CommonJS mock proxy path, and a wildcard that maps every remaining subpath into the built runtime directory.
The README groups the subpaths by purpose: unenv/mock/* for mocking utilities, unenv/node/* for APIs that are compatible with the Node.js API, unenv/npm/* for npm package shims, unenv/polyfill/* for global polyfills, and unenv/web/* for a subset of Web APIs.
That structure tells you what the project believes about compatibility. The node directory holds replacements for Node.js built-in modules, and the coverage list in the README is long, covering modules such as node:assert, node:async_hooks, node:buffer, node:child_process, node:cluster, node:crypto, node:dns, node:fs, node:http, node:net, node:os, node:path, node:punycode, node:querystring and node:readline. The mock directory exists because the useful half of a Node module in a test is often the module you replaced, and the web directory exists because some libraries expect a web standard where the server has none.
One env object has to be reshaped for each bundler
This is the part of the design that costs effort, and the README is honest about it by publishing a table instead of pretending the configuration is universal. The same four concepts have different names and different homes in every tool.
For rollup, alias goes to @rollup/plugin-alias, inject to @rollup/plugin-inject, and external to the rollup external option. For vite, alias and inject are native resolve options while external is the ssr.external list, and for esbuild the same three names are used directly. Webpack has resolve.alias, a webpack-plugin-inject package for injection, and externals. Rolldown takes resolve.alias and inject as native options. Rspack is the awkward one: it supports resolve.alias and externals, but the table marks inject as unsupported.
So adopting unenv is not a single config change, it is a small adapter per bundler that maps alias, inject and external onto whatever that tool calls them. For a library author with one build pipeline that is trivial. For a framework supporting six bundlers, it is a mapping table to maintain, and it is the reason presets exist.
The main branch is a v2 release candidate while v1 is the current release
The repository state is worth reading carefully before you install. A note at the top of the README says you are on the development v2 branch and directs you to the v1 branch for the current release. The package.json version is 2.0.0-rc.24, and the recent releases listed for the repository are v2.0.0-rc.24 from 2025-10-29, v2.0.0-rc.23 from 2025-10-28 and v2.0.0-rc.22 from 2025-10-23, all release candidates rather than a stable 2.0.0.
That combination is unusual. The last push was on 2026-10-18, so the code is moving, but the newest listed release is a candidate published almost a year earlier. If you want the version that other projects depend on today, follow the README and take the v1 branch or the v1 release line. If you want the current main branch behaviour, expect to be on a release candidate and to absorb its breaking changes. The CHANGELOG.md at the repository root is where the differences between the two lines should be recorded, and the release script in package.json uses changelogen to publish with an rc tag.
The licence is MIT, so using, forking and shipping the polyfills carries no fee. The cost of adoption is upgrade work rather than licence work.
A shim for node:fs is not a file system
Here is where the honest limitation sits. unenv makes the import resolve, and the module API is present, but the capability behind it is not. A browser has no file system, no child processes, no sockets, no cluster and no inspector protocol. The coverage list includes node:fs, node:fs/promises, node:child_process, node:cluster, node:net and node:inspector, and in a browser or an edge worker those can only be partial stand-ins rather than working implementations.
The same applies to npmShims, where a substituted package can behave differently from the original. A library that works under unenv in a browser is a library whose Node dependencies are incidental rather than load bearing, and the way to find out which kind you have is to bundle it and look at what the runtime actually does with those modules.
The alternative approach is to change the target rather than the import. Running the code in a runtime that implements the Node APIs gives real behaviour and no shims, at the cost of that runtime's constraints and of not being a browser. The other alternative is to keep the Node imports untouched and let the bundler externalise them, which is what the external list in the env object is for, and which pushes the Node requirement onto whoever runs the code. The choice is between faking the API and avoiding the need for it, and unenv sits firmly on the first side of that line.
Editorial conclusion
Adopt unenv when you are publishing a library that has to run in a browser or an edge worker and you want one shared list of aliases and shims instead of a hand written set per bundler. Do not expect it to give you Node.js itself, since modules such as node:fs and node:child_process can only be approximated outside a server. Before you depend on it, check the branch you install from: the README points to the v1 branch for the current release while the main branch carries v2.0.0-rc.24, and mixing the two will give you a different export map than the one your dependency tree expects.
Frequently asked questions
What does unenv do and who is it for?
unenv provides polyfills that add Node.js compatibility for any JavaScript runtime, including browsers and edge workers. It is aimed at library and framework authors who need one shared list of Node builtins aliased and shimmed at build time, and the README names Nitro, Nuxt, Cloudflare and ESM.sh as users.
How do I install unenv and generate a configuration?
The package is published on npm under the name unenv, and the README links that package page. The main branch is a v2 development branch, and the README tells you to check out the v1 branch for the current release. Once installed, call defineEnv with your options and read the alias, inject, external and polyfill values off the returned env object.
What is the difference between nodeCompat and npmShims in unenv?
nodeCompat, on by default, adds alias entries for Node.js builtins under the bare id and the node: prefixed form, plus inject entries for the globals global, Buffer and process. npmShims, off by default, adds alias entries that replace npm packages such as node-fetch with lighter shims.
Can I use unenv polyfills without the full configuration?
Yes. The package exposes subpath exports for unenv/mock/*, unenv/node/*, unenv/npm/*, unenv/polyfill/* and unenv/web/*, so a project can import a single polyfill directly instead of wiring the whole env object into a bundler.
Is unenv stable to install from npm today?
The package.json version is 2.0.0-rc.24, a release candidate published on 2025-10-29, and the README states that the v1 branch is the current release. The last push was on 2026-10-18, so main keeps moving while the published candidate stays where it is.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unjs-unenv)Was this useful?