---
title: "Nixpkgs for the frontend impaired"
slug: nixpkgs-for-the-frontend-impaired
url: https://listedarticles.com/articles/nixpkgs-for-the-frontend-impaired
canonical_url: https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired
content_type: blog_post
language: en
published_at: 2026-10-12T00:00:00.000Z
updated_at: 2026-10-13T20:15:37.273Z
author: "Farid Zakaria"
authored_by: human
publisher: "Farid Zakaria’s Blog"
publisher_url: https://fzakaria.com/
topics: ["Open Source", "Programming", "Developer Tools", "Web Development"]
license: all-rights-reserved
word_count: 1431
reading_minutes: 6
citation: "Farid Zakaria, Farid Zakaria’s Blog. \"Nixpkgs for the frontend impaired.\" 12 Oct 2026. https://fzakaria.com/2026/10/12/nixpkgs-for-the-frontend-impaired (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Nixpkgs for the frontend impaired

> Farid Zakaria introduces jinx, a tool that compiles Nix to TypeScript so all of nixpkgs can be evaluated in the browser with exact drvPath parity—aimed at making Nix’s package graph inspectable without installing Nix.

**tl;dr** [jinx](https://fzakaria.github.io/jinx) is a tool that compiles Nix to TypeScript. All of nixpkgs, each 231,777 attribute, produces **the exact** drvPath Nix does. [Open the playground](https://fzakaria.github.io/jinx) or [evaluate hello in your browser tab](https://fzakaria.github.io/jinx/nixpkgs/#attr=hello) with no Nix installed anywhere. 😈


Despite my admiration and joy for Nix, the language and tool, it is not what is magical. The store model is the great idea. Any similar tool built on the same model will have the same magical properties of reproducibility, and hermeticity.

I have spent [a lot](https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions) [of posts](https://fzakaria.com/2026/09/24/every-package-is-already-installed) exploring what falls out of the Nix ecosystem, but the language itself is a small lazy functional language with a derivation primitive. You could design another one in a weekend.

What you cannot design in a weekend is [nixpkgs](https://github.com/NixOS/nixpkgs): twenty years of fixes, every cross-compilation workaround, every `stdenv` phase, every patched `configure` script, every “this library needs this flag on aarch64-darwin” that somebody already solved. As of October 2026, it is 140,000-plus packages that build. That is the magic sauce 🥫, and it is written in Nix, so if you want it you write Nix.

Unfortunately it is also the trap. Anyone who wants to explore the store-model space from a different direction, a new language, a new evaluator, a new editor experience, must then start from zero packages. This is the graveyard where other reimplementations or forks of Nix go to die.

If however you could take the whole tree of nixpkgs and transform it into something another ecosystem already speaks, you could explore the store model without abandoning the twenty years of work that went into nixpkgs. 💡

I chose TypeScript. Why? Why not!?

TypeScript has a breadth of tooling and is supported in *nearly* every browser. It has an incredibly flexible type system that means we can express the shape of a derivation and all the oddities within Nix’s lazy evaluation model.

I am a frontend impaired “noob” but I thought the idea was worth exploring.

[jinx](https://fzakaria.github.io/jinx) is the result. It is a transpiler that takes a `.nix` file and produces a `.ts` file that evaluates to the same value, with the same thunks, the same derivation strings, and the same drvPaths.

I then compiled all of nixpkgs with it. Every attribute, every package, every module, every `stdenv` and `lib` function: 231,777 attributes in total.

The compiled tree lives at [fzakaria/jinx-nixpkgs](https://github.com/fzakaria/jinx-nixpkgs), one commit per nixpkgs revision.

## [§](https://fzakaria.com#what-it-does)What it does

Each `.nix` file becomes one `.ts` module of the same name in a tree that mirrors the source. A lambda is an arrow function, a `let` is a block of consts, an attribute set is an object literal, `import` is `require`. You can *read* it.

```
$ jinx compile pkgs/by-name/he/hello/package.nix
```
```
// Generated by jinx from .../pkgs/by-name/he/hello/package.nix. Do not edit.
import * as nix from "../../../../.jinx/runtime/index.ts";
const here = nix.here(import.meta.url);
const formals$1 = { args: { callPackage: false, fetchurl: false, hello: false, lib: false, nixos: false, stdenv: false, testers: false }, ellipsis: false };
export default nix.lambda((args) => {
  const $ = nix.formals(args, formals$1);
  const callPackage = nix.arg($, "callPackage");
  const fetchurl = nix.arg($, "fetchurl");
  ...
  return nix.app(nix.sel(stdenv, "mkDerivation"), (finalAttrs) => ({
    pname: "hello",
    version: "2.12.2",
    src: nix.thunk(() => nix.app(fetchurl, {
      url: nix.thunk(() => nix.str`mirror://gnu/hello/hello-${nix.sel(finalAttrs, "version")}.tar.gz`),
      hash: "sha256-...",
    })),
    ...
  }));
}, formals$1);
```
Nix is lazy, so the emitted code wraps an expression in `nix.thunk(() => ...)` wherever Nix would allocate a thunk. 🤮

The `jinx` evaluation writes nothing to `/nix/store`. `jinx instantiate` writes the `.drv` closure and `nix-store --realise` builds the derivation.

```
$ jinx eval hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
$ nix-instantiate --eval --strict hello.nix
{ drv = "/nix/store/j8q5j0x495vfrxgha5m6q40yjdi59334-hello-2.12.2.drv";
out = "/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2"; }
```
We produce **the exact same** `.drv` and the exact same `drvPath` Nix would. That is the key to unlocking nixpkgs in another ecosystem: the store does not know the difference.

## [§](https://fzakaria.com#nothing-lost-in-translation)Nothing lost in translation

We can prove our translation is faithful. We evaluate every attribute of nixpkgs’ own CI configuration, [ci/eval/outpaths.nix](https://github.com/NixOS/nixpkgs/blob/6510408d147d8c5e6d76043e899e9251756f6a75/ci/eval/outpaths.nix), with `jinx` and with [nix-eval-jobs](https://github.com/NixOS/nix-eval-jobs), and compare all the `drvPaths`.

On nixpkgs [6510408d](https://github.com/NixOS/nixpkgs/commit/6510408d147d8c5e6d76043e899e9251756f6a75), on x86_64-linux, aarch64-linux and aarch64-darwin:

| attributes | identical drvPath | fail on both sides | differ | fail on one side only | 
|---|---|---|---|---|
| 231,777 | 229,063 | 2,714 | 0 | 0 | 

Zero differences or failing on one side.<sup>1</sup>1The 2,714 that fail on both sides are the attributes Nix refuses to evaluate such as unsupported platform, broken, or insecure. 

## [§](https://fzakaria.com#but-seriously-why)But seriously, why?

Evaluating nixpkgs in TypeScript is ~3-5x slower. If that is all it did, that would not be very interesting. The point is not to make Nix faster, it is to make Nixpkgs *accessible* to the TypeScript ecosystem.

Here are some “fun” things you can do with it.

**Profile an evaluation.** `node --cpu-prof` on `firefox.drvPath`, folded through the source maps so every frame is a line of nixpkgs. Click it for the whole SVG.

**Complete nixpkgs in your editor.** Automatic language server protocol support for every attribute in nixpkgs, with source maps to the original `.nix` file. TypeScript’s type system is expressive enough to model the shape of a derivation, so you get completion and type checking for free.

**Step through a package.** Source maps put a breakpoint on a `.nix` line. Open the compiled tree in VS Code or Chrome DevTools, break in `pkgs/by-name/he/hello/package.nix`, and step into `stdenv.mkDerivation` as it builds the attribute set.

```
$ node --inspect-brk --enable-source-maps src/cli/main.ts eval hello.nix
```
**Ask nixpkgs a question in a loop.**  Interrogate and explore nixpkgs in a REPL or a script without having to remember the incantation to get a JSON dump of the whole tree and pipe it through `jq`.

```
import { loadNixpkgs } from "jinx";
const pkgs = loadNixpkgs("/path/to/nixpkgs");
const gpl = [];
for (const name of Object.keys(pkgs.lib.licenses)) {
  if (pkgs.lib.licenses[name].spdxId?.startsWith("GPL")) {
    gpl.push(name);
  }
}
console.log(JSON.stringify(gpl));
```
**Unit test Nix with node:test.** A `lib` function or a package definition is a function in a module, so it has a unit test like any other function in any other language.

```
test("hello builds the derivation Nix would", () => {
  assert.equal(
    pkgs.hello.drvPath,
    "/nix/store/cnraiw0hbdl6jzdhbkn8dah2hqj7xgij-hello-2.12.3.drv",
  );
});
```
**Write a package in TypeScript.** The compiled form from `jinx` is faithful to Nix down to every thunk, which makes it exact and miserable to write by hand. Alternatively, there is a much nicer hand-written form.
It interoperates with nixpkgs, so a package in TypeScript can call `stdenv.mkDerivation` and depend on anything in `pkgs`.

```
const hello = pkgs.stdenv.mkDerivation({
  pname: "hello",
  version: "2.12.2",
  src: pkgs.fetchurl({
    url: "mirror://gnu/hello/hello-2.12.2.tar.gz",
    hash: "sha256-WpqZbcKSzCTc9BHO6H6S9qrluNE72caBm0x6nc4IGKs=",
  }),
  meta: { license: pkgs.lib.licenses.gpl3Plus },
});
console.log(hello.drvPath);
```
**It runs in a browser.** TypeScript is supported in every modern browser.
You can evaluate nixpkgs in your tab, and it will fetch the compiled modules from GitHub as it needs them.<sup>2</sup>2There is probably some fun *code golf* trying to create output paths that you then subsequently boot with [trynix](https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser) without ever using Nix. 

## [§](https://fzakaria.com#is-it-slow)Is it slow?

Yep it’s slow, but crucially not *too* slow.

When warm, with the modules compiled and V8’s code cache populated, per attribute on my laptop:

| probe | nix | jinx warm | warm / nix | peak RSS nix | peak RSS jinx | 
|---|---|---|---|---|---|
| `lib` | 0.53 s | 1.31 s | 2.5x | 238 MB | 501 MB | 
| `hello` | 0.37 s | 1.72 s | 4.6x | 88 MB | 376 MB | 
| `firefox` | 3.19 s | 11.44 s | 3.6x | 488 MB | 1608 MB | 
| `nixos` | 4.21 s | 21.35 s | 5.1x | 603 MB | 2377 MB | 

A first evaluation that has to compile every module it reaches is 3x to 10x in speed and memory is ~2x-4x because thunks are very expensive to the garbage-collector.<sup>3</sup>3You can find the full benchmark table and the raw data on the [benchmark page](https://fzakaria.github.io/jinx/bench/) 

Let’s say it is five times slower than a C++ interpreter that has had twenty years of people shaving it, from a brand new compiler that runs on a JavaScript engine. That strikes me as a fine place to start. 🤷

## [§](https://fzakaria.com#can-i-try)Can I try?

The source is at [github.com/fzakaria/jinx](https://github.com/fzakaria/jinx).

It has no dependencies, no native code, and no daemon. You can run it with Node 24 or in your browser. The compiled nixpkgs tree is at [github.com/fzakaria/jinx-nixpkgs](https://github.com/fzakaria/jinx-nixpkgs).

I am a little annoyed that I have to keep disclosing that I used “AI” for the code. People seem to enjoy “outing” you for you it, as it is some morale failing. Let me save them the trouble: an AI co-authored a lot of this software with me.

Here is your stamp. You can stop reading here if that is a dealbreaker, the derivations will not change their hashes for you.

Nixpkgs is a TypeScript program now. [Go set a breakpoint in stdenv.](https://fzakaria.github.io/jinx/docs/why)
