This blog post was written with Hexo and Nix

A few weeks ago I was at HighEdWeb 2023 in Buffalo and caught a talk (“Maybe some other time Docker: Fast, declarative and reproducible developer environments using Nix”) from Jason Woodward about Nix. Nix is many things, including an OS, but he focused on Nix-the-package-manager and that’s what I’m going to (briefly) discuss here.

To grossly oversimplify, Nixpkgs is a collection of tens of thousands of packages precompiled for various platforms. Each version of each package can be referenced by hash. Using a nix flake, you can specify which packages should be associated with your project. The nix develop command will then use path magic to load the specified packages into your shell. When you exit from the shell the paths are unloaded.

I moved this blog to hexo about a year ago. My authoring environment needs a few things: a reasonable version of node, hexo server running in the background to serve pages, and hexo generate --watch to catch changes as I write. My old process involved manually kicking those processes off. Before I could do that, I had to run nvm use to leverage the .nvmrc file at the root of my project directory to ensure that I had the correct version of node active, including the global reference for hexo.

This could all be boiled down to a single flake:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
description = "Example JavaScript development environment for Zero to Nix";

# Flake inputs
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
};

# Flake outputs
outputs = { self, nixpkgs }:
let
# Systems supported
allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux
"x86_64-darwin" # 64-bit Intel macOS
"aarch64-darwin" # 64-bit ARM macOS
];

# Helper to provide system-specific attributes
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
# Development environment output
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = with pkgs; [
nodejs_20 # Node.js 20, plus npm, npx, and corepack
yarn
];

shellHook = ''
yarn
npx hexo server &
npx hexo generate --watch &
'';
};
});
};
}

This is derived from the Zero to Nix demo, which I found very helpful. The flake installs two packages: NodeJS 20 and the yarn package manager. I then define a shellHook, which runs three post-install commands:

1
2
3
yarn
npx hexo server &
npx hexo generate --watch &

This installs all the node packages, including node. I may be the last person to discover that using npx can eliminate the need to have node CLI packages installed globally. The second and third commands start the local hexo server (listening on port 4000 by default) and the generation command. Both are sent to the background. Running nix develop executes everything and drops me into a shell with node 20 and a working hexo environment.

When I’m done, I quit the shell and then run killall hexo to stop the two commands. I haven’t found a shutdown equivalent of shellHook, and that might not be desirable anyway.

I’m still at the this-is-sort-of-cool stage with Nix, and I’m still not all that comfortable with the environment. On the other hand, it does work, and it’s more straightforward than the previous approach with nvm.