tl;dr 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 or evaluate hello in your browser tab 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 of posts 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: 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 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, one commit per nixpkgs revision.
§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.
§Nothing lost in translation
We can prove our translation is faithful. We evaluate every attribute of nixpkgs’ own CI configuration, ci/eval/outpaths.nix, with jinx and with nix-eval-jobs, and compare all the drvPaths.
On nixpkgs 6510408d, on x86_64-linux, aarch64-linux and aarch64-darwin:
Zero differences or failing on one side.11The 2,714 that fail on both sides are the attributes Nix refuses to evaluate such as unsupported platform, broken, or insecure.
§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.22There is probably some fun code golf trying to create output paths that you then subsequently boot with trynix without ever using Nix.
§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:
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.33You can find the full benchmark table and the raw data on the benchmark page
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. 🤷
§Can I try?
The source is at 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.
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.