diff options
| author | Xe Iaso <me@xeiaso.net> | 2023-08-11 10:30:59 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2023-08-11 10:31:07 -0400 |
| commit | 91618787effe6ed66b6a2bc8bf8ac91a5e3ec4ee (patch) | |
| tree | b8b6673b3a5e656d6fca48c5d340e601b7ab95d7 | |
| parent | 5c1120f06d436ccb8850acbb61d02246e2a597a4 (diff) | |
| download | xesite-91618787effe6ed66b6a2bc8bf8ac91a5e3ec4ee.tar.xz xesite-91618787effe6ed66b6a2bc8bf8ac91a5e3ec4ee.zip | |
blog: nixexpr post
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | blog/nixexpr.markdown | 175 | ||||
| -rw-r--r-- | flake.lock | 18 | ||||
| -rw-r--r-- | lib/xesite_templates/src/lib.rs | 1 |
3 files changed, 184 insertions, 10 deletions
diff --git a/blog/nixexpr.markdown b/blog/nixexpr.markdown new file mode 100644 index 0000000..6ec7e85 --- /dev/null +++ b/blog/nixexpr.markdown @@ -0,0 +1,175 @@ +--- +title: "Introducing nixexpr: Nix expressions for JavaScript" +date: 2023-08-11 +tags: [nix, javascript, nodejs, npm, cursed] +--- + +<xeblog-hero photo ai="Nikon D3300, photo by Xe Iaso" file="eifel-tower2" prompt="A picture of the tip of the Eifel Tower facsimilie in Las Vegas with a partially cloudy sky"></xeblog-hero> + +As a regular reminder, it is a bad idea to give me ideas. Today's bad idea is +brought to you by managerial nerd sniping, insomnia, and the letter "Q". + +At a high level: writing complicated data structures in JavaScript kinda sucks. +Here's an example of the kinds of things that I've been writing as I go down the +[ElasticSearch tour-de-insanite](/blog/elasticsearch): + +```javascript +{ + highlight: { + pre_tags: ['<em>'], + post_tags: ['</em>'], + require_field_match: false, + fields: { + body_content: { + fragment_size: 200, + number_of_fragments: 1, + }, + }, + }, +} +``` + +This works, this is perfectly valid code. It creates an object that has a few +nested layers of stuff in it, but overall I just don't like how it _looks_. I +think it looks superfluous. What if we could make it look a little bit nicer? +How about something like this? + +```nix +{ + highlight = { + pre_tags = [ "em" ]; + post_tags = [ "</em>" ]; + require_fields_match = false; + fields.body_content.fragment_size = 200; + fields.body_content.number_of_fragments = 1; + }; +} +``` + +This is a Nix expression. It's a data structure that looks like JSON, but you +have the power of a programming language at your fingertips. Note the difference +between these two parts: + +```javascript +{ + fields: { + body_content: { + fragment_size: 200, + number_of_fragments: 1, + }, + }, +} +``` + +``` +{ + fields.body_content.fragment_size = 200; + fields.body_content.number_of_fragments = 1; +} +``` + +These are semantically equal, but you don't have to use so much indentation and +layering. These settings are all related, so it makes sense that the way that +you use them is on the same level as the way that you define them. + +If you want to try out this awesome power for yourself, +[Install Nix](https://github.com/DeterminateSystems/nix-installer) and then add +`@xeserv/nixexpr` to your JavaScript dependencies. + +```bash +npm install --save @xeserv/nixexpr +``` + +Then you can use it like this: + +```javascript +import { nix } from "@xeserv/nixexpr"; + +const someValue = "this is a string"; + +const myData = nix`{ + hello = "world"; + someValue = ${someValue}; +}`; + +console.log(myData); +``` + +I originally wrote this +[in Go](https://github.com/Xe/x/blob/c822591c5a46ad9e8f13d14ac96d2e9d26e7c828/cmd/yeet/main.go#L115-L151) +for my scripting automation tool named +[yeet](https://www.urbandictionary.com/define.php?term=Yeet), but I think it's +generically useful enough to exist in its own right in JavaScript. I think that +there's a lot of things that the JavaScript ecosystem can gain from Nix, and I'm +excited to see what people do with this. + +This was made so I could write scripts like this: + +```javascript +// snipped for brevity +const url = slug.push("within.website"); +const hash = nix.hashURL(url); + +const expr = nix.expr`{ stdenv }: + +stdenv.mkDerivation { + name = "within.website"; + src = builtins.fetchurl { + url = ${url}; + sha256 = ${hash}; + }; + + phases = "installPhase"; + + installPhase = '' + tar xf $src + mkdir -p $out/bin + cp web $out/bin/withinwebsite + cp config.ts $out/config.ts + ''; +} +`; +``` + +And then I'd be able to put that Nix expression into a file. I'll get into more +details about this in a future post. + +<xeblog-conv name="Mara" mood="hacker">Something something this isn't best +practice something something this is a hack to make dealing with a legacy +deployment easier something something</xeblog-conv> + +## How it works + +This is a very cheeky library, and it's all powered by one of the most fun to +abuse Nix functions ever: +[builtins.fromJSON](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-fromJSON). +This function takes a string and turns it into a Nix value at the interpreter +level and it's part of the callpath for turning a string into an integer in Nix. +It's an amazingly powerful function in its own right, but it gets even more fun +when we bring JavaScript into the mix. + +Any JavaScript data value (simple objects, strings, numbers, etc) can be +formatted as JSON with the `JSON.stringify` function: + +``` +> JSON.stringify({"hi": "there"}) +'{"hi":"there"}' +``` + +This includes strings. So if we use `JSON.stringify` to convert it to a JSON +string, then string encode it again, we can inject arbitrary JavaScript code +into Nix expressions: + +```js +let formattedValue = `(builtins.fromJSON ${ + JSON.stringify(JSON.stringify(value)) +})`; +``` + +The most horrifying part about this hack is that it works. + +## What's next? + +If this ends up getting used, I may try and make "fast paths" for strings and +numbers so that they don't have to go through the JSON encoding/decoding +process. But so far this works well enough for my purposes. @@ -148,11 +148,11 @@ ] }, "locked": { - "lastModified": 1688534083, - "narHash": "sha256-/bI5vsioXscQTsx+Hk9X5HfweeNZz/6kVKsbdqfwW7g=", + "lastModified": 1690373729, + "narHash": "sha256-e136hTT7LqQ2QjOTZQMW+jnsevWwBpMj78u6FRUsH9I=", "owner": "nix-community", "repo": "naersk", - "rev": "abca1fb7a6cfdd355231fc220c3d0302dbb4369a", + "rev": "d9a33d69a9c421d64c8d925428864e93be895dcc", "type": "github" }, "original": { @@ -178,11 +178,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1689679375, - "narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=", + "lastModified": 1690881714, + "narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "684c17c429c42515bafb3ad775d2a710947f3d67", + "rev": "9e1960bc196baf6881340d53dccb203a951745a2", "type": "github" }, "original": { @@ -242,11 +242,11 @@ ] }, "locked": { - "lastModified": 1689850762, - "narHash": "sha256-zz62sRw+Bh8zd24GQpQ3Jp0fXgMuRLmhKIpVarkl1DI=", + "lastModified": 1690926877, + "narHash": "sha256-933VGrQlTxBPpbpAtfAOTU83RbJusdL1x7gYdqJN9v4=", "owner": "typst", "repo": "typst", - "rev": "51a21403ba2e8ced2aeb6a80996d9247f0b7ffd0", + "rev": "77cc05b121bef5a7feb62345d19d9c693415d7cd", "type": "github" }, "original": { diff --git a/lib/xesite_templates/src/lib.rs b/lib/xesite_templates/src/lib.rs index f5219d9..26dcaec 100644 --- a/lib/xesite_templates/src/lib.rs +++ b/lib/xesite_templates/src/lib.rs @@ -47,7 +47,6 @@ pub fn hero(file: String, prompt: Option<String>, ai: Option<String>) -> Markup img style="padding:0" loading="lazy" alt={"hero image " (file)} src={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) "-smol.png"}; } figcaption { - "Image generated by " (ai) @if let Some(prompt) = prompt { " -- " (prompt) } } |
