To build a ClojureScript application, we need to pull dependencies from two package managers. That is, JavaScript packages from npm and Clojure packages from deps.edn. After pulling the dependencies, we can then let shadow-cljs compiles the ClojureScript files into a single javascript and make the result a npm package. In this post, we rely on nixpkgs's builtin fetchNpmDeps and buildNpmPackage for the npm side, and clj-nix for the nix side.
I will skip the details that are not unique to Nix. The following contents assume we already have a working shadow-cljs project that can produce a node.js target when running npm run release. Which is basically:
:node-script target in shadow-cljs.edn, with :main function specifiedrelease script in package.json, most probably shadow-cljs release scriptbin in package.json.During the build phase of nix package, the process cannot perform any network request, as that will defeat the whole point of determinism. The common practice is to use a lock file. Which is essentially a recipe of all the necessary resources (and their checksums) for the building process. We can then prefetch all the resources into the nix store and tell the building process where to find the necessary resources during the actual building.
On the npm side, we do it by running the following command. It will output a sha256 code, we save it for later.
nix run nixpkgs#prefetch-npm-deps package-lock.json
On the Clojure side, we execute the following command. It will generate a deps-lock.json file. We will refer it in the flake.nix.
nix run github:jlesquembre/clj-nix#deps-lock
The following nix script takes heavy inspiration from this github issue comment.
{
  description = "";
  inputs.flake-utils.url = "github:numtide/flake-utils";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  inputs.clj-nix.url = "github:jlesquembre/clj-nix";
  outputs = { self, flake-utils, nixpkgs, clj-nix }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [
            clj-nix.overlays.default
          ];
        };
      in {
        packages.default = pkgs.buildNpmPackage rec {
          pname = "proj-name";
          nativeBuildInputs = [ pkgs.babashka pkgs.clojure ];
          npmBuildScript = "release";
          version = "0.0.1";
          src = "${self}";
          # deps hash from previous prefetch-npm-deps
          npmDepsHash = "sha256-0000=";
          # deps-lock.json generated by github:jlesquembre/clj-nix#deps-lock
          deps-cache = pkgs.mk-deps-cache { lockfile = ./deps-lock.json; };
          preBuild = ''
            bb -e '(-> (slurp "deps.edn")
                       rewrite-clj.zip/of-string
                       (rewrite-clj.zip/assoc :mvn/local-repo "${deps-cache}/.m2/repository")
                       rewrite-clj.zip/root-string
                       (#(spit "deps.edn" %)))'
          '';
        };
      }
    );
}
In the preBuild script, we rewrite deps.edn file to tell clj to find dependencies in a local maven repo generated by mk-deps-cache, which is provided by clj-nix overlay. We then fill npmDepsHash with the value we got from previous commands.
With this flake.nix, we can then build the project with nix build . or run the script with nix run ..
You can check this little project of mine for a working example.
The hack in preBuild only deals with dependencies pulled from maven repos. Git dependencies aren't probably handled.