Weeknotes: Avoiding node and npm, somewhat, with Deno and JSR

Jun 17, 2026

For the last few months I've slowly been iterating the lidar data viewer from a static HTML/Javascript thing with a few manually built JSON files to indicate where the point cloud data is, over to a "proper" service where I have an OCaml backend that will index and server up the data to the front end. The eventual aim is to have the backend properly index multiple distinct source of data and fuse them, unlike the current version which uses smoke and mirrors to achieve that (e.g., I manually compiled the Swedish land cover maps to a PNG image to let me colour the point cloud that way).

The original front end was vibe coded, as I am both not a front end developer so lack the experience, and I really do not enjoy the development experience between the languages involved and the debug cycle. But I wanted to explore the data a little to see if this makes any sense, and so vibe coding got me most of the way to what I wanted in terms of exploration. That demo has been somewhat successful, which is why I'm now moving to have the front end built properly. This means I need to actually take ownership of the front end, despite my general disdain for frontend development: the ends justify the means I guess, as a lot of people have reacted positively to the demos and been interested in my goals to build a more 3D-first approach to exploring GIS data, and so I feel there's something worth pursing here. As I say, the backend in OCaml is the bit I'm comfortable with, the front end less so, but it's time to put personal preferences to once side with the goal of building a tool to enable people better.

To that end I've made myself spend a day or two trying to reign in the front end and actually make it buildable and reproducible. Because I wanted a static site for the first version to keep it manageable, I did not want to use npm and the related ecosystem for building the javascript: I just like to have a text file and copy libraries over thank you very much. This worked fine for three.js, the library used for the rendering, but not for the libraries used for parsing the point cloud data (the copc library and laz-perf): for those I had to used NPM to build a stand alone version of those files, which I've been copying around ever since. I couldn't check those into the repo either, as that's clearly just asking for confusion, and so when I set up a point cloud demo there's all this manual copying of files around. I'm not proud of this, but this is how demos get thrown together: with gaffer tape and coffee stirrers.

But now I need to make things "proper", I have to accept I'm going to need some Javascript tooling to help with package management. I've used Node/NPM in the past and I strongly dislike how it dominates the whole development experience: not just package management, but also building, dictating how your project goes, etc. I guess one could say it is opinionated, but unfortunately so am I, and I don't like how it works :) Thus I had a look around to see if I could find a more minimal alternative, and I came across Deno and JSR. In the same way that Node is actually a javascript run time and NPM is the package management repository and then some, these two related projects are quite similar: Deno is a javascript runtime and tooling and JSR is their package repository archive. So, meet the new boss, same as the old boss to some degree. However, in general I've found the workflow here much more light weight than the node ecosystem.

So, you still need a project JSON, much like packages.json with NPM, I have a deno.json. But it feels much more manageable for my small-scale project:

{
  "imports": {
    "three": "npm:three@0.184.0",
    "three/addons/": "npm:three@0.184.0/jsm/",
    "copc": "npm:copc",
    "las": "npm:laz-perf"
  },
  "tasks": {
    "check": "deno check src/main.ts",
    "bundle": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run build.ts"
  }
}

I just list the dependancies I want, and define a couple of scripts to let me do type checking and compiling the bundle up: a lot more approachable than any node project has been to set up. Two interesting things here: firstly, you can see I'm pulling packages from npm, as Deno works with both JSR and NPM repositories, and for better or worse NPM is still where most Javascript frameworks are maintained; and secondly I'm using Typescript without having to import it, as Deno is Typescript native out the box. Given my Javascript is weak, I'm using Typescript's strictness to help me avoid obvious errors.

Technically I don't need to list the imports in deno.json, as with Deno I can specify the source (NPM, JSR, or other) as part of the import statement in Javascript, but I like having my dependancies listed in one place.

I guess a third thing to note: I'm using a custom build script. This isn't normally required, normally you could just do deno bundle src/main.ts for this, but the laz-perf package has a Wasm blob that I want to copy to a particular place when I do a bundle.

import * as esbuild from "npm:esbuild";
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";

await Deno.mkdir("dist", { recursive: true });

const cmd = new Deno.Command("deno", {
  args: ["info", "--json", "npm:laz-perf"],
  stdout: "piped",
});
const output = await cmd.output();
const info = JSON.parse(new TextDecoder().decode(output.stdout));
const lazPerfPath = info.npmPackages["laz-perf@0.0.7"].localPath;

await esbuild.build({
  plugins: [...denoPlugins()],
  entryPoints: ["src/main.ts"],
  outfile: "dist/bundle.js",
  bundle: true,
  platform: "browser",
  format: "esm",
  minify: true,
  treeShaking: true,
});

await Deno.copyFile(
  `${lazPerfPath}/lib/web/laz-perf.wasm`,
  "dist/laz-perf.wasm"
);

await esbuild.stop();

Whilst this is perhaps a bit grotty, I like the explicitness of being able to take the standard build and tweak it like this. I think really what I dislike about Node/NPM is how much they take over and don't let you own in the build process. With this small Deno setup I still feel some sense of being in control of what's going on.

Now I have some infrastructure in place I now have one repo for the OCaml backend and the (now) Typescript front end in once place. I've spent some time simplifying the front end implementation and adding typing, and stripping back most of the Sweden demo specific hacks so I can now start trying to fuse in other data layers myself.

Tags: weeknotes, javascript, typescript, deno