<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="nl"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://bonaroo.nl/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bonaroo.nl/" rel="alternate" type="text/html" hreflang="nl" /><updated>2026-06-12T18:57:27+00:00</updated><id>https://bonaroo.nl/feed.xml</id><title type="html">Charper Bonaroo</title><subtitle>Bonaroo stuurt AI-agents aan met 50 jaar engineering-ervaring als kompas</subtitle><entry><title type="html">Variable Width Font Rendering on Game Boy</title><link href="https://bonaroo.nl/2026/01/19/variable-width-text.html" rel="alternate" type="text/html" title="Variable Width Font Rendering on Game Boy" /><published>2026-01-19T00:00:00+00:00</published><updated>2026-01-19T00:00:00+00:00</updated><id>https://bonaroo.nl/2026/01/19/variable-width-text</id><content type="html" xml:base="https://bonaroo.nl/2026/01/19/variable-width-text.html"><![CDATA[<h1 id="variable-width-font-rendering">Variable Width Font Rendering</h1>

<p>To render a scene on a Game Boy or Game Boy Color, one must create a tileset of tiles of 8x8 pixels, and assign these tiles to the background. The Game Boy background is 32x32 tiles and the screen renders a 20x18 portion of it.</p>

<p>It therefore makes a lot of sense to render text using 1 tile per character, resulting in a fixed-width 8x8 font. Most popular games use this.</p>

<div style="display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center;">
<img src="/assets/blog/variable-width-text/red-is-playing-the-snes.png" alt="Red Is Playing the SNES" width="498" style="margin-left: 0; max-width: none;" />
<img src="/assets/blog/variable-width-text/no-my-name-is-marin.png" alt="My Name Is Marin" width="504" style="margin-left: 0; max-width: none;" />
</div>

<p>My goal is to find a way to render variable-width fonts, with the intention to render more than 20 characters on a single line. I do intend to stick to the 8 pixels vertically, but horizontally I want to “ignore” the 8-pixel grid and create a software text renderer for the Game Boy.</p>

<h2 id="finding-a-font">Finding a font</h2>

<p>The first step is finding a font. I’m really lazy, so I’ll grab a free pixelated font from <a href="https://www.fontspace.com">FontSpace.com</a>.</p>

<p>I like <a href="https://www.fontspace.com/pixel-millennium-font-f14020">Pixel Millennium</a>, because it has a variety of character widths, making it suitable for this demo. This font is free for personal use, and I’ve <a href="https://futuremillennium.com/fonts/license/#pixel-millennium">purchased a commercial license</a> for my project.</p>

<h2 id="encoding-variable-width-characters-in-c">Encoding variable-width characters in C</h2>

<p>The next step is to convert the TTF to a map of characters I can use as a basis to render the text width. My instinct tells me to render the characters as a 1-bit bitmap, where each byte represents 8 pixels of either “background color” (0, for white) and “text color” (1, for black). My bitmap should contain the width of each character.</p>

<p>I haven’t actually checked, but I assume the smallest character is 1 pixel wide and the largest character is 5 pixels wide, so I need at least 3 bits to encode the width of a character. 3 bits nicely encodes 0 up to 7, which I expect to cover all characters. I’ll also assume a fixed height of 5 pixels for all characters.</p>

<p>Finally, I’m going to encode all characters in a single buffer of bytes named <code class="language-plaintext highlighter-rouge">font_data</code>, and because they will all have different sizes, I’m going to need some kind of index. This index will mirror an ASCII table for common characters, starting at 33 (!) and ending at 126 (~), for 94 characters total. The index will contain pointers to the start of the <code class="language-plaintext highlighter-rouge">font_data</code> for each character.</p>

<p>To make the pointers work, I’m going to have to align each character in the font_data with the start of a byte, so I expect there to be some padding bits between them.</p>

<p>To summarize, I expect to encode the font as C code as follows:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_letter_spacing</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_space_width</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_height</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_data</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="mh">0xFF</span><span class="p">,</span> <span class="cm">/* encoded pixels for all characters */</span> <span class="p">};</span>
<span class="k">const</span> <span class="kt">uint16_t</span> <span class="n">font_indices</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mi">0</span><span class="p">,</span> <span class="c1">// !</span>
  <span class="mi">2</span><span class="p">,</span> <span class="c1">// "</span>
  <span class="mi">5</span><span class="p">,</span> <span class="c1">// #</span>
  <span class="c1">// etc</span>
<span class="p">};</span>

<span class="c1">// example usage...</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">character_bitmap</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">font_data</span><span class="p">[</span><span class="n">font_indices</span><span class="p">[</span><span class="sc">'#'</span><span class="o">-</span><span class="mi">33</span><span class="p">]];</span>
</code></pre></div></div>

<p>Whether this is an optimal solution, I don’t know. But I’ll find out, by doing. Let’s generate some font data by rendering the TTF to font data!</p>

<h2 id="encoding-the-font">Encoding the font</h2>

<p>The easiest way to encode the characters to font data I can think of is by using Javascript. I can render each character onto a canvas, extract the pixels and convert them to my desired <code class="language-plaintext highlighter-rouge">font_data</code> and indices.</p>

<p>First, I render the characters to a canvas. For some reason the font keeps rendering slightly blurry, so I’ll just render the font pretty big and query the center of “assumed pixels”. I rendered the “query pixels” green, the borders red and the font blue:</p>

<p><img src="/assets/blog/variable-width-text/characters-01.png" alt="Characters" /></p>

<p>By rendering these characters, I immediately notice the characters not only have a variable height, they also have a variable vertical position. My plan to encode the characters with a fixed height of 5 will fail anyway since some characters are clearly taller than that, with the <code class="language-plaintext highlighter-rouge">$</code> being 7 pixels tall while the <code class="language-plaintext highlighter-rouge">.</code> (period) is 1 pixel tall. The underscore, <code class="language-plaintext highlighter-rouge">_</code> starts at the very bottom and the <code class="language-plaintext highlighter-rouge">'</code> starts at the very top. The <code class="language-plaintext highlighter-rouge">@</code> is not supported by the font, so I’m going to keep that character blank for now.</p>

<p>To support a variable width, height and vertical offset, I’m going to encode the width, height and vertical offset of each character. Analyzing all characters, I found that the width, height and vertical offsets all vary by at least 5, I’m going to need at least 3 bits for each value, 9 bits total. I might still pack them as 1 byte per value, depending on the size vs performance tradeoff, since extracting 3 bits from a byte is likely extra work.</p>

<p>Packing the pixel data tightly with the 9 bits of metadata, the majority of characters will fit in 3 or 2 bytes. Since most characters are 3x5 pixels, they need 15 bits. Add 9 bits of metadata, and you get a nice round 24 bits.</p>

<p>I suspect I’ll be needing to calculate the width of a string without actually rendering it, so I want to pack the width at the beginning for easy access.</p>

<p>Skipping the implementation details, the result looks like this:</p>

<p><strong>font_data.h</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="cp">#define font_data_ascii_offset 33
#define font_data_ascii_max 126
</span>
<span class="k">extern</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_data</span><span class="p">[</span><span class="mi">278</span><span class="p">];</span>
<span class="k">extern</span> <span class="k">const</span> <span class="kt">uint16_t</span> <span class="n">font_data_indices</span><span class="p">[</span><span class="mi">94</span><span class="p">];</span>
</code></pre></div></div>

<p><strong>font_data.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font_data.h"</span><span class="cp">
</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">font_data</span><span class="p">[</span><span class="mi">278</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="cm">/* &lt;blank&gt; */</span> <span class="mh">0x00</span><span class="p">,</span>
  <span class="cm">/* ! */</span> <span class="mi">105</span><span class="p">,</span> <span class="mi">46</span><span class="p">,</span>
  <span class="cm">/* " */</span> <span class="mi">83</span><span class="p">,</span> <span class="mi">90</span><span class="p">,</span>
  <span class="cm">/* # */</span> <span class="mi">109</span><span class="p">,</span> <span class="mi">212</span><span class="p">,</span> <span class="mi">87</span><span class="p">,</span> <span class="mi">95</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
  <span class="cm">/* ... */</span>
  <span class="cm">/* | */</span> <span class="mi">57</span><span class="p">,</span> <span class="mi">254</span><span class="p">,</span>
  <span class="cm">/* } */</span> <span class="mi">59</span><span class="p">,</span> <span class="mi">34</span><span class="p">,</span> <span class="mi">89</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span>
  <span class="cm">/* ~ */</span> <span class="mi">212</span><span class="p">,</span> <span class="mi">180</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">};</span>

<span class="k">const</span> <span class="kt">uint16_t</span> <span class="n">font_data_indices</span><span class="p">[</span><span class="mi">94</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="cm">/* ! */</span> <span class="mi">1</span><span class="p">,</span>
  <span class="cm">/* " */</span> <span class="mi">3</span><span class="p">,</span>
  <span class="cm">/* # */</span> <span class="mi">5</span><span class="p">,</span>
  <span class="cm">/* ... */</span>
  <span class="cm">/* | */</span> <span class="mi">269</span><span class="p">,</span>
  <span class="cm">/* } */</span> <span class="mi">271</span><span class="p">,</span>
  <span class="cm">/* ~ */</span> <span class="mi">275</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>It compiles without error! Now let’s see if I can render the characters on a Game Boy.</p>

<h2 id="rendering-characters">Rendering characters</h2>

<p>Let’s attempt to write the characters as tiles on a Game Boy, rendering one character per tile. I’m going to create 2 new files - <code class="language-plaintext highlighter-rouge">font.h</code> and <code class="language-plaintext highlighter-rouge">font.c</code>; These will contain functions for rendering the characters. Before I deal with the Game Boy’s native 2-bits-per-pixel encoding, I’m going to use GBDK-2020’s (Game Boy Development Kit) 1bpp to simplify the implementation, where one byte simply represents 1 row of 8 pixels, and each tile of 8x8 pixels is represented by 8 bytes.</p>

<p><strong>font.h</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="cp">#include</span> <span class="cpf">&lt;gb/gb.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="kt">uint8_t</span> <span class="nf">font_render_character_1bpp</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tile</span><span class="p">,</span> <span class="kt">int8_t</span> <span class="n">dx</span><span class="p">,</span> <span class="kt">int8_t</span> <span class="n">dy</span><span class="p">,</span> <span class="kt">char</span> <span class="n">c</span><span class="p">);</span>
</code></pre></div></div>

<p>With 1bpp, one can use the following GBDK functions to render the character:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Set the color for the high pixels (fgcolor) and low pixels (bgcolor)</span>
<span class="kt">void</span> <span class="nf">set_1bpp_colors</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">fgcolor</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">bgcolor</span><span class="p">);</span>

<span class="c1">// Upload `ntiles` tiles to VRAM at `start` with 8 bytes per tiles from `src`</span>
<span class="kt">void</span> <span class="nf">set_bkg_1bpp_data</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">start</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">ntiles</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">src</span><span class="p">);</span>

<span class="c1">// Render a tile from VRAM, identified by `t`, at `x`,`y` on the background</span>
<span class="kt">uint8_t</span> <span class="o">*</span> <span class="nf">set_bkg_tile_xy</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">y</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">t</span><span class="p">);</span>
</code></pre></div></div>

<p>Let’s define <code class="language-plaintext highlighter-rouge">font_render_character_1bpp</code>, returning 0, which is the minimum definition that compiles. I am going to implement this using Test-Driven-Development:</p>

<p><strong>font.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font.h"</span><span class="cp">
#include</span> <span class="cpf">"font_data.h"</span><span class="cp">
</span>
<span class="kt">uint8_t</span> <span class="nf">font_render_character_1bpp</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tile</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">dx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">dy</span><span class="p">,</span> <span class="kt">char</span> <span class="n">c</span><span class="p">)</span> <span class="p">{</span>
  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">tile</span><span class="p">;</span>
  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">dx</span><span class="p">;</span>
  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">c</span><span class="p">;</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>font_test.h</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma once
</span>
<span class="kt">char</span> <span class="o">*</span><span class="nf">font_test</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>font_test.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font_test.h"</span><span class="cp">
#include</span> <span class="cpf">"minunit.h"</span><span class="cp">
#include</span> <span class="cpf">"font.h"</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">tile_data</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>

<span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_character_1bpp_missing_character</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint8_t</span> <span class="n">size</span> <span class="o">=</span> <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="sc">' '</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">char</span> <span class="o">*</span><span class="nf">font_test</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">mu_run_test</span><span class="p">(</span><span class="n">test_font_render_character_1bpp_missing_character</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This passes, as expected. Let’s add a test!</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_character_1bpp_known_character</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint8_t</span> <span class="n">width</span> <span class="o">=</span> <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="sc">'!'</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/blog/variable-width-text/font-test-failure-01.png" alt="Game Boy-scherm met mislukte fontweergave: tekens overlappen elkaar" loading="lazy" width="320" height="288" style="image-rendering: pixelated;" /></p>

<p>As expected, this fails. In TDD spirit, let’s only return the width, as that is the only thing being tested. I’m going to extend my test suite to cover all these cases, and clean up the suite a bit:</p>

<p><strong>font_test.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font_test.h"</span><span class="cp">
#include</span> <span class="cpf">"minunit.h"</span><span class="cp">
#include</span> <span class="cpf">"font.h"</span><span class="cp">
#include</span> <span class="cpf">"font_data.h"</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">tile_data</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>

<span class="k">static</span> <span class="kt">uint8_t</span> <span class="nf">render</span><span class="p">(</span><span class="kt">char</span> <span class="n">c</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_character_1bpp_missing_character</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">' '</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Test a space</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">'@'</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Test a known missing character</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="mi">200</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Test beyond 7-bit ascii range</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="mi">18</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Test before visible characters</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_character_1bpp_known_character</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">'!'</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Known 1-pixel wide</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">'#'</span><span class="p">),</span> <span class="mi">5</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Known 5-pixel wide</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">char</span> <span class="o">*</span><span class="nf">font_test</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">mu_run_test</span><span class="p">(</span><span class="n">test_font_render_character_1bpp_missing_character</span><span class="p">);</span>
  <span class="n">mu_run_test</span><span class="p">(</span><span class="n">test_font_render_character_1bpp_known_character</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>font.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font.h"</span><span class="cp">
#include</span> <span class="cpf">"font_data.h"</span><span class="cp">
</span>
<span class="kt">uint8_t</span> <span class="nf">font_render_character_1bpp</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tile</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">dx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">dy</span><span class="p">,</span> <span class="kt">char</span> <span class="n">c</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">&lt;</span> <span class="n">font_data_ascii_offset</span>
  <span class="o">||</span> <span class="n">c</span> <span class="o">&gt;</span> <span class="n">font_data_ascii_max</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

  <span class="kt">uint16_t</span> <span class="n">i</span> <span class="o">=</span> <span class="n">font_data_indices</span><span class="p">[(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">c</span> <span class="o">-</span> <span class="n">font_data_ascii_offset</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">char_data</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">font_data</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
  <span class="kt">uint8_t</span> <span class="n">width</span> <span class="o">=</span> <span class="n">char_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x07</span><span class="p">;</span>

  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">tile</span><span class="p">;</span>
  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">dx</span><span class="p">;</span>
  <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">dy</span><span class="p">;</span>

  <span class="k">return</span> <span class="n">width</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now to render the actual tiles. But first, a test! I don’t know a pretty way to test rendered graphics, but adding this after <code class="language-plaintext highlighter-rouge">render('#')</code> this should suffice for now:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_character_1bpp_known_character</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">'!'</span><span class="p">),</span> <span class="mi">1</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Known 1-pixel wide</span>

  <span class="n">memset</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tile_data</span><span class="p">));</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">render</span><span class="p">(</span><span class="sc">'#'</span><span class="p">),</span> <span class="mi">5</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span> <span class="c1">// Known 5-pixel wide</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">0</span><span class="n">b00000000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="mi">0</span><span class="n">b01010000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="mi">0</span><span class="n">b11111000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="mi">0</span><span class="n">b01010000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span> <span class="mi">0</span><span class="n">b11111000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">5</span><span class="p">],</span> <span class="mi">0</span><span class="n">b01010000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">6</span><span class="p">],</span> <span class="mi">0</span><span class="n">b00000000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">tile_data</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span> <span class="mi">0</span><span class="n">b00000000</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/blog/variable-width-text/font-test-failure-02.png" alt="Game Boy-scherm met mislukte fontweergave: verkeerd uitgelijnde tekens" loading="lazy" width="320" height="288" style="image-rendering: pixelated;" /></p>

<p>As expected, the test fails. Now to implement the actual rendering:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint8_t</span> <span class="nf">font_render_character_1bpp</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tile</span><span class="p">,</span> <span class="kt">int8_t</span> <span class="n">dx</span><span class="p">,</span> <span class="kt">int8_t</span> <span class="n">dy</span><span class="p">,</span> <span class="kt">char</span> <span class="n">c</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">&lt;</span> <span class="n">font_data_ascii_offset</span>
  <span class="o">||</span> <span class="n">c</span> <span class="o">&gt;</span> <span class="n">font_data_ascii_max</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

  <span class="kt">uint16_t</span> <span class="n">i</span> <span class="o">=</span> <span class="n">font_data_indices</span><span class="p">[(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">c</span> <span class="o">-</span> <span class="n">font_data_ascii_offset</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">char_data</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">font_data</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
  <span class="kt">uint8_t</span> <span class="n">width</span> <span class="o">=</span> <span class="n">char_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x07</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="n">height</span> <span class="o">=</span> <span class="p">(</span><span class="n">char_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">3</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x07</span><span class="p">;</span>
  <span class="n">dy</span> <span class="o">+=</span> <span class="p">((</span><span class="n">char_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x03</span><span class="p">)</span> <span class="o">|</span> <span class="p">((</span><span class="n">char_data</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0x01</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">2</span><span class="p">);</span>

  <span class="kt">uint8_t</span> <span class="n">bit_index</span> <span class="o">=</span> <span class="mi">9</span><span class="p">;</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">height</span><span class="p">;</span> <span class="n">y</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int8_t</span> <span class="n">py</span> <span class="o">=</span> <span class="n">dy</span> <span class="o">+</span> <span class="p">(</span><span class="kt">int8_t</span><span class="p">)</span><span class="n">y</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">py</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">py</span> <span class="o">&gt;=</span> <span class="mi">8</span><span class="p">)</span>
      <span class="k">continue</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">width</span><span class="p">;</span> <span class="n">x</span><span class="o">++</span><span class="p">,</span> <span class="n">bit_index</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">int8_t</span> <span class="n">px</span> <span class="o">=</span> <span class="n">dx</span> <span class="o">+</span> <span class="p">(</span><span class="kt">int8_t</span><span class="p">)</span><span class="n">x</span><span class="p">;</span>

      <span class="k">if</span> <span class="p">(</span><span class="n">px</span> <span class="o">&lt;</span> <span class="mi">0</span>
      <span class="o">||</span> <span class="n">px</span> <span class="o">&gt;=</span> <span class="mi">8</span>
      <span class="o">||</span> <span class="o">!</span><span class="p">((</span><span class="n">char_data</span><span class="p">[</span><span class="n">bit_index</span> <span class="o">&gt;&gt;</span> <span class="mi">3</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="p">(</span><span class="n">bit_index</span> <span class="o">&amp;</span> <span class="mi">7</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mi">1u</span><span class="p">))</span>
        <span class="k">continue</span><span class="p">;</span>

      <span class="n">tile</span><span class="p">[</span><span class="n">py</span><span class="p">]</span> <span class="o">|=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="mi">7</span> <span class="o">-</span> <span class="n">px</span><span class="p">));</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">width</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This implementation is obviously not efficient, but the test does pass. Now to actually put them on the screen! Exciting stuff!</p>

<p><strong>main.c</strong> (partial)</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"font.h"</span><span class="cp">
#include</span> <span class="cpf">"font_data.h"</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
</span>
<span class="cp">#define _tile_size 8
#define _char_count 94
</span>
<span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">tile_data</span><span class="p">[</span><span class="n">_tile_size</span> <span class="o">*</span> <span class="n">_char_count</span><span class="p">];</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">render_font_characters</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tile_data</span><span class="p">));</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">_char_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
    <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tile_data</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">_tile_size</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
      <span class="n">font_data_ascii_offset</span> <span class="o">+</span> <span class="n">i</span><span class="p">);</span>
  <span class="n">set_bkg_1bpp_data</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">_char_count</span><span class="p">,</span> <span class="n">tile_data</span><span class="p">);</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">;</span> <span class="n">y</span><span class="o">++</span><span class="p">)</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="mi">12</span><span class="p">;</span> <span class="n">x</span><span class="o">++</span><span class="p">)</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">y</span> <span class="o">*</span> <span class="mi">12</span> <span class="o">+</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">_char_count</span><span class="p">)</span>
        <span class="n">set_bkg_tile_xy</span><span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="mi">12</span> <span class="o">+</span> <span class="n">x</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/blog/variable-width-text/font-on-screen-01.png" alt="Game Boy-scherm met correct gerenderd variabele-breedte font" loading="lazy" width="320" height="288" style="image-rendering: pixelated;" /></p>

<p>It works! With the dx &amp; dy arguments, I can render the characters anywhere in a tile. This should make the next step easy: Variable-Width Text Rendering.</p>

<h2 id="variable-width-rendering">Variable-Width Rendering</h2>

<p>For my next function, rendering a line of text, I’m going to use a function like this:</p>

<p><strong>font.h</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define font_letter_spacing 1
#define font_space_width 2
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">remainder</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="n">pixel_count</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="n">tile_count</span><span class="p">;</span>
<span class="p">}</span> <span class="n">font_render_line_result_t</span><span class="p">;</span>

<span class="c1">// Render a string into a 1bpp tile buffer.</span>
<span class="c1">//</span>
<span class="c1">// Characters from the string are rendered using font_render_character_1bpp,</span>
<span class="c1">// starting at dx &amp; dy. Characters are rendered until the end of the string, the</span>
<span class="c1">// end of the line or the end of the available tiles.</span>
<span class="c1">//</span>
<span class="c1">// Once the end is reached, `remainder` is returned, representing a pointer to</span>
<span class="c1">// the next character in the string not rendered, or the end of the string.</span>
<span class="c1">// Additionally, the amount of tiles written to is returned as `tile_count`, and</span>
<span class="c1">// the amount of pixels rendered is returned as `pixel_count`.</span>
<span class="n">font_render_line_result_t</span> <span class="nf">font_render_line_1bpp</span><span class="p">(</span>
  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tiles</span><span class="p">,</span>
  <span class="kt">uint8_t</span> <span class="n">tiles_length</span><span class="p">,</span>
  <span class="kt">int8_t</span> <span class="n">dx</span><span class="p">,</span>
  <span class="kt">int8_t</span> <span class="n">dy</span><span class="p">,</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">string</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Let’s write some tests for this function:</p>

<p><strong>font_test.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define tile_data_length 4
</span><span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">tile_data</span><span class="p">[</span><span class="n">tile_data_length</span> <span class="o">*</span> <span class="mi">8</span><span class="p">];</span>
<span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_line_1bpp_simple</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tile_data</span><span class="p">));</span>
  <span class="n">font_render_line_result_t</span> <span class="n">result</span> <span class="o">=</span>
    <span class="n">font_render_line_1bpp</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="n">tile_data_length</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s">"Hi"</span><span class="p">);</span>

  <span class="c1">// Note: I've created this font_get_line_width function as well,</span>
  <span class="c1">// but that's outside the scope of this blog to keep it brief.</span>
  <span class="kt">uint8_t</span> <span class="n">expected_pixels</span> <span class="o">=</span> <span class="n">font_get_line_width</span><span class="p">(</span><span class="s">"Hi"</span><span class="p">);</span>
  <span class="kt">uint8_t</span> <span class="n">expected_tiles</span> <span class="o">=</span> <span class="p">(</span><span class="n">expected_pixels</span> <span class="o">+</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>

  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="o">*</span><span class="n">result</span><span class="p">.</span><span class="n">remainder</span><span class="p">,</span> <span class="sc">'\0'</span><span class="p">,</span> <span class="s">"%c"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">pixel_count</span><span class="p">,</span> <span class="n">expected_pixels</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">tile_count</span><span class="p">,</span> <span class="n">expected_tiles</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">char</span> <span class="o">*</span><span class="nf">test_font_render_line_1bpp_too_long</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">text</span> <span class="o">=</span> <span class="s">"This is a long string!"</span><span class="p">;</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tile_data</span><span class="p">));</span>
  <span class="n">font_render_line_result_t</span> <span class="n">result</span> <span class="o">=</span>
    <span class="n">font_render_line_1bpp</span><span class="p">(</span><span class="n">tile_data</span><span class="p">,</span> <span class="n">tile_data_length</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">text</span><span class="p">);</span>

  <span class="c1">// because the tile_data is too short to fit the string, the remainder should</span>
  <span class="c1">// point to a non-NULL character</span>
  <span class="n">mu_assert</span><span class="p">(</span><span class="o">*</span><span class="n">result</span><span class="p">.</span><span class="n">remainder</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">);</span>

  <span class="c1">// The remainder should also be greater than the start of the text</span>
  <span class="n">mu_assert</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">remainder</span> <span class="o">&gt;</span> <span class="n">text</span><span class="p">);</span>

  <span class="n">mu_assert_eq</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">tile_count</span><span class="p">,</span> <span class="n">tile_data_length</span><span class="p">,</span> <span class="s">"%d"</span><span class="p">);</span>
  <span class="n">mu_assert</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">pixel_count</span> <span class="o">&lt;=</span> <span class="n">tile_data_length</span> <span class="o">*</span> <span class="mi">8</span><span class="p">);</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now to implement them.</p>

<p><strong>font.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">font_render_line_result_t</span> <span class="nf">font_render_line_1bpp</span><span class="p">(</span>
  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">tiles</span><span class="p">,</span>
  <span class="kt">uint8_t</span> <span class="n">tiles_length</span><span class="p">,</span>
  <span class="kt">int8_t</span> <span class="n">dx</span><span class="p">,</span>
  <span class="kt">int8_t</span> <span class="n">dy</span><span class="p">,</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">string</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint8_t</span> <span class="n">x</span> <span class="o">=</span> <span class="n">dx</span><span class="p">;</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">current_char</span> <span class="o">=</span> <span class="n">string</span><span class="p">;</span>

  <span class="k">while</span> <span class="p">(</span><span class="o">*</span><span class="n">current_char</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="o">*</span><span class="n">current_char</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\n'</span><span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">' '</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">x</span> <span class="o">+=</span> <span class="n">font_space_width</span><span class="p">;</span>
      <span class="n">current_char</span><span class="o">++</span><span class="p">;</span>
      <span class="k">continue</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">uint8_t</span> <span class="n">tile_x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
    <span class="kt">int8_t</span> <span class="n">offset_x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">%</span> <span class="mi">8</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">tile_x</span> <span class="o">&gt;=</span> <span class="n">tiles_length</span><span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>

    <span class="kt">uint8_t</span> <span class="n">w</span> <span class="o">=</span> <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tiles</span><span class="p">[</span><span class="n">tile_x</span> <span class="o">*</span> <span class="mi">8</span><span class="p">],</span> <span class="n">offset_x</span><span class="p">,</span> <span class="n">dy</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">offset_x</span> <span class="o">+</span> <span class="n">w</span> <span class="o">&gt;</span> <span class="mi">8</span> <span class="o">&amp;&amp;</span> <span class="n">tile_x</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&lt;</span> <span class="n">tiles_length</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">offset_x</span> <span class="o">-=</span> <span class="mi">8</span><span class="p">;</span>
      <span class="n">tile_x</span><span class="o">++</span><span class="p">;</span>
      <span class="n">font_render_character_1bpp</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tiles</span><span class="p">[</span><span class="n">tile_x</span> <span class="o">*</span> <span class="mi">8</span><span class="p">],</span> <span class="n">offset_x</span><span class="p">,</span> <span class="n">dy</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">x</span> <span class="o">+=</span> <span class="n">w</span> <span class="o">+</span> <span class="n">font_letter_spacing</span><span class="p">;</span>
    <span class="n">current_char</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="kt">uint8_t</span> <span class="n">rendered_width</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)(</span><span class="n">x</span> <span class="o">-</span> <span class="n">dx</span><span class="p">);</span>

  <span class="n">font_render_line_result_t</span> <span class="n">result</span><span class="p">;</span>
  <span class="n">result</span><span class="p">.</span><span class="n">remainder</span> <span class="o">=</span> <span class="n">current_char</span><span class="p">;</span>
  <span class="n">result</span><span class="p">.</span><span class="n">pixel_count</span> <span class="o">=</span> <span class="n">rendered_width</span> <span class="o">&gt;</span> <span class="mi">0</span>
    <span class="o">?</span> <span class="n">rendered_width</span> <span class="o">-</span> <span class="n">font_letter_spacing</span> <span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
  <span class="n">result</span><span class="p">.</span><span class="n">tile_count</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)((</span><span class="n">x</span> <span class="o">+</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">);</span>

  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>main.c</strong></p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">render_line</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">text</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// create and clear a buffer for 20 1bpp tiles</span>
  <span class="kt">uint8_t</span> <span class="n">text_tile_data</span><span class="p">[</span><span class="n">_tile_size</span> <span class="o">*</span> <span class="mi">20</span><span class="p">];</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">text_tile_data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">text_tile_data</span><span class="p">));</span>

  <span class="c1">// render the characters into 20 tiles, starting at x=4, y=0</span>
  <span class="n">font_render_line_1bpp</span><span class="p">(</span><span class="n">text_tile_data</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">text</span><span class="p">);</span>

  <span class="c1">// upload the 20 tiles to VRAM at id=100</span>
  <span class="n">set_bkg_1bpp_data</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="n">text_tile_data</span><span class="p">);</span>

  <span class="c1">// render the VRAM tiles id=100-120 at x=0-20, y=10 on the background</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">uint8_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">20</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
    <span class="n">set_bkg_tile_xy</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">100</span> <span class="o">+</span> <span class="n">i</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">render_line</span><span class="p">(</span><span class="s">"Hello, world!! This is a variable-width string!"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/blog/variable-width-text/font-on-screen-02.png" alt="Game Boy-scherm met variabele-breedte tekst in een dialoogvenster" loading="lazy" width="320" height="288" style="image-rendering: pixelated;" /></p>

<p>It works!</p>

<p>And here’s the result running on real hardware:</p>

<p><img src="/assets/blog/variable-width-text/demo-modretro.jpeg" alt="Variable-width text rendering on real Game Boy hardware" width="600" /></p>

<p>There’s lots of room for optimization and rendering of new-lines, but that’s outside the scope of this already lengthy blog. Thanks for reading!</p>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[Implementing variable-width font rendering for the Game Boy using GBDK-2020]]></summary></entry><entry><title type="html">MVC on the web frontend</title><link href="https://bonaroo.nl/2025/09/15/model-view-controller.html" rel="alternate" type="text/html" title="MVC on the web frontend" /><published>2025-09-15T00:00:00+00:00</published><updated>2025-09-15T00:00:00+00:00</updated><id>https://bonaroo.nl/2025/09/15/model-view-controller</id><content type="html" xml:base="https://bonaroo.nl/2025/09/15/model-view-controller.html"><![CDATA[<h1 id="functional-patterns-dont-fit-javascript-embracing-model-view-controller-on-the-frontend">Functional patterns don’t fit JavaScript: Embracing Model View Controller on the frontend</h1>

<p>Modern frontend development is dominated by React and other functional-like patterns. There’s likely 1000s of frontend libraries that all attempt the same thing: Creating reusable components, embracing a functional pattern around immutable data structures. However, there is a huge problem with these libraries:</p>

<p><strong>Javascript data structures are inherently not immutable.</strong></p>

<p>This causes tons of issues that need careful crafting. Manipulating an object inside a <code class="language-plaintext highlighter-rouge">useEffect</code> can cause subtle bugs. There’s tons of potential solutions that “enforce” immutability: Immer, Redux, ImmutableJS - but they all require you to model your data around these tools. In some cases, these tools <em>appear</em> to fix the problems, but you will still end up with <a href="https://github.com/immerjs/immer/issues/747">hidden gotchas</a>.</p>

<p>I feel like all these functional-like frameworks try to make Javascript something it is not: A functional language with immutable datastructures.</p>

<p>So what <em>is</em> Javascript?</p>

<h2 id="javascript-is-a-prototype-based-loosely-typed-language-with-robust-object-oriented-programming-support">Javascript is a prototype-based, loosely-typed language with robust Object Oriented Programming support</h2>

<p>Can we design a frontend development style that embraces Javascript for what it is, instead of trying to make it something it is not? I think we can. We can start looking in patterns that work for Object-Oriented-Programming languages: Model-View-Controller (MVC) and Model–View–ViewModel (MVVM).</p>

<p>Reading MVC, you might immediately jump to your experience with MVC platforms like Ruby on Rails, Django, ASP.NET or Laravel, but those are not the kind of MVC I want to explore today.</p>

<p>I am thinking of a fully client-side MVC experience. One where the views somehow convert data to HTML, controllers that handle events triggered by the views, and models that are rendered by the view and updated by the controller, all within the browser.</p>

<h2 id="testable-independent-models-views-and-controllers">Testable, independent models, views and controllers</h2>

<p>I think testing is very important. I want to systematically test every line of code written either by me or AI, including my views. Tests must run fast and have no external dependencies. Ideally, I want to test the models, views or controllers independently from each other. Let’s break them down.</p>

<h3 id="the-model">The Model</h3>

<p>The model is a very important part of the application and requires thorough testing. I want my models to be completely independent from any framework-specific nonsense. Ideally, they are just plain Javascript objects or classes with simple exposed members. They should be easily reconstructible in any state for easy testing.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Counter</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">(</span><span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">count</span> <span class="o">=</span> <span class="nx">count</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">increment</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">count</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">.increment increments counter</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">counter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Counter</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="nx">counter</span><span class="p">.</span><span class="nx">increment</span><span class="p">();</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>As is common in Object-Oriented-Programming, this model couples functions and data, which does not guarantee the model to be easily reconstructible or serializable. It would also expose all functions to any view rendering the model.</p>

<p>A procedural alternative, separating functions and data, could be:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Counter</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">create</span><span class="p">:</span> <span class="p">(</span><span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">count</span> <span class="p">};</span>
  <span class="p">},</span>

  <span class="na">increment</span><span class="p">:</span> <span class="p">(</span><span class="nx">counter</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">counter</span><span class="p">.</span><span class="nx">count</span><span class="o">++</span><span class="p">;</span>
  <span class="p">},</span>
<span class="p">};</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">.increment increments counter</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">counter</span> <span class="o">=</span> <span class="nx">Counter</span><span class="p">.</span><span class="nx">create</span><span class="p">();</span>
  <span class="nx">Counter</span><span class="p">.</span><span class="nx">increment</span><span class="p">(</span><span class="nx">counter</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>This separation of functions and data allows you to render the view without exposing the class methods to the view. It also allows you to work on serialized and deserialized data (e.g. data received via an API, JSON-deserialized data) without the need to reconstruct the object prototypes.</p>

<p>Either approach has its pros and cons, which is beyond the scope of this blog for now.</p>

<div class="mvc-demo" id="model-comparison-demo">
  <h4>Interactive Model Comparison</h4>
  <div class="demo-controls">
    <label>
      <input type="radio" name="model-type" value="oop" checked="" /> Object-Oriented Model
    </label>
    <label>
      <input type="radio" name="model-type" value="procedural" /> Procedural Model
    </label>
  </div>

  <div class="demo-panels">
    <div class="demo-panel">
      <h5>Model Code</h5>
      <pre class="demo-code" id="model-code-display"></pre>
    </div>

    <div class="demo-panel">
      <h5>Test Code</h5>
      <pre class="demo-code" id="test-code-display"></pre>
    </div>

    <div class="demo-panel">
      <h5>Live Test Results</h5>
      <div class="test-results" id="test-results-display"></div>
      <div class="test-meta" id="test-run-meta">Last run: never</div>
      <button id="run-model-test">Run Test</button>
    </div>

    <div class="demo-panel">
      <h5>Serialization Test</h5>
      <div class="serialization-test" id="serialization-test">
        <div>Original: <span id="original-model"></span></div>
        <div>JSON: <span id="serialized-model"></span></div>
        <div>Restored: <span id="restored-model"></span></div>
        <div class="result" id="serialization-result"></div>
      </div>
    </div>
  </div>
</div>

<style>
.mvc-demo {
  margin: 2rem 0;
  padding: 1.5rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  background: #fafafa;
}

.mvc-demo h4 {
  margin-top: 0;
  color: #2c3e50;
}

.demo-controls {
  margin-bottom: 1rem;
}

.demo-controls label {
  display: inline-block;
  margin-right: 1rem;
  cursor: pointer;
}

.demo-panels {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-top: 1rem;
}

.demo-panel {
  padding: 1rem;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.demo-panel h5 {
  margin-top: 0;
  color: #34495e;
}

.demo-code {
  background: #f8f9fa;
  padding: 0.8rem;
  border-radius: 4px;
  font-size: 0.9em;
  overflow-x: auto;
  max-height: 500px;
  margin: 0;
}

.test-results {
  font-family: monospace;
  background: #f8f9fa;
  padding: 0.8rem;
  border-radius: 4px;
  min-height: 50px;
  margin-bottom: 0.5rem;
}

.test-results.success {
  background: #d4edda;
  color: #155724;
}

.test-results.error {
  background: #f8d7da;
  color: #721c24;
}

.test-results.flash {
  animation: test-flash 0.6s ease;
}

.test-meta {
  font-size: 0.85em;
  color: #6c757d;
  margin: 0.25rem 0 0.5rem;
}

@keyframes test-flash {
  0% { box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4); }
  100% { box-shadow: 0 0 0 8px rgba(52, 152, 219, 0); }
}

button {
  background: #3498db;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background: #2980b9;
}

.serialization-test {
  font-family: monospace;
  background: #f8f9fa;
  padding: 0.8rem;
  border-radius: 4px;
}

.serialization-test .result {
  margin-top: 0.5rem;
  font-weight: bold;
}

.result.success {
  color: #155724;
}

.result.error {
  color: #721c24;
}
</style>

<script>
(() => {
  const oopModel = {
    code: `class Counter {
  constructor(count = 0) {
    this.count = count;
  }

  increment() {
    this.count++;
  }
}`,
    test: `test(".increment increments counter", () => {
  const counter = new Counter(0);
  counter.increment();
  assert.strictEqual(counter.count, 1);
});`,
    factory: () => {
      function Counter(count = 0) {
        this.count = count;
      }
      Counter.prototype.increment = function() {
        this.count++;
      };
      return Counter;
    }
  };

  const proceduralModel = {
    code: `const Counter = {
  create: (count = 0) => {
    return { count };
  },

  increment: (counter) => {
    counter.count++;
  },
};`,
    test: `test(".increment increments counter", () => {
  const counter = Counter.create();
  Counter.increment(counter);
  assert.strictEqual(counter.count, 1);
});`,
    factory: () => ({
      create: (count = 0) => ({ count }),
      increment: (counter) => { counter.count++; }
    })
  };

  function updateDisplay(modelType) {
    const model = modelType === 'oop' ? oopModel : proceduralModel;

    document.getElementById('model-code-display').textContent = model.code;
    document.getElementById('test-code-display').textContent = model.test;

    runSerializationTest(modelType);
  }

  function runTest(modelType) {
    const model = modelType === 'oop' ? oopModel : proceduralModel;
    const resultsEl = document.getElementById('test-results-display');
    const metaEl = document.getElementById('test-run-meta');

    try {
      if (modelType === 'oop') {
        const Counter = model.factory();
        const counter = new Counter(0);
        counter.increment();

        if (counter.count === 1) {
          resultsEl.textContent = '✓ Test passed: counter.count === 1';
          resultsEl.className = 'test-results success';
        } else {
          throw new Error(`Expected 1, got ${counter.count}`);
        }
      } else {
        const Counter = model.factory();
        const counter = Counter.create();
        Counter.increment(counter);

        if (counter.count === 1) {
          resultsEl.textContent = '✓ Test passed: counter.count === 1';
          resultsEl.className = 'test-results success';
        } else {
          throw new Error(`Expected 1, got ${counter.count}`);
        }
      }
    } catch (error) {
      resultsEl.textContent = `✗ Test failed: ${error.message}`;
      resultsEl.className = 'test-results error';
    }

    if (metaEl) {
      const label = modelType === 'oop' ? 'Object-Oriented' : 'Procedural';
      metaEl.textContent = `Last run: ${label} model at ${new Date().toLocaleTimeString()}`;
    }

    resultsEl.classList.remove('flash');
    void resultsEl.offsetWidth;
    resultsEl.classList.add('flash');
  }

  function runSerializationTest(modelType) {
    const originalEl = document.getElementById('original-model');
    const serializedEl = document.getElementById('serialized-model');
    const restoredEl = document.getElementById('restored-model');
    const resultEl = document.getElementById('serialization-result');

    try {
      let original, serialized, restored;

      if (modelType === 'oop') {
        const Counter = oopModel.factory();
        original = new Counter(5);
        originalEl.textContent = `Counter { count: ${original.count} }`;

        serialized = JSON.stringify(original);
        serializedEl.textContent = serialized;

        restored = JSON.parse(serialized);
        restoredEl.textContent = `Object { count: ${restored.count} }`;

        if (typeof restored.increment === 'function') {
          resultEl.textContent = '✓ Methods preserved';
          resultEl.className = 'result success';
        } else {
          resultEl.textContent = '✗ Methods lost in serialization';
          resultEl.className = 'result error';
        }
      } else {
        const Counter = proceduralModel.factory();
        original = Counter.create(5);
        originalEl.textContent = `{ count: ${original.count} }`;

        serialized = JSON.stringify(original);
        serializedEl.textContent = serialized;

        restored = JSON.parse(serialized);
        restoredEl.textContent = `{ count: ${restored.count} }`;

        // Test if the restored object works with the Counter functions
        Counter.increment(restored);
        if (restored.count === 6) {
          resultEl.textContent = '✓ Data structure preserved, functions work';
          resultEl.className = 'result success';
        } else {
          resultEl.textContent = '✗ Functions don\'t work with restored data';
          resultEl.className = 'result error';
        }
      }
    } catch (error) {
      resultEl.textContent = `✗ Serialization failed: ${error.message}`;
      resultEl.className = 'result error';
    }
  }

  // Event listeners
  document.addEventListener('DOMContentLoaded', () => {
    const radios = document.querySelectorAll('input[name="model-type"]');
    radios.forEach(radio => {
      radio.addEventListener('change', (e) => {
        updateDisplay(e.target.value);
      });
    });

    const runTestButton = document.getElementById('run-model-test');
    if (runTestButton) {
      runTestButton.addEventListener('click', () => {
        try {
          const selectedType = document.querySelector('input[name="model-type"]:checked').value;
          runTest(selectedType);
        } catch (error) {
          console.error('Error running test:', error);
          const resultsEl = document.getElementById('test-results-display');
          if (resultsEl) {
            resultsEl.textContent = `✗ Test failed: ${error.message}`;
            resultsEl.className = 'test-results error';
          }
        }
      });
    } else {
      console.error('Run test button not found');
    }

    // Initial setup
    updateDisplay('oop');
    runTest('oop');
  });
})();
</script>

<h2 id="the-view">The View</h2>

<p>I want my (client-side) views to be rendered in any state based on a model that can easily be initialized in any state. It will likely involve some kind of HTML template or existing HTML fragment which can be manipulated in a <code class="language-plaintext highlighter-rouge">render()</code> call, rendering the model by updating the DOM. Any user interaction should trigger an event, the view should avoid local state.</p>

<p>These views should be able to exist self-contained without external styles or other components. This should make it possible to preview and test the view with only the model (or a mock copy of the model), completely independent from any other views in any webpage.</p>

<p>One could design the views without relying on the DOM, but this will likely involve some kind of vdom or other means of patching the DOM based on rendered template strings. From a programmer’s perspective, this is nice and easy, but from a optimization perspective - I’m not a fan.</p>

<p>Instead, I think views should embrace the dom, either injecting a template once or relying on an existing HTML document, and manipulating it based on the model’s properties on render.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;template</span> <span class="na">id=</span><span class="s">"counter-view-template"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;style&gt;</span>
    <span class="nc">.root</span> <span class="p">{</span>
      <span class="nl">color</span><span class="p">:</span> <span class="no">blue</span><span class="p">;</span>
      <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">blue</span><span class="p">;</span>
      <span class="nl">padding</span><span class="p">:</span> <span class="m">3px</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nc">.count</span> <span class="p">{</span>
      <span class="nl">font-weight</span><span class="p">:</span> <span class="nb">bold</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="nt">&lt;/style&gt;</span>
  <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"root"</span><span class="nt">&gt;</span>
    Count: <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"count"</span> <span class="na">data-count</span><span class="nt">&gt;&lt;/span&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">data-increment</span><span class="nt">&gt;</span>+<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;/span&gt;</span>
<span class="nt">&lt;/template&gt;</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">CounterView</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">template</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-view-template</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">shadowRoot</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">attachShadow</span><span class="p">({</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">open</span><span class="dl">"</span> <span class="p">});</span>
    <span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">countElement</span> <span class="o">=</span> <span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-count]</span><span class="dl">"</span><span class="p">);</span>

    <span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-increment]</span><span class="dl">"</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleIncrementClickEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">));</span>
  <span class="p">}</span>

  <span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">countElement</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span> <span class="o">??</span> <span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">handleIncrementClickEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">bubbles</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">composed</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">}));</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-view</span><span class="dl">"</span><span class="p">,</span> <span class="nx">CounterView</span><span class="p">);</span>
</code></pre></div></div>

<p>The view can be used like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;counter-view</span> <span class="na">id=</span><span class="s">"my-counter-view"</span><span class="nt">&gt;&lt;/counter-view&gt;</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">counter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Counter</span><span class="p">(</span><span class="mi">69</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">myCounterView</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-counter-view</span><span class="dl">"</span><span class="p">);</span>

<span class="nx">myCounterView</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">);</span>

<span class="nx">myCounterView</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">counter</span><span class="p">.</span><span class="nx">increment</span><span class="p">();</span>
  <span class="nx">myCounterView</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Example fiddle: https://jsfiddle.net/cv23r7kd/7/</p>

<div class="mvc-demo" id="counter-mvc-demo">
  <h4>Live Counter MVC Demo</h4>

  <div class="demo-controls">
    <label>
      <input type="radio" name="mvc-model-type" value="oop" checked="" /> Object-Oriented Model
    </label>
    <label>
      <input type="radio" name="mvc-model-type" value="procedural" /> Procedural Model
    </label>
  </div>

  <div class="mvc-sections">
    <div class="mvc-section">
      <h5>Model <span class="section-highlight">M</span></h5>
      <div class="model-state">
        <div>Current state: <code id="model-state-display">{ count: 0 }</code></div>
        <button id="direct-increment">Increment via Model</button>
      </div>
    </div>

    <div class="mvc-section">
      <h5>View <span class="section-highlight">V</span></h5>
      <div class="view-container">
        <counter-view-demo id="demo-counter-view"></counter-view-demo>
      </div>
      <div class="view-info">
        <div>Last render: <span id="last-render-time">Never</span></div>
        <button id="force-render">Force Re-render</button>
      </div>
    </div>

    <div class="mvc-section">
      <h5>Controller <span class="section-highlight">C</span></h5>
      <div class="controller-info">
        <div>Event listeners: <span id="event-listeners-count">1</span></div>
        <div>Updates handled: <span id="updates-handled">0</span></div>
        <button id="toggle-controller">Disable Controller</button>
      </div>
    </div>
  </div>

  <div class="event-log">
    <h5>Event Flow</h5>
    <div id="event-log-display"></div>
    <button id="clear-log">Clear Log</button>
  </div>
</div>

<template id="counter-view-demo-template">
  <style>
    .root {
      color: blue;
      border: 1px solid blue;
      padding: 3px;
      display: inline-block;
      font-family: monospace;
    }
    .count {
      font-weight: bold;
    }
    button {
      background: blue;
      color: white;
      border: none;
      padding: 2px 6px;
      margin-left: 5px;
      cursor: pointer;
    }
  </style>
  <span class="root">
    Count: <span class="count" data-count=""></span>
    <button data-increment="">+</button>
  </span>
</template>

<style>
.mvc-sections {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
  margin: 1rem 0;
}

.mvc-section {
  padding: 1rem;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  position: relative;
}

.section-highlight {
  background: #e74c3c;
  color: white;
  padding: 2px 6px;
  border-radius: 50%;
  font-weight: bold;
  font-size: 0.8em;
}

.model-state, .view-info, .controller-info {
  margin-top: 0.5rem;
}

.model-state code {
  background: #f8f9fa;
  padding: 2px 4px;
  border-radius: 3px;
}

.view-container {
  margin: 0.5rem 0;
  padding: 0.5rem;
  background: #f8f9fa;
  border-radius: 4px;
  min-height: 40px;
}

.event-log {
  margin-top: 1rem;
  padding: 1rem;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
}

#event-log-display {
  background: #f8f9fa;
  padding: 0.8rem;
  border-radius: 4px;
  height: 120px;
  overflow-y: auto;
  font-family: monospace;
  font-size: 0.85em;
  white-space: pre-line;
}

.event-entry {
  margin: 2px 0;
  padding: 2px 4px;
  border-radius: 2px;
}

.event-entry.model { background: #e8f5e8; }
.event-entry.view { background: #e8f4fd; }
.event-entry.controller { background: #fff2e8; }

.disabled {
  opacity: 0.5;
  pointer-events: none;
}
</style>

<script>
(() => {
  // Counter Models
  const oopCounterModel = () => {
    function Counter(count = 0) {
      this.count = count;
    }
    Counter.prototype.increment = function() {
      this.count++;
    };
    return Counter;
  };

  const proceduralCounterModel = () => ({
    create: (count = 0) => ({ count }),
    increment: (counter) => { counter.count++; }
  });

  // Custom Element for CounterView
  class CounterViewDemo extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById("counter-view-demo-template");
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.appendChild(template.content.cloneNode(true));

      this.countElement = shadowRoot.querySelector("[data-count]");
      shadowRoot.querySelector("[data-increment]")
        .addEventListener("click", this.handleIncrementClickEvent.bind(this));
    }

    render(counter) {
      const count = typeof counter.count !== 'undefined' ? counter.count : 0;
      this.countElement.textContent = String(count);
      logEvent('view', `View rendered with count: ${count}`);
      document.getElementById('last-render-time').textContent = new Date().toLocaleTimeString();
    }

    handleIncrementClickEvent(event) {
      event.preventDefault();
      logEvent('view', 'View: Increment button clicked, dispatching counter-increment event');
      this.dispatchEvent(new CustomEvent("counter-increment", {
        bubbles: true,
        composed: true
      }));
    }
  }

  // Controller
  class CounterController {
    constructor(counter, counterView, modelType) {
      this.counter = counter;
      this.modelType = modelType;
      this.updatesHandled = 0;
      this.enabled = true;

      this.handleCounterIncrement = () => {
        if (!this.enabled) return;

        logEvent('controller', 'Controller: Handling counter-increment event');

        if (this.modelType === 'oop') {
          this.counter.increment();
        } else {
          this.counterModel.increment(this.counter);
        }

        this.updatesHandled++;
        document.getElementById('updates-handled').textContent = this.updatesHandled;

        logEvent('model', `Model: Counter incremented to ${this.counter.count}`);

        this.counterView.render(this.counter);
        this.updateModelStateDisplay();
      };

      this.setView(counterView);
    }

    setView(newCounterView) {
      if (this.counterView)
        this.removeView();

      this.counterView = newCounterView;
      this.counterView.render(this.counter);
      this.counterView.addEventListener("counter-increment", this.handleCounterIncrement);

      document.getElementById('event-listeners-count').textContent = '1';
    }

    removeView() {
      if (!this.counterView) return;
      this.counterView.removeEventListener("counter-increment", this.handleCounterIncrement);
      this.counterView = null;
      document.getElementById('event-listeners-count').textContent = '0';
    }

    setModelType(modelType, counterModel) {
      this.modelType = modelType;
      this.counterModel = counterModel;
    }

    updateModelStateDisplay() {
      document.getElementById('model-state-display').textContent =
        `{ count: ${this.counter.count} }`;
    }

    toggleEnabled() {
      this.enabled = !this.enabled;
      const toggleBtn = document.getElementById('toggle-controller');
      const controllerSection = toggleBtn.closest('.mvc-section');

      if (this.enabled) {
        toggleBtn.textContent = 'Disable Controller';
        controllerSection.classList.remove('disabled');
        logEvent('controller', 'Controller: Enabled');
      } else {
        toggleBtn.textContent = 'Enable Controller';
        controllerSection.classList.add('disabled');
        logEvent('controller', 'Controller: Disabled');
      }
    }
  }

  // Event logging
  let eventLogEl;

  function logEvent(type, message) {
    if (!eventLogEl) return;

    const timestamp = new Date().toLocaleTimeString();
    const entry = document.createElement('div');
    entry.className = `event-entry ${type}`;
    entry.textContent = `${timestamp} [${type.toUpperCase()}] ${message}`;

    eventLogEl.appendChild(entry);
    eventLogEl.scrollTop = eventLogEl.scrollHeight;
  }

  // Demo state
  let currentModelType = 'oop';
  let counter, counterModel, counterView, controller;

  function initializeDemo(modelType) {
    if (controller) {
      controller.removeView();
    }

    currentModelType = modelType;

    if (modelType === 'oop') {
      const Counter = oopCounterModel();
      counter = new Counter(0);
      counterModel = null;
    } else {
      counterModel = proceduralCounterModel();
      counter = counterModel.create(0);
    }

    counterView = document.getElementById('demo-counter-view');
    controller = new CounterController(counter, counterView, modelType);
    controller.setModelType(modelType, counterModel);
    controller.updateModelStateDisplay();

    logEvent('controller', `Demo initialized with ${modelType} model`);
  }

  // Event handlers
  document.addEventListener('DOMContentLoaded', () => {
    // Register custom element
    if (!customElements.get('counter-view-demo')) {
      customElements.define('counter-view-demo', CounterViewDemo);
    }

    eventLogEl = document.getElementById('event-log-display');

    // Model type radio buttons
    document.querySelectorAll('input[name="mvc-model-type"]').forEach(radio => {
      radio.addEventListener('change', (e) => {
        initializeDemo(e.target.value);
      });
    });

    // Direct increment button
    document.getElementById('direct-increment').addEventListener('click', () => {
      if (currentModelType === 'oop') {
        counter.increment();
      } else {
        counterModel.increment(counter);
      }

      logEvent('model', `Model: Direct increment to ${counter.count}`);
      counterView.render(counter);
      controller.updateModelStateDisplay();
    });

    // Force render button
    document.getElementById('force-render').addEventListener('click', () => {
      logEvent('view', 'Manual re-render triggered');
      counterView.render(counter);
    });

    // Toggle controller button
    document.getElementById('toggle-controller').addEventListener('click', () => {
      controller.toggleEnabled();
    });

    // Clear log button
    document.getElementById('clear-log').addEventListener('click', () => {
      eventLogEl.innerHTML = '';
    });

    // Initialize demo
    setTimeout(() => initializeDemo('oop'), 100);
  });
})();
</script>

<h3 id="testing-the-view">Testing the view</h3>

<p>In my opinion, views need to be visually inspected in any state. You can achieve this by rendering your view in various states on a demo page. This will demonstrate how your view can be used and at the same time it can be used to verify they’re rendered correctly in the browser you’re using.</p>

<p>If you want automated tests, you can either run tests on NodeJS with JSDOM, or you can run tests using a client-side test runner like Mocha, in a real browser. Ideally, you want to design these tests in a way you can run them in the browser and in NodeJS. The NodeJS test suite can then run the first pass to catch any issues with the code, and another more sophisticated tool can run the test suite in a wide array of browsers and report back browser-specific issues.</p>

<p>Your tests could look something like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">counter</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">view</span><span class="p">;</span>

<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">counter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Counter</span><span class="p">(</span><span class="mi">69</span><span class="p">);</span>
  <span class="nx">view</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-view</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">view</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">render renders the counter's count</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">view</span><span class="p">.</span><span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-count]</span><span class="dl">"</span><span class="p">).</span><span class="nx">textContent</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">toEqual</span><span class="p">(</span><span class="dl">"</span><span class="s2">69</span><span class="dl">"</span><span class="p">)</span>
<span class="p">});</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">clicking increment button emits counter-increment event</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">events</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="nx">view</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">events</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="p">});</span>
  <span class="nx">view</span><span class="p">.</span><span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-increment]</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">events</span><span class="p">).</span><span class="nx">toMatchObject</span><span class="p">([{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span> <span class="p">}]);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>You can use <a href="https://mochajs.org/">Mocha</a> and <a href="https://www.npmjs.com/package/expect">Jest’s expect</a> to run your tests in the browser. You can use <a href="https://github.com/jsdom/jsdom">JSDom</a> to run the same tests in NodeJS.</p>

<div class="mvc-demo" id="view-testing-playground">
  <h4>View Testing Playground</h4>

  <div class="playground-sections">
    <div class="playground-section">
      <h5>Model State Editor</h5>
      <div class="state-editor">
        <label>Count: <input type="number" id="test-count-input" value="42" min="0" max="999" /></label>
        <button id="apply-test-state">Apply State</button>
      </div>
    </div>

    <div class="playground-section">
      <h5>Live View</h5>
      <div class="view-preview">
        <counter-view-test id="test-counter-view"></counter-view-test>
      </div>
    </div>

    <div class="playground-section">
      <h5>DOM Inspector</h5>
      <div class="dom-inspector">
        <div class="inspector-section">
          <strong>Rendered Count:</strong>
          <div id="shadow-dom-structure"></div>
        </div>
        <div class="inspector-section">
          <strong>Event Listeners:</strong>
          <div id="event-listeners-info"></div>
        </div>
        <button id="inspect-view">Inspect View</button>
      </div>
    </div>
  </div>

  <div class="test-runner-section">
    <h5>Automated Tests</h5>
    <div class="test-controls">
      <button id="run-render-test">Test: Render displays count</button>
      <button id="run-click-test">Test: Button click emits event</button>
      <button id="run-all-tests">Run All Tests</button>
    </div>
    <div class="test-results-container">
      <div id="automated-test-results"></div>
    </div>
  </div>
</div>

<template id="counter-view-test-template">
  <style>
    .root {
      color: blue;
      border: 1px solid blue;
      padding: 3px;
      display: inline-block;
      font-family: monospace;
      background: white;
    }
    .count {
      font-weight: bold;
    }
    button {
      background: blue;
      color: white;
      border: none;
      padding: 2px 6px;
      margin-left: 5px;
      cursor: pointer;
    }
    button:hover {
      background: darkblue;
    }
  </style>
  <span class="root">
    Count: <span class="count" data-count=""></span>
    <button data-increment="">+</button>
  </span>
</template>

<style>
.playground-sections {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 1rem;
  margin: 1rem 0;
}

.playground-section {
  padding: 1rem;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.state-editor label {
  display: block;
  margin-bottom: 0.5rem;
}

.state-editor input {
  width: 80px;
  padding: 0.25rem;
  border: 1px solid #ccc;
  border-radius: 3px;
}

.view-preview {
  padding: 1rem;
  background: #f8f9fa;
  border-radius: 4px;
  text-align: center;
  min-height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dom-inspector {
  font-size: 0.9em;
}

.inspector-section {
  margin-bottom: 1rem;
}

.inspector-section strong {
  display: block;
  margin-bottom: 0.25rem;
  color: #2c3e50;
}

#shadow-dom-structure, #event-listeners-info {
  background: #f8f9fa;
  padding: 0.5rem;
  border-radius: 3px;
  font-family: monospace;
  white-space: pre-line;
  font-size: 0.85em;
  max-height: 100px;
  overflow-y: auto;
}

.test-runner-section {
  margin-top: 1rem;
  padding: 1rem;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.test-controls {
  margin-bottom: 1rem;
}

.test-controls button {
  margin-right: 0.5rem;
  margin-bottom: 0.5rem;
}

#automated-test-results {
  background: #f8f9fa;
  padding: 1rem;
  border-radius: 4px;
  font-family: monospace;
  font-size: 0.9em;
  min-height: 80px;
  white-space: pre-line;
}

.test-pass {
  color: #28a745;
}

.test-fail {
  color: #dc3545;
}

.test-summary {
  font-weight: bold;
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid #dee2e6;
}
</style>

<script>
(() => {
  // Custom Element for testing
  class CounterViewTest extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById("counter-view-test-template");
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.appendChild(template.content.cloneNode(true));

      this.countElement = shadowRoot.querySelector("[data-count]");
      this.incrementButton = shadowRoot.querySelector("[data-increment]");
      this.incrementButton.addEventListener("click", this.handleIncrementClickEvent.bind(this));
    }

    render(counter) {
      const count = typeof counter.count !== 'undefined' ? counter.count : 0;
      this.countElement.textContent = String(count);
    }

    handleIncrementClickEvent(event) {
      event.preventDefault();
      this.dispatchEvent(new CustomEvent("counter-increment", {
        bubbles: true,
        composed: true
      }));
    }
  }

  let testView;
  let testResults = [];

  function updateTestView() {
    const count = parseInt(document.getElementById('test-count-input').value, 10);
    const testModel = { count };
    testView.render(testModel);
  }

  function inspectView() {
    if (!testView || !testView.shadowRoot) return;

    const shadowRoot = testView.shadowRoot;

    // Inspect DOM structure
    const structureEl = document.getElementById('shadow-dom-structure');
    const countEl = shadowRoot.querySelector('[data-count]');
    const countValue = countEl ? countEl.textContent : 'N/A';
    structureEl.textContent = `Count: ${countValue}`;

    // Inspect event listeners
    const listenersEl = document.getElementById('event-listeners-info');
    const button = shadowRoot.querySelector('[data-increment]');
    const hasClickListener = button && typeof button.onclick === 'function' ||
                           (button && button._listeners && button._listeners.click);

    listenersEl.textContent = `Button element: ${button ? 'found' : 'not found'}\nClick listener: ${hasClickListener ? 'attached' : 'via addEventListener (hidden)'}`;
  }

  function runTest(testName, testFn) {
    try {
      testFn();
      const result = `✓ PASS: ${testName}`;
      testResults.push({ type: 'pass', message: result });
      return true;
    } catch (error) {
      const result = `✗ FAIL: ${testName}\n  ${error.message}`;
      testResults.push({ type: 'fail', message: result });
      return false;
    }
  }

  function displayTestResults() {
    const resultsEl = document.getElementById('automated-test-results');
    const passCount = testResults.filter(r => r.type === 'pass').length;
    const failCount = testResults.filter(r => r.type === 'fail').length;

    const output = testResults.map(result =>
      `<span class="test-${result.type}">${result.message}</span>`
    ).join('\n\n');

    const summary = `<div class="test-summary">${passCount} passed, ${failCount} failed</div>`;

    resultsEl.innerHTML = output + summary;
  }

  function runRenderTest() {
    testResults = [];

    runTest("render displays the correct count", () => {
      const testModel = { count: 123 };
      testView.render(testModel);

      const countElement = testView.shadowRoot.querySelector("[data-count]");
      if (!countElement) {
        throw new Error("Count element not found");
      }

      const displayedText = countElement.textContent;
      if (displayedText !== "123") {
        throw new Error(`Expected "123", got "${displayedText}"`);
      }
    });

    displayTestResults();
  }

  function runClickTest() {
    testResults = [];

    runTest("button click emits counter-increment event", () => {
      let eventFired = false;
      let eventDetails = null;

      const listener = (event) => {
        eventFired = true;
        eventDetails = { type: event.type, bubbles: event.bubbles };
      };

      testView.addEventListener("counter-increment", listener);

      const button = testView.shadowRoot.querySelector("[data-increment]");
      if (!button) {
        throw new Error("Increment button not found");
      }

      button.click();

      testView.removeEventListener("counter-increment", listener);

      if (!eventFired) {
        throw new Error("counter-increment event was not fired");
      }

      if (eventDetails.type !== "counter-increment") {
        throw new Error(`Expected event type "counter-increment", got "${eventDetails.type}"`);
      }

      if (!eventDetails.bubbles) {
        throw new Error("Event should bubble");
      }
    });

    displayTestResults();
  }

  function runAllTests() {
    testResults = [];

    runTest("render displays the correct count", () => {
      const testModel = { count: 456 };
      testView.render(testModel);

      const countElement = testView.shadowRoot.querySelector("[data-count]");
      const displayedText = countElement.textContent;

      if (displayedText !== "456") {
        throw new Error(`Expected "456", got "${displayedText}"`);
      }
    });

    runTest("button click emits counter-increment event", () => {
      let eventFired = false;
      const listener = () => { eventFired = true; };

      testView.addEventListener("counter-increment", listener);
      testView.shadowRoot.querySelector("[data-increment]").click();
      testView.removeEventListener("counter-increment", listener);

      if (!eventFired) {
        throw new Error("counter-increment event was not fired");
      }
    });

    runTest("view updates when rendered with different counts", () => {
      testView.render({ count: 100 });
      let count1 = testView.shadowRoot.querySelector("[data-count]").textContent;

      testView.render({ count: 200 });
      let count2 = testView.shadowRoot.querySelector("[data-count]").textContent;

      if (count1 !== "100" || count2 !== "200") {
        throw new Error(`View not updating properly: ${count1} -> ${count2}`);
      }
    });

    displayTestResults();
  }

  // Initialize playground
  document.addEventListener('DOMContentLoaded', () => {
    // Register custom element
    if (!customElements.get('counter-view-test')) {
      customElements.define('counter-view-test', CounterViewTest);
    }

    testView = document.getElementById('test-counter-view');

    // Event listeners
    document.getElementById('apply-test-state').addEventListener('click', updateTestView);
    document.getElementById('inspect-view').addEventListener('click', inspectView);
    document.getElementById('run-render-test').addEventListener('click', runRenderTest);
    document.getElementById('run-click-test').addEventListener('click', runClickTest);
    document.getElementById('run-all-tests').addEventListener('click', runAllTests);
    testView.addEventListener('counter-increment', () => {
      const input = document.getElementById('test-count-input');
      const current = parseInt(input.value, 10) || 0;
      input.value = String(current + 1);
      updateTestView();
      inspectView();
    });

    document.getElementById('test-count-input').addEventListener('input', updateTestView);

    // Initialize
    setTimeout(() => {
      updateTestView();
      inspectView();
    }, 100);
  });
})();
</script>

<h2 id="the-controller">The Controller</h2>

<p>The controller depends on the model and view. The implementation is already obvious from the previous example on how the view and model are used together:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">CounterController</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">(</span><span class="nx">counter</span><span class="p">,</span> <span class="nx">counterView</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">counter</span> <span class="o">=</span> <span class="nx">counter</span><span class="p">;</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">handleCounterIncrement</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">counter</span><span class="p">.</span><span class="nx">increment</span><span class="p">();</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">counter</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">setView</span><span class="p">(</span><span class="nx">counterView</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">setView</span><span class="p">(</span><span class="nx">newCounterView</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">)</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">removeView</span><span class="p">();</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span> <span class="o">=</span> <span class="nx">newCounterView</span><span class="p">;</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">counter</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleCounterIncrement</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">removeView</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">)</span>
      <span class="k">return</span><span class="p">;</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">removeEventListener</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleCounterIncrement</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">counterView</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The usage example can then be changed to:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">counter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Counter</span><span class="p">(</span><span class="mi">69</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">counterView</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-counter-view</span><span class="dl">"</span><span class="p">);</span>
<span class="k">new</span> <span class="nx">CounterController</span><span class="p">(</span><span class="nx">counter</span><span class="p">,</span> <span class="nx">counterView</span><span class="p">);</span>
</code></pre></div></div>

<p>You can test it here: https://jsfiddle.net/cv23r7kd/11/</p>

<p>You might look at this controller and think nothing much of it, but for “real” use-cases, the controller can grow fast. It can coordinate the rendering of multiple views of the same model, it can handle async actions of the view or model, and it could dynamically swap out models and views.</p>

<h3 id="testing-the-controller">Testing the controller</h3>

<p>Since the controller also relies on the views, it must be tested in the same environment as the views: The browser and/or JSDom. For simplicity, I choose to use a custom view for testing, since there’s no easy way to assert the rendered result on the <code class="language-plaintext highlighter-rouge">CounterView</code>. My <code class="language-plaintext highlighter-rouge">TestCounterView</code> extends <code class="language-plaintext highlighter-rouge">EventTarget</code> for <code class="language-plaintext highlighter-rouge">addEventListener</code> and <code class="language-plaintext highlighter-rouge">removeEventListener</code>.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">TestCounterView</span> <span class="kd">extends</span> <span class="nx">EventTarget</span> <span class="p">{</span>
  <span class="nx">state</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>

  <span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span> <span class="na">count</span><span class="p">:</span> <span class="nx">counter</span><span class="p">.</span><span class="nx">count</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">let</span> <span class="nx">counter</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">counterView</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">controller</span><span class="p">;</span>

<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">counter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Counter</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="nx">counterView</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestCounterView</span><span class="p">();</span>
  <span class="nx">controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CounterController</span><span class="p">(</span><span class="nx">counter</span><span class="p">,</span> <span class="nx">counterView</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">controller renders the view</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">state</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="mi">0</span> <span class="p">});</span>
<span class="p">});</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">controller increments and renders on counter-increment event</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">counterView</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">));</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">state</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
<span class="p">});</span>

<span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">removeView removes the counter-increment listener</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">controller</span><span class="p">.</span><span class="nx">removeView</span><span class="p">();</span>

  <span class="nx">counterView</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">));</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">counterView</span><span class="p">.</span><span class="nx">state</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="mi">0</span> <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="no-observing-of-the-model-state">No observing of the model state</h2>

<p>A lot of MVC and MVVM patterns currently present on the web (Backbone, Knockout, Angular 1) relied heavily on observables or automatic bindings. This makes it easy for multiple views to automatically update when a model changes, but event-driven programming is hard to debug.</p>

<p>I therefore choose not to make my models observable. I want my models to remain completely agnostic of any knowledge of the frontend.</p>

<p>For applications that rely on a large web of models, views and controllers, the controller layer needs to coordinate the updates between all models and views.</p>

<h2 id="the-verbosity-of-the-dom">The verbosity of the DOM</h2>

<p>The current implementation is pretty verbose and includes more boilerplate than I would like. I expect most of the boilerplate can be DRYed up using small helpers. For example:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;template</span> <span class="na">id=</span><span class="s">"counter-view-template"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;style&gt;</span>
    <span class="nc">.root</span> <span class="p">{</span>
      <span class="nl">color</span><span class="p">:</span> <span class="no">blue</span><span class="p">;</span>
      <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">blue</span><span class="p">;</span>
      <span class="nl">padding</span><span class="p">:</span> <span class="m">3px</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nc">.count</span> <span class="p">{</span>
      <span class="nl">font-weight</span><span class="p">:</span> <span class="nb">bold</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="nt">&lt;/style&gt;</span>
  <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"root"</span><span class="nt">&gt;</span>
    Count: <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"count"</span> <span class="na">data-ref=</span><span class="s">"count"</span><span class="nt">&gt;&lt;/span&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">data-ref=</span><span class="s">"increment"</span><span class="nt">&gt;</span>+<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;/span&gt;</span>
<span class="nt">&lt;/template&gt;</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BoundTemplateHTMLElement</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="nx">refs</span> <span class="o">=</span> <span class="p">{};</span>

  <span class="kd">constructor</span><span class="p">(</span><span class="nx">templateId</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">();</span>

    <span class="kd">const</span> <span class="nx">template</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">templateId</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">attachShadow</span><span class="p">({</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">open</span><span class="dl">"</span> <span class="p">});</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">elem</span> <span class="k">of</span> <span class="k">this</span><span class="p">.</span><span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-ref]</span><span class="dl">"</span><span class="p">))</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">[</span><span class="nx">elem</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data-ref</span><span class="dl">"</span><span class="p">)]</span> <span class="o">=</span> <span class="nx">elem</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">emit</span><span class="p">(</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="nx">type</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">bubbles</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">composed</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">}));</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="nx">CounterView</span> <span class="kd">extends</span> <span class="nx">BoundTemplateHTMLElement</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-view-template</span><span class="dl">"</span><span class="p">);</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">increment</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleIncrementClickEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">));</span>
  <span class="p">}</span>

  <span class="nx">render</span><span class="p">(</span><span class="nx">counter</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">count</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">count</span> <span class="o">??</span> <span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">handleIncrementClickEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-increment</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">"</span><span class="s2">counter-view</span><span class="dl">"</span><span class="p">,</span> <span class="nx">CounterView</span><span class="p">);</span>
</code></pre></div></div>

<p>An obvious alternative is <a href="https://lit.dev/docs/components/rendering/"><code class="language-plaintext highlighter-rouge">LitElement</code></a>, which fits in nicely with this pattern. Lit does have <a href="https://lit.dev/docs/components/properties/">reactive properties</a>, but you can just use these to read and write the view components (which also makes testing easier).</p>

<div class="mvc-demo" id="controller-coordination-demo">
  <h4>Controller Coordination: Multiple Views</h4>

  <p>This demonstrates how a single controller can coordinate multiple views of the same model, handling events and ensuring all views stay synchronized.</p>

  <div class="coordination-controls">
    <button id="add-view">Add Counter View</button>
    <button id="remove-view">Remove Last View</button>
    <button id="reset-model">Reset Counter</button>
    <span>Current count: <strong id="shared-count">0</strong></span>
  </div>

  <div class="views-container" id="views-container">
  </div>

  <div class="coordination-info">
    <h5>Controller Status</h5>
    <div class="status-grid">
      <div>Active Views: <span id="active-views-count">0</span></div>
      <div>Event Listeners: <span id="total-listeners">0</span></div>
      <div>Last Update: <span id="last-update">Never</span></div>
    </div>
  </div>
</div>

<div class="mvc-demo" id="boilerplate-evolution-demo">
  <h4>Boilerplate Evolution Demo</h4>

  <p>See how the verbose DOM manipulation can be simplified with helper classes while maintaining the same MVC principles.</p>

  <div class="evolution-tabs">
    <button class="tab-button active" data-tab="vanilla">Vanilla DOM</button>
    <button class="tab-button" data-tab="helper">With Helpers</button>
    <button class="tab-button" data-tab="lit">LitElement Style</button>
  </div>

  <div class="evolution-content">
    <div class="tab-content active" id="vanilla-tab">
      <div class="code-example">
        <h5>Vanilla Implementation</h5>
        <pre class="demo-code" id="vanilla-code"></pre>
      </div>
      <div class="demo-result">
        <h5>Result</h5>
        <vanilla-counter-demo id="vanilla-demo"></vanilla-counter-demo>
      </div>
    </div>

    <div class="tab-content" id="helper-tab">
      <div class="code-example">
        <h5>With Helper Classes</h5>
        <pre class="demo-code" id="helper-code"></pre>
      </div>
      <div class="demo-result">
        <h5>Result</h5>
        <helper-counter-demo id="helper-demo"></helper-counter-demo>
      </div>
    </div>

    <div class="tab-content" id="lit-tab">
      <div class="code-example">
        <h5>LitElement Style</h5>
        <pre class="demo-code" id="lit-code"></pre>
      </div>
      <div class="demo-result">
        <h5>Result (Simulated)</h5>
        <lit-counter-demo id="lit-demo"></lit-counter-demo>
      </div>
    </div>
  </div>
</div>

<!-- Templates for multiple view demo -->
<template id="multi-counter-template">
  <style>
    .multi-counter {
      display: inline-block;
      margin: 0.25rem;
      padding: 0.5rem;
      border: 1px solid #007bff;
      border-radius: 4px;
      background: white;
      font-family: monospace;
    }
    .multi-counter button {
      background: #007bff;
      color: white;
      border: none;
      padding: 0.25rem 0.5rem;
      margin-left: 0.5rem;
      border-radius: 3px;
      cursor: pointer;
    }
    .count-display {
      font-weight: bold;
    }
  </style>
  <div class="multi-counter">
    <span class="count-display" data-count="">0</span>
    <button data-increment="">+</button>
  </div>
</template>

<style>
.coordination-controls {
  margin: 1rem 0;
  display: flex;
  gap: 1rem;
  align-items: center;
  flex-wrap: wrap;
}

.views-container {
  min-height: 100px;
  padding: 1rem;
  border: 2px dashed #ddd;
  border-radius: 4px;
  margin: 1rem 0;
  background: #f8f9fa;
}

.coordination-info {
  margin-top: 1rem;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1rem;
  margin-top: 0.5rem;
}

.evolution-tabs {
  display: flex;
  gap: 0;
  margin-bottom: 1rem;
}

.tab-button {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background: #f8f9fa !important;
  color: #333 !important;
  cursor: pointer;
  border-bottom: none;
}

.tab-button.active {
  background: white !important;
  color: #333 !important;
  border-bottom: 1px solid white;
  position: relative;
  z-index: 1;
}

.tab-button:hover {
  background: #e2e6ea !important;
  color: #333 !important;
}

.evolution-content {
  border: 1px solid #ddd;
  padding: 1rem;
  background: white;
}

.tab-content {
  display: none;
}

.tab-content.active {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
  min-height: 500px;
}

.code-example h5, .demo-result h5 {
  margin-top: 0;
}

.demo-result {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 120px;
  padding: 1rem;
  justify-content: center;
}

</style>

<script>
(() => {
  // Multi-View Controller Demo
  class MultiCounterView extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById("multi-counter-template");
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.appendChild(template.content.cloneNode(true));

      this.countElement = shadowRoot.querySelector("[data-count]");
      shadowRoot.querySelector("[data-increment]")
        .addEventListener("click", this.handleIncrementClickEvent.bind(this));
    }

    render(counter) {
      this.countElement.textContent = String(counter.count ?? 0);
    }

    handleIncrementClickEvent(event) {
      event.preventDefault();
      this.dispatchEvent(new CustomEvent("counter-increment", {
        bubbles: true,
        composed: true
      }));
    }
  }

  // Coordination Controller
  class MultiViewController {
    constructor() {
      this.counter = { count: 0 };
      this.views = [];
      this.eventListenerCount = 0;
      this.updateUI();
    }

    addView() {
      const view = document.createElement('multi-counter-view');
      view.render(this.counter);

      const handleIncrement = () => {
        this.counter.count++;
        this.renderAllViews();
        this.updateUI();
      };

      view.addEventListener('counter-increment', handleIncrement);
      view._handleIncrement = handleIncrement; // Store reference for removal

      this.views.push(view);
      this.eventListenerCount++;

      document.getElementById('views-container').appendChild(view);
      this.updateUI();
    }

    removeView() {
      if (this.views.length === 0) return;

      const view = this.views.pop();
      view.removeEventListener('counter-increment', view._handleIncrement);
      view.remove();
      this.eventListenerCount--;
      this.updateUI();
    }

    renderAllViews() {
      this.views.forEach(view => view.render(this.counter));
      document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
    }

    resetModel() {
      this.counter.count = 0;
      this.renderAllViews();
      this.updateUI();
    }

    updateUI() {
      document.getElementById('shared-count').textContent = this.counter.count;
      document.getElementById('active-views-count').textContent = this.views.length;
      document.getElementById('total-listeners').textContent = this.eventListenerCount;
    }
  }

  // Boilerplate Evolution Demo
  const codeExamples = {
    vanilla: `class CounterView extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById("template");
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.content.cloneNode(true));

    this.countElement = shadowRoot.querySelector("[data-count]");
    shadowRoot.querySelector("[data-increment]")
      .addEventListener("click", this.handleClick.bind(this));
  }

  render(counter) {
    this.countElement.textContent = String(counter.count ?? 0);
  }

  handleClick(event) {
    event.preventDefault();
    this.dispatchEvent(new CustomEvent("counter-increment", {
      bubbles: true, composed: true
    }));
  }
}`,

    helper: `class CounterView extends BoundTemplateHTMLElement {
  constructor() {
    super("counter-view-template");

    this.refs.increment.addEventListener(
      "click", this.handleIncrementClickEvent.bind(this));
  }

  render(counter) {
    this.refs.count.textContent = String(counter.count ?? 0);
  }

  handleIncrementClickEvent(event) {
    event.preventDefault();
    this.emit("counter-increment");
  }
}`,

    lit: `class CounterView extends LitElement {
  static properties = {
    count: { type: Number }
  };

  constructor() {
    super();
    this.count = 0;
  }

  render() {
    return html\`
      <span class="root">
        Count: <span class="count">\${this.count}</span>
        <button @click=\${this.handleIncrement}>+</button>
      </span>
    \`;
  }

  handleIncrement() {
    this.dispatchEvent(new CustomEvent('counter-increment'));
  }
}`
  };


  // Custom elements for evolution demo
  class VanillaCounterDemo extends HTMLElement {
    constructor() {
      super();
      this.innerHTML = '<div style="color: blue; border: 1px solid blue; padding: 5px; font-family: monospace;">Count: <span>0</span> <button onclick="this.parentElement.querySelector(\'span\').textContent = parseInt(this.parentElement.querySelector(\'span\').textContent) + 1">+</button></div>';
    }
  }

  class HelperCounterDemo extends HTMLElement {
    constructor() {
      super();
      this.innerHTML = '<div style="color: blue; border: 1px solid blue; padding: 5px; font-family: monospace;">Count: <span>0</span> <button onclick="this.parentElement.querySelector(\'span\').textContent = parseInt(this.parentElement.querySelector(\'span\').textContent) + 1">+</button> <small>(with helpers)</small></div>';
    }
  }

  class LitCounterDemo extends HTMLElement {
    constructor() {
      super();
      this.innerHTML = '<div style="color: blue; border: 1px solid blue; padding: 5px; font-family: monospace;">Count: <span>0</span> <button onclick="this.parentElement.querySelector(\'span\').textContent = parseInt(this.parentElement.querySelector(\'span\').textContent) + 1">+</button> <small>(LitElement style)</small></div>';
    }
  }

  // Initialize all demos
  let multiController;

  document.addEventListener('DOMContentLoaded', () => {
    // Register custom elements
    if (!customElements.get('multi-counter-view')) {
      customElements.define('multi-counter-view', MultiCounterView);
    }
    if (!customElements.get('vanilla-counter-demo')) {
      customElements.define('vanilla-counter-demo', VanillaCounterDemo);
    }
    if (!customElements.get('helper-counter-demo')) {
      customElements.define('helper-counter-demo', HelperCounterDemo);
    }
    if (!customElements.get('lit-counter-demo')) {
      customElements.define('lit-counter-demo', LitCounterDemo);
    }

    // Multi-view controller demo
    multiController = new MultiViewController();

    document.getElementById('add-view').addEventListener('click', () => {
      multiController.addView();
    });

    document.getElementById('remove-view').addEventListener('click', () => {
      multiController.removeView();
    });

    document.getElementById('reset-model').addEventListener('click', () => {
      multiController.resetModel();
    });

    // Boilerplate evolution demo
    Object.keys(codeExamples).forEach(key => {
      const codeEl = document.getElementById(`${key}-code`);
      if (codeEl) {
        codeEl.textContent = codeExamples[key];
      }
    });

    // Tab switching
    document.querySelectorAll('.tab-button').forEach(button => {
      button.addEventListener('click', (e) => {
        const targetTab = e.target.dataset.tab;

        // Update button states
        document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
        e.target.classList.add('active');

        // Update content
        document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
        document.getElementById(`${targetTab}-tab`).classList.add('active');
      });
    });


    // Add some initial views to the controller demo
    multiController.addView();
    multiController.addView();
  });

})();
</script>

<p>In a future blog post, I’d love to provide an example of a simple TODO app with server-side communications.</p>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[A proposal for MVC on the client, instead of React-like frameworks.]]></summary></entry><entry><title type="html">Designing applications around AI development agents</title><link href="https://bonaroo.nl/2025/05/20/enforced-ai-test-driven-development.html" rel="alternate" type="text/html" title="Designing applications around AI development agents" /><published>2025-05-20T00:00:00+00:00</published><updated>2025-05-20T00:00:00+00:00</updated><id>https://bonaroo.nl/2025/05/20/enforced-ai-test-driven-development</id><content type="html" xml:base="https://bonaroo.nl/2025/05/20/enforced-ai-test-driven-development.html"><![CDATA[<h1 id="designing-applications-around-ai-development-agents">Designing applications around AI development agents</h1>

<p>Asking ChatGPT to write a Vertex2D library will get you a complete, (likely) functional library in 30 seconds. AI is really good at writing libraries around existing problems that fit in context.</p>

<p>Easily accessible models (Examples: Claude 3.7, Gemini 2.5) these days have a context size of around a million tokens. As of writing this post, all blog posts we’ve written will fit in about 250K tokens, so that leaves us with a lot of tokens remaining to ask questions, discuss the blog posts, and much more. Modern AI models are big enough to be able to load complete applications into context, if the application is small enough.</p>

<p>I believe that a future in AI-written applications lies in creating applications consisting of many small modules, where each module is small enough to fit in the context of an AI agent, together with all the tests, documentation and requirements for that module.</p>

<p>How do you make applications small enough? Easy: Split them up. Delegate implementation details to dependencies. These dependencies? AI can write them for you.</p>

<h2 id="ai-agents-a-new-employee-for-every-task">AI Agents: A new employee for every task</h2>

<p>Querying a large language model is basically like asking an experienced developer a question on his first day. Sure, he knows a lot of things, but he knows <em>nothing</em> about your application, business, conventions or expectations. You’ll have to tell them, every time.</p>

<p>Imagine you have a new employee and the only way to communicate with him is via email. To have him perform a task, you send him all relevant documentation and instruct the employee what to do.</p>

<p>Without thinking about it, the employee reads all the documentation and attempts to solve the problem right away.</p>

<p>To have this employee succeed, you’ll have to send pretty detailed instructions, and ideally you also give this employee access to anything it needs.</p>

<p>After the task is completed, the employee leaves. If you have follow-up questions or tasks, a new employee is assigned. This new employee knows nothing and needs to read the previous conversation to attempt to solve your problem.</p>

<p>These employees are your AI agents. The instructions you provide to these employees is called a <em>prompt</em>. You need a pretty detailed prompt to give these employees a proper chance to produce something you need, but you also need to provide no more than required or they’ll hallucinate, make stuff up, or fix unrelated things.</p>

<p>When these AI agents are tasked with anything non-trivial, you want the AI agents to be able to debug their code, and only their code. You want the AI to be able to write tests and run the test suite with only their code.</p>

<p>Dependencies need to be incredibly simple or need to be mocked predictably. If a test fails, it must be because the AI agent screwed up his own code, not because a dependency is failing.</p>

<h2 id="testable-modules">Testable modules</h2>

<p>We want to split our application into smaller modules, and ideally you can develop and test these modules independently from each other. Designing composable modules has always been a good idea, but it has been too easy to just create a “big ball of mud” where everything relies on everything else.</p>

<p>I think we will need to define very strong boundaries, almost like you’re designing micro-services, but without the actual micro-services. Instead of literally hosting many slow, inter-communicating HTTP servers, I imagine we can just create a framework that allows you to define boundaries. These boundary definitions can then be used to validate inputs and outputs of modules and isolate errors thrown by a module.</p>

<p>That way, calls between modules can easily be mocked in tests, and it is easier to identify which module is misbehaving when something goes wrong. By validating the boundaries and catching errors around boundaries, we can quickly identify a lot of problems early on, directly at the source.</p>

<p>To aid with validating and enforcing boundaries, I think it’s helpful to strongly prefer functional-style functions, only accepting and returning primitive values, or objects composed of primitive values. These value objects should be clonable and replacable by literals: ideally they’re JSON-serializable.</p>

<p>Let’s start with an example, imagine the structure of a blog-like website.</p>

<h2 id="the-blog-structure">The Blog Structure</h2>

<p>A blog website can have modules like this:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SqliteDatabase</code>: A database connection, taking SQL queries and returning the results</li>
  <li><code class="language-plaintext highlighter-rouge">PostsRepo</code>: A module responsible for loading and saving posts on persistent storage, like a database. Can be implemented using a <code class="language-plaintext highlighter-rouge">SqlitePostsRepo</code> (using <code class="language-plaintext highlighter-rouge">SqliteDatabase</code>), <code class="language-plaintext highlighter-rouge">FilePostsRepo</code> saving the posts in JSON files or <code class="language-plaintext highlighter-rouge">MemoryPostsRepo</code> keeping them in-memory. (useful for testing?)</li>
  <li><code class="language-plaintext highlighter-rouge">PublicPosts</code>: A module responsible for accessing posts accessible by the public, only returning posts that are published and only returning properties that should be accessible by the public. Uses a <code class="language-plaintext highlighter-rouge">PostsRepo</code> to load posts.</li>
  <li><code class="language-plaintext highlighter-rouge">AdminPosts</code>: A module for managing posts, like creating, editing, publishing and deleting posts. Uses a <code class="language-plaintext highlighter-rouge">PostsRepo</code> to load and save posts.</li>
  <li><code class="language-plaintext highlighter-rouge">PublicPostsRss</code>: A module wrapping <code class="language-plaintext highlighter-rouge">PublicPosts</code>, returning published posts as an RSS feed.</li>
  <li><code class="language-plaintext highlighter-rouge">PublicWeb</code>: A module wrapping <code class="language-plaintext highlighter-rouge">PublicPosts</code>, returning published posts as HTML pages, like a homepage and a detailed post page, and maybe some additional navigational elements like “posts per month”.</li>
  <li><code class="language-plaintext highlighter-rouge">PostsWebComponents</code>: A module rendering <code class="language-plaintext highlighter-rouge">PublicPosts</code> as HTML elements. Can be a react-like thing or just render to plain HTML. Is used by <code class="language-plaintext highlighter-rouge">PublicWeb</code> to render the page. Because styling and designing a webpage can be complicated, I would like this to be a separate module, so we can render the pages with just a bit of dummy data.</li>
  <li><code class="language-plaintext highlighter-rouge">ErrorWebComponents</code>: A module for rendering error pages and components.</li>
</ul>

<h2 id="rendering-posts">Rendering posts</h2>

<p>To render the homepage, we’ll render a list of published posts. This will involve a variety of modules, including:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PostsRepo</code>: A module responsible for loading and saving posts on persistent storage, like a database.</li>
  <li><code class="language-plaintext highlighter-rouge">PublicPosts</code>: A module using a repo to load published posts.</li>
</ul>

<p>The boundary can look like this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicPosts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"schemas"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"PublicPost"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicPost"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="nl">"publishedAt"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ZonedDateTime"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicAuthor"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="nl">"contentHtml"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"getLatestPosts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getLatestPosts"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"page"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="p">,</span><span class="w"> </span><span class="nl">"min"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"output"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"posts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"array"</span><span class="p">,</span><span class="w"> </span><span class="nl">"items"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicPost"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"getPost"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getPost"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"output"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"post"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"oneOf"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicPost"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"null"</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="p">},</span><span class="w">
          </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"oneOf"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"null"</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"runtimeDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"PostsRepo"</span><span class="p">,</span><span class="w"> </span><span class="s2">"PublicRoutes"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>We can define this boundary using <a href="https://zod.dev">zod</a>. Notice how all inputs and outputs are JSON serializable. This <code class="language-plaintext highlighter-rouge">PublicPosts</code> module might be implemented like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getLatestPosts</span><span class="p">(</span><span class="nx">limit</span><span class="p">,</span> <span class="nx">offset</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">PostsRepo</span><span class="p">.</span><span class="nx">findPublishedPostsDescending</span><span class="p">(</span><span class="nx">offset</span><span class="p">,</span> <span class="nx">limit</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">posts</span><span class="p">:</span> <span class="nx">posts</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">post</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">renderPublicPost</span><span class="p">(</span><span class="nx">post</span><span class="p">))</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getPost</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">post</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">PostsRepo</span><span class="p">.</span><span class="nx">findPublishedPostById</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">post</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">not-found</span><span class="dl">"</span> <span class="p">};</span>
  <span class="k">return</span> <span class="p">{</span> <span class="na">post</span><span class="p">:</span> <span class="nx">renderPublicPost</span><span class="p">(</span><span class="nx">post</span><span class="p">)</span> <span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">renderPublicPost</span><span class="p">(</span><span class="nx">post</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">title</span><span class="p">:</span> <span class="nx">post</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
    <span class="na">publishedAt</span><span class="p">:</span> <span class="nx">post</span><span class="p">.</span><span class="nx">publishedAt</span><span class="p">,</span>
    <span class="na">author</span><span class="p">:</span> <span class="nx">renderPublicAuthor</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">author</span><span class="p">),</span>
    <span class="na">contentHtml</span><span class="p">:</span> <span class="nx">markdown2html</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">contentMd</span><span class="p">),</span>
    <span class="na">href</span><span class="p">:</span> <span class="nx">PublicRoutes</span><span class="p">.</span><span class="nx">postPath</span><span class="p">(</span><span class="nx">post</span><span class="p">),</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">renderPublicAuthor</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="na">href</span><span class="p">:</span> <span class="nx">PublicRoutes</span><span class="p">.</span><span class="nx">authorPath</span><span class="p">(</span><span class="nx">user</span><span class="p">),</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now imagine the <code class="language-plaintext highlighter-rouge">PublicWeb</code> module, which depends on <code class="language-plaintext highlighter-rouge">PublicPosts</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PublicWeb"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"schemas"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"getHome"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getHome"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"req"</span><span class="p">,</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HttpRequest"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"output"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HttpResponse"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"getPostDetail"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getPostDetail"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"integer"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"output"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"$ref"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HttpResponse"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"runtimeDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"PublicPosts"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"PostsWebComponents"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"ErrorWebComponents"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getHome</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">page</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">1</span><span class="dl">"</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">limit</span> <span class="o">=</span> <span class="mi">30</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">PublicPosts</span><span class="p">.</span><span class="nx">getLatestPosts</span><span class="p">(</span><span class="nx">limit</span><span class="p">,</span> <span class="p">(</span><span class="nx">page</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="nx">limit</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">res</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="nx">PostsWebComponents</span><span class="p">.</span><span class="nx">renderIndex</span><span class="p">(</span><span class="nx">response</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getPostDetail</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span> <span class="o">||</span> <span class="dl">""</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">PublicPosts</span><span class="p">.</span><span class="nx">getPost</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">error</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">not-found</span><span class="dl">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="nx">res</span><span class="p">(</span><span class="mi">404</span><span class="p">,</span> <span class="nx">ErrorWebComponents</span><span class="p">.</span><span class="nx">renderError</span><span class="p">(</span><span class="dl">"</span><span class="s2">Post not found</span><span class="dl">"</span><span class="p">));</span>
  <span class="k">return</span> <span class="nx">res</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="nx">PostsWebComponents</span><span class="p">.</span><span class="nx">renderDetail</span><span class="p">(</span><span class="nx">response</span><span class="p">));</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">res</span><span class="p">(</span><span class="nx">status</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">status</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html; charset=utf-8</span><span class="dl">"</span> <span class="p">},</span> <span class="nx">body</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In these modules, there’s a clear separation of concerns. Note that a view (<code class="language-plaintext highlighter-rouge">PostsWebComponents.renderIndex</code>) can’t access any data that isn’t previously loaded - no magic auto-loading of relations or unintentional access of properties here.</p>

<p>The <code class="language-plaintext highlighter-rouge">PublicWeb</code> controller can only access <code class="language-plaintext highlighter-rouge">PublicPosts</code>, not <code class="language-plaintext highlighter-rouge">AdminPosts</code>, because only <code class="language-plaintext highlighter-rouge">PublicPosts</code> is listed as a dependency in the schema. Even if <code class="language-plaintext highlighter-rouge">AdminPosts</code> were to be added as a dependency, both the call to <code class="language-plaintext highlighter-rouge">AdminPosts.*</code> and the addition of the <code class="language-plaintext highlighter-rouge">AdminPosts</code> dependency would be clear indicators that a <code class="language-plaintext highlighter-rouge">PublicWeb</code> module suddenly uses a module only meant in modules related to admin access.</p>

<p>In many applications, you can access any module or class from anywhere, even in views. Loading Admin-only sensitive data on the homepage would be just as easy as loading any other data, and you rely on code reviews to catch mistakes or carelessness.</p>

<p>I imagine we will have an agent designing these schemas, and other agents implementing the modules. Because the agent writing the module can’t alter any dependencies, it can automatically not access any data it isn’t supposed to access. If a module agent needs additional dependencies, it should request the schema agent.</p>

<h2 id="module-agents">Module agents</h2>

<p>I don’t intend to give the agents access to a couple of source files directly. Instead, I want to give the module agent access to a virtual file containing an array of functions, tests and references (dependencies, imports) using the initial prompt and a set of tools:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">getFunctionNames()</code>, <code class="language-plaintext highlighter-rouge">getFunction(name)</code>, <code class="language-plaintext highlighter-rouge">setFunction(name, params, body)</code>, <code class="language-plaintext highlighter-rouge">deleteFunction(name)</code>: Create, update or delete a function, uniquely identified by <code class="language-plaintext highlighter-rouge">name</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">getTestNames()</code>, <code class="language-plaintext highlighter-rouge">getTest(description)</code>, <code class="language-plaintext highlighter-rouge">setTest(description, body)</code>, <code class="language-plaintext highlighter-rouge">deleteTest(description)</code>: Create, update or delete a test, uniquely identified by <code class="language-plaintext highlighter-rouge">description</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">getReferences()</code>, <code class="language-plaintext highlighter-rouge">setReference(alias, source)</code>: Request a dependency to be added, uniquely identified by <code class="language-plaintext highlighter-rouge">alias</code>. If the dependency is not present in the schema, the agent (or human) managing the schema will be requested to allow the dependency.</li>
  <li><code class="language-plaintext highlighter-rouge">testAll()</code>: Runs all tests. Includes code coverage.</li>
  <li><code class="language-plaintext highlighter-rouge">setFunctionAndTest(fun, test)</code>: A test-driven-development cycle, combining <code class="language-plaintext highlighter-rouge">setFunction</code> and <code class="language-plaintext highlighter-rouge">setTest</code>. Runs a sequence of steps, aborting if any of the steps fail. Steps: 1. Run all tests, assert they succeed, 2. Add the test, assert it fails, 3. Add the function, assert the new test succeeds, 4. Run all tests, assert they succeed.</li>
</ul>

<p>The agent will be initialized with:</p>

<ul>
  <li>a list of all modules in the application</li>
  <li>a list of all possible dependencies</li>
  <li>full source code and tests of its module</li>
  <li>its current schema</li>
  <li>general documentation about the whole application</li>
  <li>a prompt for code-style</li>
</ul>

<p>You can imagine this initial prompt will be pretty big.</p>

<p>I want to put a lot of emphasis on test-driven-development and getting 100% code coverage.</p>

<h2 id="mocking-dependencies">Mocking dependencies</h2>

<p>In my example, <code class="language-plaintext highlighter-rouge">PublicWeb</code> depends on <code class="language-plaintext highlighter-rouge">PublicPosts</code>, which depends on <code class="language-plaintext highlighter-rouge">PostsRepo</code> which would depend on <code class="language-plaintext highlighter-rouge">SqliteDatabase</code>. We likely want to mock these dependencies at one point.</p>

<p>I think it works best if runtime dependencies are mocked directly. <code class="language-plaintext highlighter-rouge">PublicWeb</code> relies on <code class="language-plaintext highlighter-rouge">PublicPosts</code>, so it should mock <code class="language-plaintext highlighter-rouge">PublicPosts</code>. This makes sure that each module is only concerned about its declared interface, even in tests.</p>

<p>This leaves a strong demand for integration or end-to-end tests. I can imagine to have a separate agent writing integration tests, which doesn’t mock any module except modules that rely on infrastructure (the database, http calls, APIs, etc) and another one for end-to-end tests, which just tests against a running application.</p>

<p>The integration tests will likely mock the database, or run a clean in-memory database, and use real modules to setup a scenario to test with.</p>

<p>For example, an integration test for <code class="language-plaintext highlighter-rouge">PostPosts</code> and <code class="language-plaintext highlighter-rouge">AdminPosts</code> can look like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">publicRoutes</span> <span class="o">=</span> <span class="nx">inject</span><span class="p">(</span><span class="nx">PublicRoutes</span><span class="p">,</span> <span class="p">{</span> <span class="na">host</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:9999/</span><span class="dl">"</span> <span class="p">})</span>
<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">inject</span><span class="p">(</span><span class="nx">SqliteDatabase</span><span class="p">,</span> <span class="p">{</span> <span class="na">databasePath</span><span class="p">:</span> <span class="dl">"</span><span class="s2">:memory:</span><span class="dl">"</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">postsRepo</span> <span class="o">=</span> <span class="nx">inject</span><span class="p">(</span><span class="nx">PostsRepo</span><span class="p">,</span> <span class="p">{</span> <span class="na">SqliteDatabase</span><span class="p">:</span> <span class="nx">db</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">adminPosts</span> <span class="o">=</span> <span class="nx">inject</span><span class="p">(</span><span class="nx">AdminPosts</span><span class="p">,</span> <span class="p">{</span> <span class="na">PostsRepo</span><span class="p">:</span> <span class="nx">postsRepo</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">publicPosts</span> <span class="o">=</span> <span class="nx">inject</span><span class="p">(</span><span class="nx">PublicPosts</span><span class="p">,</span>
  <span class="p">{</span> <span class="na">PostsRepo</span><span class="p">:</span> <span class="nx">postsRepo</span><span class="p">,</span> <span class="na">PublicRoutes</span><span class="p">:</span> <span class="nx">publicRoutes</span> <span class="p">})</span>

<span class="kd">const</span> <span class="nx">createdPost</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">adminPosts</span><span class="p">.</span><span class="nx">createPost</span><span class="p">({</span>
  <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hello</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">contentMd</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hello, **world**!</span><span class="dl">"</span>
<span class="p">});</span>
<span class="k">await</span> <span class="nx">adminPosts</span><span class="p">.</span><span class="nx">publishPost</span><span class="p">(</span><span class="nx">createdPost</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">publishedPost</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">publicPosts</span><span class="p">.</span><span class="nx">getPost</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">publishedPost</span><span class="p">).</span><span class="nx">toEqual</span><span class="p">({</span>
  <span class="na">href</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:9999/posts/1337-hello</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hello</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">publishedAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2025-12-25T12:00:00+01:00[Europe/Amsterdam]</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">contentHtml</span><span class="p">:</span> <span class="s2">`&lt;p&gt;Hello, &lt;strong&gt;world&lt;/strong&gt;!&lt;/p&gt;`</span><span class="p">,</span>
  <span class="na">author</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">user</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Toby</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">href</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:9999/users/422-toby</span><span class="dl">"</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="schema-changes--refactoring-across-boundaries">Schema changes &amp; refactoring across boundaries</h2>

<p>For graceful refactoring &amp; schema changes, I’m thinking to add the possibility to have boundary migrations where you can define a set of functions to transform between old and new versions of the boundary. For example, let’s imagine we’re changing the <code class="language-plaintext highlighter-rouge">name</code> field of the <code class="language-plaintext highlighter-rouge">PublicAuthor</code> to be <code class="language-plaintext highlighter-rouge">givenName</code> and <code class="language-plaintext highlighter-rouge">familyName</code>. A transform might be defined like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PublicAuthorNameTransform</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">target</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PublicAuthor</span><span class="dl">"</span><span class="p">,</span>
  <span class="k">from</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">name</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">},</span>
  <span class="nx">to</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">givenName</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
    <span class="nx">familyName</span><span class="p">:</span> <span class="p">{</span> <span class="nl">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">},</span>
  <span class="nx">transform</span><span class="p">(</span><span class="nx">author</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="p">...</span><span class="nx">author</span><span class="p">,</span>
      <span class="na">givenName</span><span class="p">:</span> <span class="nx">author</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
      <span class="na">familyName</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This way, we can have focus on our new module with a new version of the schema without breaking any other modules.</p>

<p>Once the old version of the schema has been removed, the transform can be automatically deleted. The framework could automatically detect any schema mismatches and automatically detect an appropriate schema transform, and also automatically delete any transform that is no longer required.</p>

<p>Notice how the transform is only concerned with relevant properties. That way, multiple versions of the same schema can exist at the same time, allowing for gracefully migrations between them.</p>

<h2 id="implementation">Implementation</h2>

<p>There are a lot of tools today that take on autonomous bugfixing and software development, but these are mostly general purpose tools you can drop into an existing codebase.</p>

<p>I’m currently working on creating the tools to implement this (not open source, at this moment)</p>

<p>I imagine by designing the infrastructure, prompts and the framework together, we could get better, faster and/or cheaper results. Or it will be a huge, impractical mess that serves no purpose for real-world applications. We’ll see!</p>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[AI is really good in writing small modules. Can we design applications as a network of small modules?]]></summary></entry><entry><title type="html">User signup &amp;amp; login without dependencies</title><link href="https://bonaroo.nl/2025/01/10/user-signup-and-login.html" rel="alternate" type="text/html" title="User signup &amp;amp; login without dependencies" /><published>2025-01-10T00:00:00+00:00</published><updated>2025-01-10T00:00:00+00:00</updated><id>https://bonaroo.nl/2025/01/10/user-signup-and-login</id><content type="html" xml:base="https://bonaroo.nl/2025/01/10/user-signup-and-login.html"><![CDATA[<h1 id="user-signup--login">User Signup &amp; Login</h1>

<blockquote>
  <p>This blog is a part 3 of my bluebird blog. <a href="https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies.html">See part 1</a> and <a href="https://bonaroo.nl/2025/01/09/the-template-language.html">part 2</a></p>
</blockquote>

<p>I don’t think there’s an easy way to send email without dependencies. I suppose if I really wanted email, I can use some email-as-a-service API using NodeJS’ HTTP client. However, the email-as-a-service could be considered a dependency, so I won’t.</p>

<p>Traditionally, if the user forgets their password, they can get a password reset link via email. Without email, this obviously isn’t possible. For bluebird, my twitter-clone, forgetting your password results in losing access to your account.</p>

<p>Let’s create a signup and login process. Before I start writing HTML, I want to implement the authentication (signup + login) process in a module for easy testing.</p>

<p>The authentication process will be handled by a class named <code class="language-plaintext highlighter-rouge">Auth</code> and will feature at least 2 methods: <code class="language-plaintext highlighter-rouge">login</code> and <code class="language-plaintext highlighter-rouge">signup</code>.</p>

<h2 id="signup">Signup</h2>

<p>The signup method needs to create a user if it doesn’t already exists. The created user should be returned. If the user does exist, some kind of error is returned.</p>

<p>For a method to return either an error or a user, I’m going to make the function method return an object with either an <code class="language-plaintext highlighter-rouge">error</code> property for failure, or a <code class="language-plaintext highlighter-rouge">user</code> property for success.</p>

<p>Following the Red-Green-Refactor rule, I’m writing a failing test first, then write a minimal implementation to make it pass.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/auth.test.mjs</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">describe</span><span class="p">,</span> <span class="nx">it</span><span class="p">,</span> <span class="nx">beforeEach</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:test</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Auth</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../src/auth.mjs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:assert</span><span class="dl">"</span><span class="p">;</span>

<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">Auth</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">returns created user</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">auth</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Auth</span><span class="p">();</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">assert</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// src/auth.mjs</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">Auth</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">signup</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">user</span><span class="p">:</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now to check for existing users. Note how I use JSDoc for typing. JSDoc is completely ignored by NodeJS, but it does help in some editors when the type of a variable isn’t obvious.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/auth.test.mjs (partial)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="cm">/** @type {Auth} */</span>
  <span class="kd">let</span> <span class="nx">auth</span><span class="p">;</span>

  <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">auth</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Auth</span><span class="p">();</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">returns created user</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">returns user-exists error if user already exists</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>

    <span class="kd">const</span> <span class="p">{</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_EXISTS</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// src/auth.mjs</span>
<span class="cm">/**
 * @typedef {Object} User
 * @property {string} name
 */</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">Auth</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">USER_EXISTS</span> <span class="o">=</span> <span class="nb">Symbol</span><span class="p">(</span><span class="dl">"</span><span class="s2">USER_EXISTS</span><span class="dl">"</span><span class="p">);</span>

  <span class="cm">/** @type {Map&lt;string, User&gt;} */</span>
  <span class="err">#</span><span class="nx">users</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>

  <span class="cm">/**
   *
   * @param {string} name
   * @param {string} pass
   * @returns {Promise&lt;{ user: User }|{ error: symbol }&gt;}
   */</span>
  <span class="k">async</span> <span class="nx">signup</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_EXISTS</span> <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">};</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Using the current implementation, I’m storing the created users in a variable. Later, I’ll find a way to persist them in some kind of database.</p>

<p>Also note how I haven’t implemented any password hashing. There is no way to test whether a password is stored or hashed properly without writing an additional method to verify the password, so that’s what I’m going to do now using the login-method.</p>

<h2 id="login">Login</h2>

<p>I continue writing tests.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/auth.test.mjs (partial)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">login</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">for unknown user, return USER_NOT_FOUND</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_NOT_FOUND</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// src/auth.mjs (partial)</span>
<span class="kd">class</span> <span class="nx">Auth</span> <span class="p">{</span>
  <span class="cm">/**
   *
   * @param {string} name
   * @param {string} pass
   * @returns {Promise&lt;{ user: User }|{ error: symbol }&gt;}
   */</span>
  <span class="k">async</span> <span class="nx">login</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_NOT_FOUND</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><a href="https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Authentication_Cheat_Sheet.md#authentication-and-error-messages">OWASP</a> considers it a security risk to let the user know whether a username exists or not. They suggest to respond with a generic error message regardless of wether the password was incorrect, the account doesn’t exist or the account is disabled.</p>

<p>However, in many implementations, there’s still other ways for any user to find whether a username exists or not. Most notably, the signup screen generally shows you a user with that username already exists, and password recovery often also returns some kind of error indicating whether the email address is known or not.</p>

<p>OWASP suggests the signup page and account recovery to not report whether a user exists or not and instead send an email to the user. For this project, I don’t have email available, so that’s not going to work.</p>

<p>Additionally, I don’t think the security issue is worth the hit on usability. I’m perfectly comfortable with rendering useful errors to users at the cost of this potential security risk. Let’s continue!</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/auth.test.mjs (partial)</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">for known user but invalid password, return INVALID_PASSWORD</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">invalid</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">INVALID_PASSWORD</span><span class="p">);</span>
<span class="p">});</span>

<span class="c1">// src/auth.mjs (partial)</span>
<span class="kd">class</span> <span class="nx">User</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">login</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_NOT_FOUND</span> <span class="p">};</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">INVALID_PASSWORD</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// test/auth.test.mjs (partial)</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">for known user and valid password, return user</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>What’s the minimal implementation to check a password? Yep: plain text!</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">User</span> <span class="p">{</span>
    <span class="k">async</span> <span class="nx">signup</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_EXISTS</span> <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span> <span class="p">};</span> <span class="c1">// &lt;-- Don't do this!</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">login</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_NOT_FOUND</span> <span class="p">};</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">pass</span> <span class="o">!=</span> <span class="nx">pass</span><span class="p">)</span> <span class="c1">// &lt;-- Don't do this!</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">INVALID_PASSWORD</span> <span class="p">};</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All tests pass. Now to find a way to make it not pass. First of all, I never want the password exposed. There’s no perfect way to test whether the password is exposed by the returned user object, but a very minimal, naive approach would be to check none of the returned properties has the password as a value.</p>

<p>I’m going to reuse the last test I wrote for that purpose, iterating all properties of the returned object and checking for the password.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">for known user and valid password, return user</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">secret</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">secret</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">k</span> <span class="k">in</span> <span class="nx">user</span><span class="p">)</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">notEqual</span><span class="p">(</span><span class="nx">user</span><span class="p">[</span><span class="nx">k</span><span class="p">],</span> <span class="dl">"</span><span class="s2">secret</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>To hide the password, I’m going to hash the password in some way. The last time I hashed passwords myself, I used a single round of <code class="language-plaintext highlighter-rouge">md5</code>. This was a long time ago. After that, I only used <a href="https://www.npmjs.com/package/bcrypt">bcrypt</a>.</p>

<p>But that’s a dependency. NodeJS seems to offer <a href="https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback">scrypt</a> which is basically the same as bcrypt and only differs in one letter.</p>

<p>Unlike the bcrypt implementations I’m used to, scrypt requires you to bring your own salt. Scrypt seems to accept all kinds of parameters and options, so I’m going to wrap scrypt in my own hashing module which basically has a method for hashing a password and comparing a hash with a password.</p>

<p>I’m going to create a <code class="language-plaintext highlighter-rouge">verify</code> and <code class="language-plaintext highlighter-rouge">hash</code> method in the <code class="language-plaintext highlighter-rouge">Pass</code> class and their behaviour is pretty simple.</p>

<h4 id="testpasstestmjs">test/pass.test.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">describe</span><span class="p">,</span> <span class="nx">it</span><span class="p">,</span> <span class="nx">beforeEach</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:test</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Pass</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../src/pass.mjs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:assert</span><span class="dl">"</span><span class="p">;</span>

<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">Pass</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="cm">/** @type {Pass} */</span>
  <span class="kd">let</span> <span class="nx">pass</span><span class="p">;</span>

  <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">pass</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pass</span><span class="p">({</span> <span class="na">cost</span><span class="p">:</span> <span class="mi">2</span> <span class="p">});</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">hash creates a password hash</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-password</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">typeof</span> <span class="nx">hash</span><span class="p">,</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">Pass.verify returns true for the correct password</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-password</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">await</span> <span class="nx">Pass</span><span class="p">.</span><span class="nx">verify</span><span class="p">(</span><span class="nx">hash</span><span class="p">,</span> <span class="dl">"</span><span class="s2">my-password</span><span class="dl">"</span><span class="p">),</span> <span class="kc">true</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">Pass.verify returns false for any other password</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-password</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="k">await</span> <span class="nx">Pass</span><span class="p">.</span><span class="nx">verify</span><span class="p">(</span><span class="nx">hash</span><span class="p">,</span> <span class="dl">"</span><span class="s2">wrong-password</span><span class="dl">"</span><span class="p">),</span> <span class="kc">false</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<h4 id="srcpassmjs">src/pass.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">crypto</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:crypto</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">promisify</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:util</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">scrypt</span> <span class="o">=</span> <span class="nx">promisify</span><span class="p">(</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">scrypt</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">randomBytes</span> <span class="o">=</span> <span class="nx">promisify</span><span class="p">(</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">randomBytes</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">pattern</span> <span class="o">=</span> <span class="sr">/^</span><span class="se">\$</span><span class="sr">s0</span><span class="se">\$(?&lt;</span><span class="sr">cost&gt;</span><span class="se">\d</span><span class="sr">+</span><span class="se">)\$(?&lt;</span><span class="sr">keylen&gt;</span><span class="se">\d</span><span class="sr">+</span><span class="se">)\$(?&lt;</span><span class="sr">salt&gt;</span><span class="se">[^</span><span class="sr">$</span><span class="se">]</span><span class="sr">+</span><span class="se">)\$(?&lt;</span><span class="sr">key&gt;</span><span class="se">[^</span><span class="sr">$</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">$/</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">Pass</span> <span class="p">{</span>
  <span class="err">#</span><span class="nx">keylen</span> <span class="o">=</span> <span class="mi">60</span><span class="p">;</span>
  <span class="err">#</span><span class="nx">cost</span> <span class="o">=</span> <span class="mi">16384</span><span class="p">;</span>

  <span class="kd">constructor</span><span class="p">({</span> <span class="nx">keylen</span><span class="p">,</span> <span class="nx">cost</span> <span class="p">}</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">keylen</span> <span class="o">=</span> <span class="nx">keylen</span> <span class="o">??</span> <span class="mi">60</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">cost</span> <span class="o">=</span> <span class="nx">cost</span> <span class="o">??</span> <span class="mi">16384</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="cm">/**
   *
   * @param {string} password
   * @returns {Promise&lt;string&gt;}
   */</span>
  <span class="k">async</span> <span class="nx">hash</span><span class="p">(</span><span class="nx">password</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">Pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="k">await</span> <span class="nx">randomBytes</span><span class="p">(</span><span class="mi">24</span><span class="p">),</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">keylen</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">cost</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="cm">/**
   *
   * @param {string} hash
   * @param {string} password
   * @returns {Promise&lt;boolean&gt;}
   *
   */</span>
  <span class="kd">static</span> <span class="k">async</span> <span class="nx">verify</span><span class="p">(</span><span class="nx">hash</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">hash</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">pattern</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">result</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Pass.verify was given an invalid string as a hash`</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">cost</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">groups</span><span class="p">.</span><span class="nx">cost</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">keylen</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">groups</span><span class="p">.</span><span class="nx">keylen</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">groups</span><span class="p">.</span><span class="nx">salt</span><span class="p">,</span> <span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">expectedHash</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="nx">keylen</span><span class="p">,</span> <span class="nx">cost</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">expectedHash</span> <span class="o">===</span> <span class="nx">hash</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="cm">/**
   *
   * @param {string} password
   * @param {Buffer} salt
   * @param {number} keylen
   * @param {number} cost
   */</span>
  <span class="kd">static</span> <span class="k">async</span> <span class="nx">hash</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="nx">keylen</span><span class="p">,</span> <span class="nx">cost</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scrypt</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="nx">keylen</span><span class="p">,</span> <span class="p">{</span> <span class="nx">cost</span> <span class="p">});</span>
    <span class="k">return</span> <span class="s2">`$s0$</span><span class="p">${</span><span class="nx">cost</span><span class="p">}</span><span class="s2">$</span><span class="p">${</span><span class="nx">keylen</span><span class="p">}</span><span class="s2">$</span><span class="p">${</span><span class="nx">salt</span><span class="p">.</span><span class="nx">toString</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">)}</span><span class="s2">$</span><span class="p">${</span><span class="nx">key</span><span class="p">.</span><span class="nx">toString</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">)}</span><span class="s2">`</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I intentionally made <code class="language-plaintext highlighter-rouge">Pass.verify</code> a static method to make sure it doesn’t rely on the config of a <code class="language-plaintext highlighter-rouge">Pass</code>-instance. That way, the tests automatically prove that a generated hash can be verified regardless the settings used at creation,
since there is no way to access instance variables from a static method.</p>

<p>I put all required parameters inside the generated password hash string, like <code class="language-plaintext highlighter-rouge">bcrypt</code> does. That way, I can safely change the parameters like cost, salt size or keylen in future releases without breaking existing password hashes.</p>

<p>Why <code class="language-plaintext highlighter-rouge">$s0$</code>? I don’t know, I liked it. Bcrypt uses a similar prefix for versioning, so I thought I use it as well: <code class="language-plaintext highlighter-rouge">s</code> for <code class="language-plaintext highlighter-rouge">scrypt</code> and <code class="language-plaintext highlighter-rouge">0</code> for version <code class="language-plaintext highlighter-rouge">0</code>.</p>

<p>For testing, I’m going to set the <code class="language-plaintext highlighter-rouge">cost</code> very low, but for real applications I would put the cost on a value to ensure password hashing takes about a second.</p>

<p>Back to the <code class="language-plaintext highlighter-rouge">Auth</code>-class. It’s going to need an instance of <code class="language-plaintext highlighter-rouge">Pass</code> for hashing passwords.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/auth.test.mjs (partial)</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">auth</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Auth</span><span class="p">({</span>
    <span class="na">pass</span><span class="p">:</span> <span class="k">new</span> <span class="nx">Pass</span><span class="p">({</span> <span class="na">cost</span><span class="p">:</span> <span class="mi">2</span> <span class="p">})</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// src/auth.mjs</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Pass</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./pass.mjs</span><span class="dl">"</span><span class="p">;</span>

<span class="cm">/**
 * @typedef {Object} User
 * @property {string} name
 * @property {string} passHash
 */</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">Auth</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">INVALID_PASSWORD</span> <span class="o">=</span> <span class="nb">Symbol</span><span class="p">(</span><span class="dl">"</span><span class="s2">INVALID_PASSWORD</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">static</span> <span class="nx">USER_EXISTS</span> <span class="o">=</span> <span class="nb">Symbol</span><span class="p">(</span><span class="dl">"</span><span class="s2">USER_EXISTS</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">static</span> <span class="nx">USER_NOT_FOUND</span> <span class="o">=</span> <span class="nb">Symbol</span><span class="p">(</span><span class="dl">"</span><span class="s2">USER_NOT_FOUND</span><span class="dl">"</span><span class="p">);</span>

  <span class="cm">/** @type {Map&lt;string, User&gt;} */</span>
  <span class="err">#</span><span class="nx">users</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>

  <span class="cm">/** @type {Pass} */</span>
  <span class="err">#</span><span class="nx">pass</span><span class="p">;</span>

  <span class="cm">/** @param  opts */</span>
  <span class="kd">constructor</span><span class="p">({</span> <span class="nx">pass</span> <span class="p">})</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">pass</span> <span class="o">=</span> <span class="nx">pass</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">signup</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_EXISTS</span> <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="na">passHash</span><span class="p">:</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">pass</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">pass</span><span class="p">)</span> <span class="p">};</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">login</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">pass</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">users</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">USER_NOT_FOUND</span> <span class="p">};</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">await</span> <span class="nx">Pass</span><span class="p">.</span><span class="nx">verify</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">passHash</span><span class="p">,</span> <span class="nx">pass</span><span class="p">))</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">error</span><span class="p">:</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">INVALID_PASSWORD</span> <span class="p">};</span>
    <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All tests now pass and the modules are ready for signup and login. Let’s create the <code class="language-plaintext highlighter-rouge">login</code> and <code class="language-plaintext highlighter-rouge">signup</code> routes in <code class="language-plaintext highlighter-rouge">App</code>.</p>

<h2 id="app-controllers--views">App: Controllers &amp; Views</h2>

<p>Our app already includes routes for login, specifically:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/app.mjs (partial)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">App</span> <span class="p">{</span>
  <span class="k">async</span> <span class="dl">"</span><span class="s2">GET /login</span><span class="dl">"</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">errorCode</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">""</span> <span class="p">}</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="dl">"</span><span class="s2">POST /login</span><span class="dl">"</span><span class="p">({</span> <span class="nx">body</span> <span class="p">})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span>
      <span class="k">return</span> <span class="nx">redirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span> <span class="p">});</span>

    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">errorCode</span><span class="p">:</span> <span class="nx">error</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span>
      <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">username</span> <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>These routes were part of my <a href="https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies.html">previous blog</a>.</p>

<p>The signup routes are just as simple. The <code class="language-plaintext highlighter-rouge">GET /signup</code> will just render a template with default params, and the <code class="language-plaintext highlighter-rouge">POST /signup</code> will delegate the params to the model (the <code class="language-plaintext highlighter-rouge">Auth</code>-module)</p>

<p>But before I write those, I’m going to write some failing tests first!</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app.test.mjs (partial)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">App</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">GET /signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">renders signup template</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">GET</span><span class="p">(</span><span class="dl">"</span><span class="s2">/signup</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">assertRender</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup.ejs</span><span class="dl">"</span><span class="p">,</span>
        <span class="p">{</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">""</span> <span class="p">},</span> <span class="na">errorCode</span><span class="p">:</span> <span class="kc">null</span> <span class="p">});</span>
    <span class="p">});</span>
  <span class="p">});</span>

  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST /signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">for existing user, return a USER_EXISTS error</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">POST</span><span class="p">(</span><span class="dl">"</span><span class="s2">/signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span> <span class="p">});</span>

      <span class="nx">assertRender</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">errorCode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">USER_EXISTS</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">});</span>
    <span class="p">});</span>

    <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">with valid params, sets cookie and redirects to profile</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">POST</span><span class="p">(</span><span class="dl">"</span><span class="s2">/signup</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span> <span class="p">});</span>
      <span class="nx">assertRedirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span> <span class="p">}</span> <span class="p">});</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p>These tests are very similar to the tests for login. The implementation is also very similar. So similar, I might as well copy/paste them.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/app.mjs (partial)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">App</span> <span class="p">{</span>
  <span class="k">async</span> <span class="dl">"</span><span class="s2">GET /signup</span><span class="dl">"</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">errorCode</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">""</span> <span class="p">}</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="dl">"</span><span class="s2">POST /signup</span><span class="dl">"</span><span class="p">({</span> <span class="nx">body</span> <span class="p">})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span>
      <span class="k">return</span> <span class="nx">redirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span> <span class="p">});</span>

    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">signup.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">errorCode</span><span class="p">:</span> <span class="nx">error</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span>
      <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">username</span> <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The template is just as interesting:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- signup.ejs --&gt;</span>
<span class="nt">&lt;h1&gt;</span>Signup!<span class="nt">&lt;/h1&gt;</span>

<span class="nt">&lt;form</span> <span class="na">method=</span><span class="s">"POST"</span> <span class="na">action=</span><span class="s">"/signup"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;</span><span class="err">%</span> <span class="na">if</span> <span class="err">(</span><span class="na">errorCode</span><span class="err">)</span> <span class="err">{</span> <span class="err">%</span><span class="nt">&gt;</span>
    <span class="nt">&lt;p&gt;&lt;</span><span class="err">%=</span> <span class="err">{</span>
      <span class="err">"</span><span class="na">USER_EXISTS</span><span class="err">"</span><span class="na">:</span> <span class="err">"</span><span class="na">User</span> <span class="na">exists</span><span class="err">"</span>
    <span class="err">}[</span><span class="na">errorCode</span><span class="err">]</span> <span class="err">??</span> <span class="na">errorCode</span> <span class="err">%</span><span class="nt">&gt;&lt;/p&gt;</span>
  <span class="nt">&lt;</span><span class="err">%</span> <span class="err">}</span> <span class="err">%</span><span class="nt">&gt;</span>

  <span class="nt">&lt;label&gt;</span>
    <span class="nt">&lt;strong&gt;</span>Username<span class="nt">&lt;/strong&gt;&lt;br&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span> <span class="na">value=</span><span class="s">"&lt;%= params.username %&gt;"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;/label&gt;&lt;br&gt;</span>

  <span class="nt">&lt;label&gt;</span>
    <span class="nt">&lt;strong&gt;</span>Password<span class="nt">&lt;/strong&gt;&lt;br&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;/label&gt;&lt;br&gt;</span>

  <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>Signup<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/form&gt;</span>
</code></pre></div></div>

<p>This completes a very basic signup feature. However, the <code class="language-plaintext highlighter-rouge">/profile</code> page does not yet exist. To complete this blog, I’m going to implement that page!</p>

<h3 id="the-profile-page">The Profile Page</h3>

<p>I intent to make cookies readable like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/app.mjs (partial)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">App</span> <span class="p">{</span>
  <span class="dl">"</span><span class="s2">GET /profile</span><span class="dl">"</span><span class="p">({</span> <span class="nx">cookies</span> <span class="p">})</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">profile.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">cookies</span><span class="p">.</span><span class="nx">username</span> <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- templates/profile.ejs --&gt;</span>
<span class="nt">&lt;h1&gt;</span>Hello, <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">name</span> <span class="err">%</span><span class="nt">&gt;</span>!<span class="nt">&lt;/h1&gt;</span>
</code></pre></div></div>

<p>This route and template serve as a placeholder while I implement the handling of cookies. Let’s first implement that any cookie gets passed to the request handler.</p>

<h3 id="insecure-cookies">Insecure cookies</h3>

<p>Let’s first update the AppRequest type definition.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/app.mjs (partial)</span>
<span class="cm">/**
 * @typedef {Object} AppRequest
 * @property {AppRequestMethod} method
 * @property {string} path
 * @property {AppRequestParams} body
 * @property {Record&lt;string, string|null&gt;} cookies
 */</span>
</code></pre></div></div>

<p>This has no use in NodeJS whatsoever, but it helps documenting the code and adds type hints to the editor. Now we just have to parse cookies from the <code class="language-plaintext highlighter-rouge">cookie</code> header:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/main.mjs (partial)</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parseCookies</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./cookies.mjs</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// inside http.createServer's callback:</span>
<span class="kd">let</span> <span class="p">{</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">json</span><span class="p">,</span> <span class="nx">headers</span><span class="p">,</span> <span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="nx">cookies</span><span class="p">,</span> <span class="nx">location</span> <span class="p">}</span> <span class="o">=</span>
  <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span>
    <span class="na">method</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">method</span><span class="p">,</span>
    <span class="na">path</span><span class="p">:</span> <span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span><span class="p">,</span>
    <span class="na">cookies</span><span class="p">:</span> <span class="nx">parseCookies</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">cookie</span><span class="p">),</span>
    <span class="nx">body</span><span class="p">,</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>I was too lazy to implement <code class="language-plaintext highlighter-rouge">parseCookies</code> myself so I asked ChatGPT:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// cookies.mjs</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">parseCookies</span><span class="p">(</span><span class="nx">cookieHeader</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cookieHeader</span><span class="p">)</span> <span class="k">return</span> <span class="p">{};</span>

  <span class="k">return</span> <span class="nx">cookieHeader</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">;</span><span class="dl">'</span><span class="p">).</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">cookies</span><span class="p">,</span> <span class="nx">cookie</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">]</span> <span class="o">=</span> <span class="nx">cookie</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">=</span><span class="dl">'</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">part</span><span class="p">.</span><span class="nx">trim</span><span class="p">());</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">value</span> <span class="o">||</span> <span class="dl">''</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">cookies</span><span class="p">;</span>
  <span class="p">},</span> <span class="p">{});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>ChatGPT’s implementation seems totally reasonable so I’ll just go with it.</p>

<p>I have no intention to create a test for <code class="language-plaintext highlighter-rouge">src/main.mjs</code> at the moment, so I’ll just manually test it (by starting the server and signing up via the browser). After a quick check, it seems to work perfectly fine.</p>

<p>I do intent to test <code class="language-plaintext highlighter-rouge">/profile</code>, so let’s just break TDD’s rules and write a test after writing the implementation.</p>

<p>First, I need to modify my <code class="language-plaintext highlighter-rouge">GET</code> and <code class="language-plaintext highlighter-rouge">POST</code> methods to handle cookies. I add an <code class="language-plaintext highlighter-rouge">overrides</code> param which can be used to include cookies. I use this new param in my new tests:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app.test.mjs (partial)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">GET /profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">with cookie, returns user</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">GET</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span> <span class="p">}</span> <span class="p">});</span>
    <span class="nx">assertRender</span><span class="p">(</span><span class="dl">"</span><span class="s2">profile.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span> <span class="p">});</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">without cookie, redirects to login</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">GET</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assertRedirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">POST</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">body</span><span class="p">,</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">body</span><span class="p">,</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{},</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">});</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">GET</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{},</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I need to handle the unauthenticated case in my <code class="language-plaintext highlighter-rouge">GET /profile</code> method:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app.mjs</span>
<span class="dl">"</span><span class="s2">GET /profile</span><span class="dl">"</span><span class="p">({</span> <span class="nx">cookies</span><span class="p">,</span> <span class="nx">path</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cookies</span><span class="p">.</span><span class="nx">username</span><span class="p">)</span>
    <span class="k">return</span> <span class="nx">redirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">)</span>
  <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">profile.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">cookies</span><span class="p">.</span><span class="nx">username</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There is a huge flaw with this approach: Cookies are basically user input. Any visitor can alter the contents of the cookie, so anyone can now login as any user by just submitting a cookie with the user’s username.</p>

<p>To reject tainted cookies, or cookies that are otherwise manipulated, I’ll complete this blog by signing the cookies with a secret.</p>

<p>First, I’ll generate a secret and save it in my <code class="language-plaintext highlighter-rouge">.envrc</code>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"export SECRET='</span><span class="si">$(</span>openssl rand <span class="nt">-base64</span> 48<span class="si">)</span><span class="s2">'"</span> <span class="o">&gt;&gt;</span> .envrc
direnv allow
</code></pre></div></div>

<p>Now to sign the cookies with this secret using HMAC. I’m going to create two helper functions for this. I’ll also have all cookies signed by default, and verify all cookies in every request.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/cookies.mjs (partial)</span>
<span class="k">import</span> <span class="nx">crypto</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:crypto</span><span class="dl">"</span><span class="p">;</span>

<span class="cm">/**.
 * @param {string} value
 * @param {string} [secret=process.env.SECRET]
 * @returns {string}
 */</span>
<span class="kd">function</span> <span class="nx">signCookie</span><span class="p">(</span><span class="nx">value</span><span class="p">,</span> <span class="nx">secret</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SECRET</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">hmac</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">createHmac</span><span class="p">(</span><span class="dl">"</span><span class="s2">sha256</span><span class="dl">"</span><span class="p">,</span> <span class="nx">secret</span><span class="p">);</span>
  <span class="nx">hmac</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">signature</span> <span class="o">=</span> <span class="nx">hmac</span><span class="p">.</span><span class="nx">digest</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64url</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">value</span><span class="p">}</span><span class="s2">.</span><span class="p">${</span><span class="nx">signature</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/**
 * @param {string} signedValue
 * @param {string} [secret=process.env.SECRET]
 * @returns {string|null}
 */</span>
<span class="kd">function</span> <span class="nx">verifyCookie</span><span class="p">(</span><span class="nx">signedValue</span><span class="p">,</span> <span class="nx">secret</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SECRET</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">signature</span><span class="p">]</span> <span class="o">=</span> <span class="nx">signedValue</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">value</span> <span class="o">||</span> <span class="o">!</span><span class="nx">signature</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">expectedSignature</span> <span class="o">=</span> <span class="nx">crypto</span>
    <span class="p">.</span><span class="nx">createHmac</span><span class="p">(</span><span class="dl">"</span><span class="s2">sha256</span><span class="dl">"</span><span class="p">,</span> <span class="nx">secret</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">digest</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64url</span><span class="dl">"</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">timingSafeEqual</span><span class="p">(</span>
    <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">signature</span><span class="p">),</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">expectedSignature</span><span class="p">)))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">value</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">parseCookies</span><span class="p">(</span><span class="nx">cookieHeader</span><span class="p">,</span> <span class="nx">secret</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SECRET</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cookieHeader</span><span class="p">)</span> <span class="k">return</span> <span class="p">{};</span>

  <span class="k">return</span> <span class="nx">cookieHeader</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">;</span><span class="dl">'</span><span class="p">).</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">cookies</span><span class="p">,</span> <span class="nx">cookie</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">]</span> <span class="o">=</span> <span class="nx">cookie</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">=</span><span class="dl">'</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">part</span><span class="p">.</span><span class="nx">trim</span><span class="p">());</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span>
      <span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">verifyCookie</span><span class="p">(</span><span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">value</span> <span class="o">||</span> <span class="dl">''</span><span class="p">),</span> <span class="nx">secret</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">cookies</span><span class="p">;</span>
  <span class="p">},</span> <span class="p">{});</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/main.mjs (partial)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">cookies</span><span class="p">)</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">in</span> <span class="nx">cookies</span><span class="p">)</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">cookies</span><span class="p">.</span><span class="nx">hasOwnProperty</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">`</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">=</span><span class="p">${</span><span class="nx">signCookie</span><span class="p">(</span><span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">])</span> <span class="o">??</span> <span class="dl">""</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="s2">`Path=/`</span><span class="p">,</span>
        <span class="s2">`HttpOnly`</span><span class="p">,</span>
        <span class="s2">`SameSite=Strict`</span><span class="p">,</span>
        <span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">?</span> <span class="s2">`Max-Age=</span><span class="p">${</span><span class="nx">YEAR_SECONDS</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="s2">`Max-Age=-1`</span>
      <span class="p">].</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">; </span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">set-cookie</span><span class="dl">"</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Since the signing and verifying of cookies is handled in <code class="language-plaintext highlighter-rouge">src/main.mjs</code>, none of the request handlers need to change their implementation. The handlers receive verified cookies, and they can set cookies which are always signed automatically. I don’t have to change any of my handlers or their tests.</p>

<p>Again, I tested the change manually by checking if the browser gets assigned a signed cookie, and altering the cookie breaks the login. It did, so it works! In a real production-ready application, I would very likely add some tests specifically testing the HTTP request &amp; response handling.</p>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[A challenge to write an application with not a single dependency]]></summary></entry><entry><title type="html">Template language without dependencies</title><link href="https://bonaroo.nl/2025/01/09/the-template-language.html" rel="alternate" type="text/html" title="Template language without dependencies" /><published>2025-01-09T00:00:00+00:00</published><updated>2025-01-09T00:00:00+00:00</updated><id>https://bonaroo.nl/2025/01/09/the-template-language</id><content type="html" xml:base="https://bonaroo.nl/2025/01/09/the-template-language.html"><![CDATA[<h1 id="the-template-language">The Template Language</h1>

<blockquote>
  <p>This blog is a part 2 of my bluebird blog. <a href="https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies.html">See part 1</a></p>
</blockquote>

<p>For my twitter clone without dependencies, I’ve decided to design my route handlers in a way that they’ll return variables and a template name. This allows for easy testing, as my tests can just do assertions on the template name and variables rather than inspecting a HTML document.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// request</span>
<span class="p">{</span>
  <span class="nl">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/profile/1234</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">user-id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">54</span> <span class="p">},</span>
<span class="p">}</span>

<span class="c1">// response</span>
<span class="p">{</span>
  <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
  <span class="na">template</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-profile-show</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">variables</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">user</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">id</span><span class="p">:</span> <span class="mi">54</span><span class="p">,</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">John Doe</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="na">posts</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">55412</span><span class="p">,</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Have you seen the new iThing?</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">createdAt</span><span class="p">:</span> <span class="mi">1699788972</span> <span class="p">}</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In this blog, I’m going to implement this template language.</p>

<h2 id="designing-the-template-language">Designing the template language</h2>

<p>The template language I need, needs to output HTML documents with only a set of variables as input. I want a template to be compiled to a JS function. For example, I want <code class="language-plaintext highlighter-rouge">Hello &lt;%= name %&gt;</code> to compile to something like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="s2">`Hello </span><span class="p">${</span><span class="nx">escapeHtml</span><span class="p">(</span><span class="nx">name</span><span class="p">)}</span><span class="s2">`</span><span class="p">;</span>
</code></pre></div></div>

<p>I’ll go with a classic <code class="language-plaintext highlighter-rouge">&lt;%= %&gt;</code> syntax, because it’s incredibly common and well known. Most developers that see this syntax will intuitively know that you can just write regular code in there and the output of the code will be added to the output.</p>

<p>It must support variables and auto-escape HTML entities. Loops, if/else statements and including other templates must also be supported. It would be nice if we could invoke arbitrary functions and do some basic math;</p>

<p>So basically, I want it to be able to execute arbitrary javascript.</p>

<h2 id="the-implementation">The implementation</h2>

<p>I guess I just get started writing code and see where I end up. First, a test.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">simple template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, &lt;%= name %&gt;</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">world</span><span class="dl">"</span> <span class="p">}),</span> <span class="dl">"</span><span class="s2">Hello, world</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The simplest implementation I can think of is to use a regular expression. All content outside <code class="language-plaintext highlighter-rouge">&lt;%= %&gt;</code> will just be added to the output, and the content between will be executed as JS.</p>

<!-- show a slide with detailed exploded view of this regular expression -->

<p>The regular expression used is <code class="language-plaintext highlighter-rouge">/(.*?)&lt;%(.*?)%&gt;/sg</code>. This regular expression captures any text until the first <code class="language-plaintext highlighter-rouge">&lt;%</code> it finds using <code class="language-plaintext highlighter-rouge">(.*?)&lt;%</code>. Then it captures any text until <code class="language-plaintext highlighter-rouge">%&gt;</code> using <code class="language-plaintext highlighter-rouge">(.*?)%&gt;</code>. The <code class="language-plaintext highlighter-rouge">s</code> modifier allows the <code class="language-plaintext highlighter-rouge">.</code> (dot) to match newlines. The <code class="language-plaintext highlighter-rouge">g</code> modifier allows multiple matches.</p>

<!-- it would be very nice if there was some kind of animation going on here -->

<p>Javascript’s <code class="language-plaintext highlighter-rouge">replace</code> function on string allows executing code for every match while also returning a replacement value, <code class="language-plaintext highlighter-rouge">""</code> in my code. Because every match is replaced by an empty string, only the text after the last <code class="language-plaintext highlighter-rouge">%&gt;</code> is returned by the <code class="language-plaintext highlighter-rouge">replace</code>-function, which I call the <code class="language-plaintext highlighter-rouge">tail</code>.</p>

<p>I use <code class="language-plaintext highlighter-rouge">JSON.stringify</code> to create a string literal.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Template</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">parse</span><span class="p">(</span><span class="nx">template</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">body</span> <span class="o">=</span> <span class="p">[</span>
      <span class="dl">"</span><span class="s2">eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);</span><span class="dl">"</span><span class="p">,</span>
      <span class="s2">`out = [];`</span>
    <span class="p">];</span>
    <span class="kd">const</span> <span class="nx">tail</span> <span class="o">=</span> <span class="nx">template</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">&lt;%</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">%&gt;/</span><span class="nx">sg</span><span class="p">,</span> <span class="p">(</span><span class="nx">_</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="nx">code</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">content</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>

      <span class="k">if</span> <span class="p">(</span><span class="nx">code</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">=</span><span class="dl">"</span><span class="p">))</span>
        <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">code</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="mi">1</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>

      <span class="k">return</span> <span class="dl">""</span><span class="p">;</span>
    <span class="p">});</span>

    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">tail</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>
    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`return out.join("");`</span><span class="p">);</span>

    <span class="k">return</span> <span class="k">new</span> <span class="nb">Function</span><span class="p">(</span><span class="dl">'</span><span class="s1">vars</span><span class="dl">'</span><span class="p">,</span> <span class="nx">body</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">))</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>For the template in the test, this function returns a function like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span><span class="p">(</span><span class="nx">vars</span><span class="p">)</span> <span class="p">{</span>
  <span class="nb">eval</span><span class="p">(</span><span class="s2">`var { </span><span class="p">${</span><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">vars</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">, </span><span class="dl">'</span><span class="p">)}</span><span class="s2"> } = vars;`</span><span class="p">);</span>
  <span class="nx">out</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, </span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
  <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">out</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<!-- CONTENT BELOW COPIED FROM STORY, NOT UPDATED FOR BLOG -->

<p>Another notable part of this code is the eval statement. To allow the template to refer to any variable in <code class="language-plaintext highlighter-rouge">vars</code> (<code class="language-plaintext highlighter-rouge">name</code> in this example), I need to make the properties in <code class="language-plaintext highlighter-rouge">vars</code> available as local variables in the function.</p>

<p>There is no easy way to determine the possible variables while compiling, so I generate them at runtime. The only way I know to assign arbitrary local variables at runtime, is to use <code class="language-plaintext highlighter-rouge">eval</code>.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="p">{</span> <span class="nx">foo</span> <span class="p">}</span> <span class="o">=</span> <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="mi">1</span> <span class="p">};</span>
<span class="c1">// foo = 1</span>
<span class="nb">eval</span><span class="p">(</span><span class="dl">'</span><span class="s1">var { bar } = { bar: 2 }</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// bar = 2</span>
</code></pre></div></div>

<p>Another method is to use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with"><code class="language-plaintext highlighter-rouge">with</code>-statement</a>, which is <a href="https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-with-statement">discouraged</a>. Let’s try it anyway.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span><span class="p">(</span><span class="nx">vars</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">with</span> <span class="p">(</span><span class="nx">vars</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">out</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, </span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
    <span class="nx">out</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">out</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The generated function works perfectly. Too bad the feature is discouraged, legacy or deprecated, depending on who you ask. So far my options are evil <code class="language-plaintext highlighter-rouge">eval</code> or deprecated <code class="language-plaintext highlighter-rouge">with</code>. Ideally, I want to determine the variables used at compile-time, but this requires compiling the Javascript code to determine the variables used.</p>

<p>There is no easy way to get an abstract syntax tree of some piece of Javascript using plain NodeJS.</p>

<p>Now to escape HTML entities, support <code class="language-plaintext highlighter-rouge">if</code>/<code class="language-plaintext highlighter-rouge">else</code> statements and add some minor fixes.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Template.parse</span>
<span class="kd">let</span> <span class="nx">body</span> <span class="o">=</span> <span class="p">[</span>
  <span class="dl">"</span><span class="s2">eval(`var { ${Object.keys(vars).join(', ')} } = vars;`);</span><span class="dl">"</span><span class="p">,</span>
  <span class="s2">`out = [];`</span>
<span class="p">];</span>
<span class="kd">const</span> <span class="nx">tail</span> <span class="o">=</span> <span class="nx">template</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">&lt;%</span><span class="se">(</span><span class="sr">.*</span><span class="se">?)</span><span class="sr">%&gt;/</span><span class="nx">sg</span><span class="p">,</span> <span class="p">(</span><span class="nx">_</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="nx">code</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">content</span><span class="p">)</span>
    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">content</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">code</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">=</span><span class="dl">"</span><span class="p">))</span>
    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(escapeHtml(</span><span class="p">${</span><span class="nx">code</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="mi">1</span><span class="p">)}</span><span class="s2">));`</span><span class="p">);</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">code</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">-</span><span class="dl">"</span><span class="p">))</span>
    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">code</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="mi">1</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>
  <span class="k">else</span>
    <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">code</span><span class="p">);</span>

  <span class="k">return</span> <span class="dl">""</span><span class="p">;</span>
<span class="p">});</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">tail</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`out.push(</span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">tail</span><span class="p">)}</span><span class="s2">);`</span><span class="p">);</span>

<span class="nx">body</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`return out.join("");`</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">body</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Function</span><span class="p">(</span><span class="dl">'</span><span class="s1">vars</span><span class="dl">'</span><span class="p">,</span> <span class="nx">body</span><span class="p">);</span>
<span class="k">return</span> <span class="p">(</span><span class="nx">vars</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">fn</span><span class="p">({</span> <span class="p">...</span><span class="nx">vars</span><span class="p">,</span> <span class="p">...</span><span class="nx">Template</span><span class="p">.</span><span class="nx">locals</span> <span class="p">});</span>

<span class="c1">// Template.locals</span>
<span class="nl">locals</span><span class="p">:</span> <span class="p">{</span>
  <span class="na">escapeHtml</span><span class="p">:</span> <span class="p">(</span><span class="nx">str</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="s2">`</span><span class="p">${</span><span class="nx">str</span><span class="p">}</span><span class="s2">`</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">&lt;&gt;&amp;"'</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="nx">s</span> <span class="o">=&gt;</span>
    <span class="p">({</span> <span class="dl">"</span><span class="s2">&lt;</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&amp;lt;</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">&gt;</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&amp;gt;</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">&amp;</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&amp;amp;</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1">"</span><span class="dl">'</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&amp;quot;</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">'</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&amp;#39;</span><span class="dl">"</span> <span class="p">})[</span><span class="nx">s</span><span class="p">])</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I also added some more tests.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">Template.parse</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">simple template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, &lt;%= name %&gt;</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">world</span><span class="dl">"</span> <span class="p">}),</span> <span class="dl">"</span><span class="s2">Hello, world</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">math template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">1 + 1 = &lt;%= 1 + 1 %&gt;</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">fn</span><span class="p">({}),</span> <span class="dl">"</span><span class="s2">1 + 1 = 2</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">function template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello &lt;%= foo() %&gt;</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">foo</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="dl">"</span><span class="s2">world</span><span class="dl">"</span> <span class="p">}),</span> <span class="dl">"</span><span class="s2">Hello world</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">if-else template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="s2">`Answer: &lt;% if (answer) { %&gt;Yes&lt;% } else { %&gt;No&lt;% } %&gt;`</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">deepEqual</span><span class="p">(</span>
      <span class="p">[</span><span class="nx">fn</span><span class="p">({</span> <span class="na">answer</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}),</span> <span class="nx">fn</span><span class="p">({</span> <span class="na">answer</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})],</span>
      <span class="p">[</span><span class="dl">"</span><span class="s2">Answer: Yes</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Answer: No</span><span class="dl">"</span><span class="p">]);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">multiline template</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="s2">`
    Answer:
    &lt;% if (answer) { %&gt;
      Yes
    &lt;% } else { %&gt;
      No
    &lt;% } %&gt;
  `</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">deepEqual</span><span class="p">(</span>
      <span class="p">[</span><span class="nx">delws</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">answer</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})),</span> <span class="nx">delws</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">answer</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}))],</span>
      <span class="p">[</span><span class="dl">"</span><span class="s2">Answer: Yes</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Answer: No</span><span class="dl">"</span><span class="p">]);</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">escape html</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello, &lt;%= name %&gt;</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">fn</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&lt;script&gt; &amp; </span><span class="se">\"</span><span class="s2"> '</span><span class="dl">"</span> <span class="p">}),</span> <span class="dl">"</span><span class="s2">Hello, &amp;lt;script&amp;gt; &amp;amp; &amp;quot; &amp;#39;</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="kd">function</span> <span class="nx">delws</span><span class="p">(</span><span class="nx">str</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">str</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/^</span><span class="se">\s</span><span class="sr">+|</span><span class="se">\s</span><span class="sr">+$/g</span><span class="p">,</span> <span class="dl">""</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\s</span><span class="sr">+/g</span><span class="p">,</span> <span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To allow includes, I’m going to add a function to parse all template files in a directory. This function will keep a dictionary with template names as keys and their parsed template functions as values.</p>

<h4 id="srctemplatemjs">src/template.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">Template</span> <span class="o">=</span> <span class="p">{</span>
  <span class="cm">/**
   *
   * @param {string} path Directory containing one or more template files
   * @returns {Promise&lt;(template: string, vars: Record&lt;string, any&gt;) =&gt; string&gt;}
   */</span>
  <span class="k">async</span> <span class="nx">parseDirectory</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/** @type {Map&lt;string, function&gt;} */</span>
    <span class="kd">const</span> <span class="nx">templates</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>

    <span class="kd">const</span> <span class="nx">include</span> <span class="o">=</span> <span class="p">(</span><span class="nx">templateName</span><span class="p">,</span> <span class="nx">vars</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">template</span> <span class="o">=</span> <span class="nx">templates</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">templateName</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">template</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Template </span><span class="p">${</span><span class="nx">path</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">templateName</span><span class="p">}</span><span class="s2"> does not exist, `</span>
        <span class="o">+</span> <span class="s2">`templates found: </span><span class="p">${</span><span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">templates</span><span class="p">.</span><span class="nx">keys</span><span class="p">()).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">, </span><span class="dl">"</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span>
      <span class="k">return</span> <span class="nx">template</span><span class="p">({</span> <span class="p">...</span><span class="nx">vars</span><span class="p">,</span> <span class="nx">include</span> <span class="p">});</span>
    <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">readDir</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">prefix</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">innerPath</span> <span class="o">=</span> <span class="nx">join</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">prefix</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">fileNames</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">promises</span><span class="p">.</span><span class="nx">readdir</span><span class="p">(</span><span class="nx">join</span><span class="p">(</span><span class="nx">innerPath</span><span class="p">));</span>

      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">fileName</span> <span class="k">of</span> <span class="nx">fileNames</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">templateName</span> <span class="o">=</span> <span class="nx">join</span><span class="p">(</span><span class="nx">prefix</span><span class="p">,</span> <span class="nx">fileName</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">filePath</span> <span class="o">=</span> <span class="nx">join</span><span class="p">(</span><span class="nx">innerPath</span><span class="p">,</span> <span class="nx">fileName</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">fileName</span><span class="p">.</span><span class="nx">endsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">.ejs</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">promises</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="nx">filePath</span><span class="p">,</span> <span class="p">{</span> <span class="na">encoding</span><span class="p">:</span> <span class="dl">"</span><span class="s2">utf-8</span><span class="dl">"</span> <span class="p">});</span>
          <span class="nx">templates</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">templateName</span><span class="p">,</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filePath</span> <span class="p">}));</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">((</span><span class="k">await</span> <span class="nx">promises</span><span class="p">.</span><span class="nx">stat</span><span class="p">(</span><span class="nx">filePath</span><span class="p">)).</span><span class="nx">isDirectory</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">await</span> <span class="nx">readDir</span><span class="p">(</span><span class="nx">join</span><span class="p">(</span><span class="nx">prefix</span><span class="p">,</span> <span class="nx">fileName</span><span class="p">));</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">};</span>

    <span class="k">await</span> <span class="nx">readDir</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>

    <span class="k">return</span> <span class="nx">include</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="testtemplatesejs">test/templates/**.ejs</h4>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- layout/header.ejs --&gt;</span>
<span class="nt">&lt;h1&gt;</span>Header<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- bar.ejs --&gt;</span>
Hello <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">name</span> <span class="err">%</span><span class="nt">&gt;</span>

<span class="c">&lt;!-- foo.ejs --&gt;</span>
<span class="nt">&lt;</span><span class="err">%</span><span class="na">-</span> <span class="na">include</span><span class="err">("</span><span class="na">layout</span><span class="err">/</span><span class="na">header.ejs</span><span class="err">")</span> <span class="err">%</span><span class="nt">&gt;</span>
<span class="nt">&lt;</span><span class="err">%</span><span class="na">-</span> <span class="na">include</span><span class="err">("</span><span class="na">bar.ejs</span><span class="err">",</span> <span class="err">{</span> <span class="na">name</span> <span class="err">})</span> <span class="err">%</span><span class="nt">&gt;</span>
</code></pre></div></div>

<h4 id="testtemplatetestmjs">test/template.test.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">parseDirectory</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">parses a tree</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parseDirectory</span><span class="p">(</span><span class="dl">"</span><span class="s2">test/templates</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">delws</span><span class="p">(</span><span class="nx">fn</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">World!</span><span class="dl">"</span> <span class="p">})),</span>
        <span class="dl">"</span><span class="s2">&lt;h1&gt;Header&lt;/h1&gt; Hello World!</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">});</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>Now to integrate this template engine in the <code class="language-plaintext highlighter-rouge">main.mjs</code> file to render the template using the <code class="language-plaintext highlighter-rouge">.ejs</code> templates.</p>

<h4 id="templateshomeejs">templates/home.ejs</h4>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>Hello, <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">name</span> <span class="err">%</span><span class="nt">&gt;</span>!<span class="nt">&lt;/h1&gt;</span>
</code></pre></div></div>

<h4 id="srcmainmjs">src/main.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">render</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Template</span><span class="p">.</span><span class="nx">parseDirectory</span><span class="p">(</span><span class="dl">"</span><span class="s2">templates</span><span class="dl">"</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">createServer</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="p">{</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">json</span><span class="p">,</span> <span class="nx">headers</span><span class="p">,</span> <span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">();</span>

  <span class="c1">// (hidden)</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">template</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">render</span><span class="p">(</span><span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">));</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8000/
<span class="c"># &lt;h1&gt;Hello, World!&lt;/h1&gt;</span>
</code></pre></div></div>

<p>Now we’re ready to start writing our application, which will continue in <a href="https://bonaroo.nl/2025/01/10/user-signup-and-login.html">the next blog</a></p>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[A challenge to write an application with not a single dependency]]></summary></entry><entry><title type="html">Building NodeJS applications without dependencies</title><link href="https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies.html" rel="alternate" type="text/html" title="Building NodeJS applications without dependencies" /><published>2023-12-12T00:00:00+00:00</published><updated>2023-12-12T00:00:00+00:00</updated><id>https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies</id><content type="html" xml:base="https://bonaroo.nl/2023/12/12/building-node-applications-without-dependencies.html"><![CDATA[<h1 id="building-nodejs-applications-without-dependencies">Building NodeJS applications without dependencies</h1>

<p>Video to accompany this blog post:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/QZyX1ZjZqJs?si=e3-MtrqtcuHc4dyp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<h2 id="introduction">Introduction</h2>

<p>I’m going to build a twitter clone using NodeJS, but without any dependencies.</p>

<p>I’ve written many Node applications using dependencies like <code class="language-plaintext highlighter-rouge">express</code>, <code class="language-plaintext highlighter-rouge">jest</code>, <code class="language-plaintext highlighter-rouge">lodash</code>, <code class="language-plaintext highlighter-rouge">webpack</code>, <code class="language-plaintext highlighter-rouge">typescript</code>, <code class="language-plaintext highlighter-rouge">typeorm</code> and many more. My applications can have tens of these dependencies, and some of these dependencies will also have many dependencies, and I end up with literally 1000s of dependencies.</p>

<p>Updating these projects can be a major pain as dependencies get outdated and need updating, and I got tired of it. I decided to challenge myself to write an application with not a single dependency.</p>

<p>In this blog, I’ll start with a simple HTTP server and some tests.</p>

<p>Normally, I use <code class="language-plaintext highlighter-rouge">express</code> as a webserver, and I use <code class="language-plaintext highlighter-rouge">jest</code> for testing. Additionally, for testing HTTP requests, I like to use <code class="language-plaintext highlighter-rouge">supertest</code> and <code class="language-plaintext highlighter-rouge">jsdom</code> for checking the HTML responses.</p>

<!-- example express server, example tests using jest, supertest & jsdom -->

<p>Naturally, without dependencies, I can’t use any of these things.</p>

<p>Before I start working on an actual HTTP server, let’s consider how to test the application. I can start an HTTP server for testing and use Node’s <code class="language-plaintext highlighter-rouge">fetch</code> or http client to query the server, but this makes testing slow and dependent on the network stack.</p>

<!-- example server, example tests using fetch -->

<p>So instead, I’m going to abstract away any HTTP details. I’m going to define a boundary that accepts plain objects representing HTTP requests and returns plain objects representing HTTP responses.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// request</span>
<span class="p">{</span>
  <span class="nl">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/profile/1234</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">user-id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">54</span> <span class="p">},</span>
<span class="p">}</span>

<span class="c1">// response</span>
<span class="p">{</span>
  <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
  <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span> <span class="p">},</span>
  <span class="na">content</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&lt;!DOCTYPE ...</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That way I can convert a http request as implemented by NodeJS to the plain object representing the request, pass it to the implementation behind the boundary, and then write the response to the NodeJS http response.</p>

<p>Meanwhile, in the tests, I can just mock the requests by creating object literals representing the requests and do assertions on the response object.</p>

<!-- sample handler returning html document + test -->

<h2 id="assertions-on-content-in-the-html-document">Assertions on content in the HTML document</h2>

<p>Checking HTTP responses where an application would return a HTML response could be a bit of a pain. For example, let’s assert the page returns an H1 containing “hello world”:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">).</span><span class="nx">toContain</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;h1&gt;Hello world&lt;/h1&gt;</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>For the following responses, the assertion would fail:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>
  Hello world
<span class="nt">&lt;/h1&gt;</span>
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1</span> <span class="na">class=</span><span class="s">"my-header"</span><span class="nt">&gt;</span>Hello world<span class="nt">&lt;/h1&gt;</span>
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"h1"</span><span class="nt">&gt;</span>Hello world<span class="nt">&lt;/h2&gt;</span>
</code></pre></div></div>

<p>Meanwhile, I usually don’t care the exact formatting or semantics of the HTML document. I just want to know if some content is or isn’t rendered. Easy, you might think:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">).</span><span class="nx">toContain</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello world</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>This causes all the tests to pass. Great. But what if the page doesn’t render a ‘Hello world’? Well…</p>

<blockquote>
  <p><strong>FAILED: <code class="language-plaintext highlighter-rouge">"Hello world"</code> not found in</strong></p>

  <p><code class="language-plaintext highlighter-rouge">&lt;main id="content" class="mw-body" role="main"&gt; &lt;header class="mw-body-header vector-page-titlebar"&gt; &lt;nav role="navigation" aria-label="Contents" class="vector-toc-landmark"&gt; &lt;div id="vector-page-titlebar-toc" class="vector-dropdown vector-page-titlebar-toc vector-button-flush-left"&gt; &lt;input type="checkbox" id="vector-page-titlebar-toc-checkbox" role="button" aria-haspopup="true" data-event-name="ui.dropdown-vector-page-titlebar-toc" class="vector-dropdown-checkbox " aria-label="Toggle the table of contents"&gt; &lt;label id="vector-page-titlebar-toc-label" for="vector-page-titlebar-toc-checkbox" class="vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--weight-quiet cdx-button--icon-only" aria-hidden="true"&gt;&lt;span class="vector-icon mw-ui-icon-listBullet mw-ui-icon-wikimedia-listBullet"&gt;&lt;/span&gt; &lt;span class="vector-dropdown-label-text"&gt;Toggle the table of contents&lt;/span&gt; &lt;/label&gt; &lt;div class="vector-dropdown-content"&gt; &lt;div id="vector-page-titlebar-toc-unpinned-container" class="vector-unpinned-container"&gt; &lt;div id="vector-toc" class="vector-toc vector-pinnable-element"&gt; &lt;div class="vector-pinnable-header vector-toc-pinnable-header vector-pinnable-header-pinned" data-feature-name="toc-pinned" data-pinnable-element-id="vector-toc"&gt; &lt;h2 class="vector-pinnable-header-label"&gt;Contents&lt;/h2&gt; &lt;button class="vector-pinnable-header-toggle-button vector-pinnable-header-pin-button" data-event-name="pinnable-header.vector-toc.pin"&gt;move to sidebar&lt;/button&gt; &lt;button class="vector-pinnable-header-toggle-button vector-pinnable-header-unpin-button" data-event-name="pinnable-header.vector-toc.unpin"&gt;hide&lt;/button&gt; &lt;/div&gt; &lt;ul class="vector-toc-contents" id="mw-panel-toc-list"&gt; &lt;li id="toc-mw-content-text" class="vector-toc-list-item vector-toc-level-1 vector-toc-level-1-active vector-toc-list-item-active"&gt; &lt;a href="#" class="vector-toc-link"&gt; &lt;div class="vector-toc-text"&gt;(Top)&lt;/div&gt; &lt;/a&gt; &lt;/li&gt; &lt;li id="toc-History" class="vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded"&gt; &lt;a class="vector-toc-link" href="#History"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;1&lt;/span&gt;History&lt;/div&gt; &lt;/a&gt; &lt;button aria-controls="toc-History-sublist" class="cdx-button cdx-button--weight-quiet cdx-button--icon-only vector-toc-toggle" aria-expanded="true"&gt; &lt;span class="vector-icon vector-icon--x-small mw-ui-icon-wikimedia-expand"&gt;&lt;/span&gt; &lt;span&gt;Toggle History subsection&lt;/span&gt; &lt;/button&gt; &lt;ul id="toc-History-sublist" class="vector-toc-list"&gt; &lt;li id="toc-Origin" class="vector-toc-list-item vector-toc-level-2"&gt; &lt;a class="vector-toc-link" href="#Origin"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;1.1&lt;/span&gt;Origin&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-Origin-sublist" class="vector-toc-list"&gt; &lt;li id="toc-Simultaneous_references" class="vector-toc-list-item vector-toc-level-3"&gt; &lt;a class="vector-toc-link" href="#Simultaneous_references"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;1.1.1&lt;/span&gt;Simultaneous references&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-Simultaneous_references-sublist" class="vector-toc-list"&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li id="toc-Growth_in_2008" class="vector-toc-list-item vector-toc-level-2"&gt; &lt;a class="vector-toc-link" href="#Growth_in_2008"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;1.2&lt;/span&gt;Growth in 2008&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-Growth_in_2008-sublist" class="vector-toc-list"&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li id="toc-Later_usage" class="vector-toc-list-item vector-toc-level-2"&gt; &lt;a class="vector-toc-link" href="#Later_usage"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;1.3&lt;/span&gt;Later usage&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-Later_usage-sublist" class="vector-toc-list"&gt; &lt;/ul&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li id="toc-Reaction" class="vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded"&gt; &lt;a class="vector-toc-link" href="#Reaction"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;2&lt;/span&gt;Reaction&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-Reaction-sublist" class="vector-toc-list"&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li id="toc-See_also" class="vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded"&gt; &lt;a class="vector-toc-link" href="#See_also"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;3&lt;/span&gt;See also&lt;/div&gt; &lt;/a&gt; &lt;ul id="toc-See_also-sublist" class="vector-toc-list"&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li id="toc-References" class="vector-toc-list-item vector-toc-level-1 vector-toc-list-item-expanded"&gt; &lt;a class="vector-toc-link" href="#References"&gt; &lt;div class="vector-toc-text"&gt; &lt;span class="vector-toc-numb"&gt;4&lt;/span&gt;References&lt;/div&gt; &lt;/a&gt;</code></p>
</blockquote>

<p>There’s no fun in debugging that response.</p>

<!-- prepare a terminal screenshot of a failing test -->

<p>There are other options.</p>

<p>I can make the server only respond with JSON and handle the rendering on the client using a React app. That way, I can just test the JSON responses, which are much easier to debug and reason about.</p>

<!-- show react app and server handler returning json? -->

<p>However, I don’t want to introduce a whole new app just to make testing the server easier. Single Page Applications can quickly become very complex and are prone to errors. In my experience, Server-Side Rendering applications are easier to build and maintain, less prone to errors and can be just as responsive.</p>

<p>In server-side generated webapplications, you usually use templates to convert data to html. So what if I make the boundary just return a template name and all data required to render the page using that template?</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// request</span>
<span class="p">{</span>
  <span class="nl">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/profile/1234</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">user-id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">54</span> <span class="p">},</span>
<span class="p">}</span>

<span class="c1">// response</span>
<span class="p">{</span>
  <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
  <span class="na">template</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-profile-show</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">variables</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">user</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">id</span><span class="p">:</span> <span class="mi">54</span><span class="p">,</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">John Doe</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="na">posts</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">55412</span><span class="p">,</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Have you seen the new iThing?</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">createdAt</span><span class="p">:</span> <span class="mi">1699788972</span> <span class="p">}</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then in my test, I can just assert the template name and data, which is much easier to reason about and results in much more accurate failure messages.</p>

<!-- show failed test example -->

<p>As long as I don’t too much crazy stuff in the templates, I can generally assume that if the data and template returned are correct and the template renders without error, the page should pass the test.</p>

<p>This method of testing isn’t perfect. It’s possible for a template to just render nothing at all, and the tests would still pass. In my experience, there’s just no way to test whether a HTML document actually renders some content. Even if the content would be present in the HTML document, it could be rendered incorrectly or not at all using CSS.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">).</span><span class="nx">toContain</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello world</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- PASS! --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">style=</span><span class="s">"display: none;"</span><span class="nt">&gt;</span>Hello world!<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- LGTM! --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">style=</span><span class="s">"position: absolute; top: -1000;"</span><span class="nt">&gt;</span>Hello world!<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- SURE! --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">style=</span><span class="s">"color: transparent;"</span><span class="nt">&gt;</span>Hello world!<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- PERFECT! --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">style=</span><span class="s">"font-size: 0px;"</span><span class="nt">&gt;</span>Hello world!<span class="nt">&lt;/h1&gt;</span>

<span class="c">&lt;!-- YEAH! --&gt;</span>
<span class="nt">&lt;h1</span> <span class="na">id=</span><span class="s">"foo"</span><span class="nt">&gt;</span>Hello world!<span class="nt">&lt;/h1&gt;</span>
<span class="nt">&lt;script&gt;</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#foo</span><span class="dl">"</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span><span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>Therefore, in my opinion, templates can only be tested by a pair of human eyes, rendering them using a real browser. Ideally, this manual testing can be done in multiple browsers very quickly.</p>

<p>To test the templates, I’ll could render the page in every test and save the output. If there’s a fatal error rendering the page, the test will fail. If the rendering succeeds, I can manually check the HTML document by opening it in my browser. Maybe I’ll create an overview page with all HTML documents in iframes to quickly review multiple pages.</p>

<h2 id="implementation">Implementation</h2>

<p>After implementing some example pages and building an <code class="language-plaintext highlighter-rouge">.ejs</code>-like template language, the code and tests look like this:</p>

<h4 id="appmjs">app.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">App</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">handleRequest</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">fn</span> <span class="o">=</span> <span class="k">this</span><span class="p">[</span><span class="s2">`</span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">method</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">path</span><span class="p">}</span><span class="s2">`</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">fn</span><span class="p">)</span>
      <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">not-found.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="nx">req</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">404</span> <span class="p">});</span>
    <span class="k">return</span> <span class="nx">fn</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">req</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="k">async</span> <span class="dl">"</span><span class="s2">GET /login</span><span class="dl">"</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">errorCode</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">""</span> <span class="p">}</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="dl">"</span><span class="s2">POST /login</span><span class="dl">"</span><span class="p">({</span> <span class="nx">body</span> <span class="p">})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span>
      <span class="k">return</span> <span class="nx">redirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span> <span class="p">});</span>

    <span class="k">return</span> <span class="nx">render</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">errorCode</span><span class="p">:</span> <span class="nx">error</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span>
      <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">username</span> <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">render</code> and <code class="language-plaintext highlighter-rouge">redirect</code> functions just return response objects.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">render</span><span class="p">(</span><span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span> <span class="o">=</span> <span class="p">{},</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">redirect</span><span class="p">(</span><span class="nx">location</span><span class="p">,</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">location</span><span class="p">,</span> <span class="na">status</span><span class="p">:</span> <span class="mi">302</span><span class="p">,</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>They’re very simple one-liners meant to improve readability and set some defaults.</p>

<h4 id="apptestmjs">app.test.mjs</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">GET /login</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">renders login template</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">GET</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assertRender</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">""</span> <span class="p">},</span> <span class="na">errorCode</span><span class="p">:</span> <span class="kc">null</span> <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST /login</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">with unknown username param, returns user not found error</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">POST</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span> <span class="p">});</span>

    <span class="nx">assertRender</span><span class="p">(</span><span class="dl">"</span><span class="s2">login.ejs</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span>
      <span class="na">errorCode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">USER_NOT_FOUND</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">});</span>
  <span class="p">});</span>

  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">with valid params, sets cookie and redirects to profile</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">signup</span><span class="p">(</span><span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span><span class="p">);</span>
    <span class="k">await</span> <span class="nx">POST</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">"</span><span class="s2">bar</span><span class="dl">"</span> <span class="p">});</span>
    <span class="nx">assertRedirect</span><span class="p">(</span><span class="dl">"</span><span class="s2">/profile</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">foo</span><span class="dl">"</span> <span class="p">}</span> <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="testing-utilities">Testing utilities</h3>

<p>In a <code class="language-plaintext highlighter-rouge">beforeEach</code> function, the app is initialized. In my testing file, I have <code class="language-plaintext highlighter-rouge">POST</code> and <code class="language-plaintext highlighter-rouge">GET</code> functions available. Even though they’re oneliners, they improve readability by reducing noise.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">POST</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">body</span> <span class="p">});</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">GET</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="nx">path</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">assertRender</code> and <code class="language-plaintext highlighter-rouge">assertRedirect</code> check the response in <code class="language-plaintext highlighter-rouge">res</code>, assigned by <code class="language-plaintext highlighter-rouge">GET</code> or <code class="language-plaintext highlighter-rouge">POST</code>. <code class="language-plaintext highlighter-rouge">assertRender</code> also renders the template, to check for errors in the template.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">assertRender</span><span class="p">(</span><span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span> <span class="o">=</span> <span class="p">{},</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">deepStrictEqual</span><span class="p">(</span><span class="nx">res</span><span class="p">,</span>
    <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">template</span><span class="p">)</span>
    <span class="nx">renderTemplate</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">template</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">variables</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">assertRedirect</span><span class="p">(</span><span class="nx">location</span><span class="p">,</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">deepStrictEqual</span><span class="p">(</span><span class="nx">res</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">302</span><span class="p">,</span> <span class="nx">location</span><span class="p">,</span> <span class="p">...</span><span class="nx">overrides</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">renderTemplate</code> is created by my template engine. It is a function that accepts a template name and variables and returns HTML as a string.</p>

<p>The implementation of <code class="language-plaintext highlighter-rouge">renderTemplate</code> is handled by the <a href="https://bonaroo.nl/2025/01/10/the-template-language.html">second blog</a></p>

<p>This template engine renders an EJS-like template like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1&gt;</span>Login!<span class="nt">&lt;/h1&gt;</span>

<span class="nt">&lt;form</span> <span class="na">method=</span><span class="s">"POST"</span> <span class="na">action=</span><span class="s">"/login"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;</span><span class="err">%</span> <span class="na">if</span> <span class="err">(</span><span class="na">errorCode</span><span class="err">)</span> <span class="err">{</span> <span class="err">%</span><span class="nt">&gt;</span>
    <span class="nt">&lt;p&gt;&lt;</span><span class="err">%=</span> <span class="err">{</span>
      <span class="err">"</span><span class="na">USER_NOT_FOUND</span><span class="err">"</span><span class="na">:</span> <span class="err">"</span><span class="na">User</span> <span class="na">not</span> <span class="na">found</span><span class="err">",</span>
      <span class="err">"</span><span class="na">INVALID_PASSWORD</span><span class="err">"</span><span class="na">:</span> <span class="err">"</span><span class="na">Invalid</span> <span class="na">password</span><span class="err">"</span>
    <span class="err">}[</span><span class="na">errorCode</span><span class="err">]</span> <span class="err">??</span> <span class="na">errorCode</span> <span class="err">%</span><span class="nt">&gt;&lt;/p&gt;</span>
  <span class="nt">&lt;</span><span class="err">%</span> <span class="err">}</span> <span class="err">%</span><span class="nt">&gt;</span>

  <span class="nt">&lt;label&gt;</span>
    <span class="nt">&lt;strong&gt;</span>Username<span class="nt">&lt;/strong&gt;&lt;br&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span> <span class="na">value=</span><span class="s">"&lt;%= params.username %&gt;"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;/label&gt;&lt;br&gt;</span>

  <span class="nt">&lt;label&gt;</span>
    <span class="nt">&lt;strong&gt;</span>Password<span class="nt">&lt;/strong&gt;&lt;br&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;/label&gt;&lt;br&gt;</span>

  <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>Login<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/form&gt;</span>
</code></pre></div></div>

<p>To some nice HTML.</p>

<p>In the next blog, we’ll do a deep dive into this template language.</p>

<p>As a bonus, here is the code responsible for actually processing HTTP requests and generating responses.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">createServer</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> <span class="s2">`http://</span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">host</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">method</span> <span class="o">!=</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">resolve</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="kd">const</span> <span class="nx">bodyData</span> <span class="o">=</span> <span class="p">[];</span>

      <span class="nx">req</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">data</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">chunk</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">bodyData</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">chunk</span><span class="p">));</span>
      <span class="nx">req</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">end</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">querystring</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">bodyData</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">""</span><span class="p">)));</span> <span class="p">});</span>
    <span class="p">});</span>

    <span class="kd">let</span> <span class="p">{</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">json</span><span class="p">,</span> <span class="nx">headers</span><span class="p">,</span> <span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="nx">cookies</span><span class="p">,</span> <span class="nx">location</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">app</span><span class="p">.</span><span class="nx">handleRequest</span><span class="p">({</span>
      <span class="na">method</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">method</span><span class="p">,</span>
      <span class="na">path</span><span class="p">:</span> <span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span><span class="p">,</span>
      <span class="nx">body</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">=</span> <span class="nx">status</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">headers</span><span class="p">)</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">in</span> <span class="nx">headers</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">headers</span><span class="p">.</span><span class="nx">hasOwnProperty</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span>
          <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">cookies</span><span class="p">)</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">in</span> <span class="nx">cookies</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">cookies</span><span class="p">.</span><span class="nx">hasOwnProperty</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s2">`</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">=</span><span class="p">${</span><span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">??</span> <span class="dl">""</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
            <span class="s2">`Path=/`</span><span class="p">,</span>
            <span class="s2">`HttpOnly`</span><span class="p">,</span>
            <span class="s2">`SameSite=Strict`</span><span class="p">,</span>
            <span class="nx">cookies</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">?</span> <span class="s2">`Max-Age=</span><span class="p">${</span><span class="nx">YEAR_SECONDS</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="s2">`Max-Age=-1`</span>
          <span class="p">].</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">; </span><span class="dl">"</span><span class="p">);</span>
          <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">set-cookie</span><span class="dl">"</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
        <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">location</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Location</span><span class="dl">"</span><span class="p">,</span> <span class="nx">location</span><span class="p">);</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">json</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">json</span><span class="p">));</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">template</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span><span class="p">);</span>
      <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">render</span><span class="p">(</span><span class="nx">template</span><span class="p">,</span> <span class="nx">variables</span><span class="p">));</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Request handler had no response`</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">=</span> <span class="mi">500</span><span class="p">;</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">setHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">text/plain</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="s2">`SOMETHING WENT WRONG\n</span><span class="p">${</span><span class="nx">error</span><span class="p">.</span><span class="nx">stack</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[A challenge to write an application with not a single dependency]]></summary></entry><entry><title type="html">Circuit Simulator: Compiling a bitmap</title><link href="https://bonaroo.nl/2023/10/30/microengineer-compiler.html" rel="alternate" type="text/html" title="Circuit Simulator: Compiling a bitmap" /><published>2023-10-30T00:00:00+00:00</published><updated>2023-10-30T00:00:00+00:00</updated><id>https://bonaroo.nl/2023/10/30/microengineer-compiler</id><content type="html" xml:base="https://bonaroo.nl/2023/10/30/microengineer-compiler.html"><![CDATA[<h1 id="circuit-simulator-compiling-a-bitmap">Circuit Simulator: Compiling a bitmap</h1>

<h2 id="introduction">Introduction</h2>

<p>I want to build a game where you can draw a circuit and have it be running while you’re drawing. The circuit should control some thing in a virtual environment, like a tiny car or traffic lights.</p>

<p>The circuit “language”, as designed by <a href="https://realhet.wordpress.com/2015/09/02/bitmap-logic-simulator/">realhet</a>, features only 3 building blocks: The wire (any pixel where the R, G or B is &gt;=244), a wire crossing and a not gate.</p>

<p><img src="/assets/blog/help.png" alt="help.png" /></p>

<p>My first goal is to build the bitmap compiler, where “the game” can only load an image of <a href="https://realhet.wordpress.com/2015/09/02/bitmap-logic-simulator/">this CPU made by realhet</a> (which I’ve slightly modified). It will compile the image and run it as fast as possible.</p>

<p>In this blog, I’ll share the development proces of building the bitmap compiler by sharing snippets of code.</p>

<h2 id="about-me">About me</h2>

<p>I usually write web-applications using languages like Javascript, Ruby and Elixir. I’m not a C programmer. This C code may be unconventional or otherwise wrong.</p>

<p>This blog includes incomplete code samples, including a mix of code from header files (<code class="language-plaintext highlighter-rouge">.h</code>) and source files (<code class="language-plaintext highlighter-rouge">.c</code>).</p>

<h2 id="reading-an-image">Reading an image</h2>

<p>To start with, I need to read the image from disk to memory. I’ll be using <code class="language-plaintext highlighter-rouge">libpng</code> to read the bitmap file to a custom <code class="language-plaintext highlighter-rouge">image_data_t</code>-struct.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint8_t</span> <span class="n">r</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="n">g</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="n">b</span><span class="p">;</span>
<span class="p">}</span> <span class="n">rgb_t</span><span class="p">;</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">width</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">height</span><span class="p">;</span>
  <span class="n">rgb_t</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span>
<span class="p">}</span> <span class="n">image_data_t</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p>For those not familiar with C, a struct is simply an object with predefined fields and no methods. <code class="language-plaintext highlighter-rouge">uint8_t</code> and <code class="language-plaintext highlighter-rouge">uint32_t</code> are unsigned (<code class="language-plaintext highlighter-rouge">u</code>) integers (<code class="language-plaintext highlighter-rouge">int</code>) of 8-bits or 32-bits in size. These integer types are defined in <a href="https://cplusplus.com/reference/cstdint/"><code class="language-plaintext highlighter-rouge">stdint.h</code></a></p>
</blockquote>

<p>With the help of Google and ChatGPT, I’ve scrambled together an obsene amount of code for reading an image. Every time I encounter code like this, it’s a reminder how much is actually happening under the hood for a simple task like reading an image. Because this blog isn’t about reading PNG files, I’ve shared the code <a href="https://gist.github.com/tobyhinloopen/c19f3553f81246b8ca48711415b499b4#file-image_data_png-h-L7">here</a>.</p>

<p>I then write a test using a very tiny testing header found <a href="https://gist.github.com/sam159/0849461161e86249f849">here</a>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">image_data_t</span> <span class="n">image_data</span><span class="p">;</span>
<span class="n">image_data_initialize</span><span class="p">(</span><span class="o">&amp;</span><span class="n">image_data</span><span class="p">);</span>

<span class="n">image_data_png_read</span><span class="p">(</span><span class="o">&amp;</span><span class="n">image_data</span><span class="p">,</span> <span class="s">"./assets/fixed-cpu.png"</span><span class="p">);</span>

<span class="n">mu_assert</span><span class="p">(</span><span class="s">"error, image_data.width != 1024"</span><span class="p">,</span> <span class="n">image_data</span><span class="p">.</span><span class="n">width</span> <span class="o">==</span> <span class="mi">1024</span><span class="p">);</span>
<span class="n">mu_assert</span><span class="p">(</span><span class="s">"error, image_data.height != 1024"</span><span class="p">,</span> <span class="n">image_data</span><span class="p">.</span><span class="n">height</span> <span class="o">==</span> <span class="mi">1024</span><span class="p">);</span>
</code></pre></div></div>

<p>Success!</p>

<h2 id="rules-of-the-bitmap-language">Rules of the bitmap language</h2>

<p>Any pixel that has an R, G or B value of &gt;=224 is considered a conductive pixel. Any other pixel is considered non-conductive. Conductive and non-conductive pixels can have any color, it only matters whether they have an R, G or B value of &gt;=224.</p>

<p>For the circuit simulation, a conductive pixel can be powered or unpowered, and it’s powered-state can change at most once per tick. Conductive pixels that touch sides share their “powered”-state. This allows the player to draw “wires”. Powering the wire at any point powers all the pixels in the whole line at once. A line is powered if it has one or more sources of power.</p>

<p>The wire crossing allows crossing 2 independent wires without connecting them. A 3x3 grid containing 4 conductive pixels in a <code class="language-plaintext highlighter-rouge">+</code>-shape is considered a wire crossing. The lines connecting to the upper and lower pixels share state, and the lines connecting to the left and right pixels share state, but otherwise their state is kept separate, because they don’t touch.</p>

<p>A NOT-gate acts as a power source, powering the output wire if the input wire is unpowered. Like the wire crossing, the NOT-gate is a 3x3 grid, but with a <code class="language-plaintext highlighter-rouge">C</code>-shaped input and a single pixel for the output.</p>

<p>The state of the input is independent from the output, but the output will become powered if the input is unpowered. Once the input wire is powered, the NOT-gate will no longer act as a power source and the output wire might become unpowered if there’s no other power source for that wire.</p>

<p>For example, two not-gates with an output connected to a single wire will power the output wire if the input of any not-gate is unpowered. Only if both not-gates are powered, the output wire will be unpowered, basically making a <a href="https://en.wikipedia.org/wiki/NAND_gate">NAND gate</a>. You can connect the outwire wire to another not-gate to make an AND-gate, where the output of that new gate is powered when both input not-gates are powered.</p>

<h2 id="compiling-the-image-finding-wires-not-gates-and-crossings">Compiling the image: Finding wires, NOT-gates and crossings</h2>

<p>The first step of compiling the image will be finding wires, not-gates and crossings. I’ll store these in a struct named <code class="language-plaintext highlighter-rouge">nodemap_t</code>. This struct will be like <code class="language-plaintext highlighter-rouge">image_data_t</code>, but instead of storing a color per pixel, it will store whether a pixel is “nothing”, a conductive wire, the center of a crossing, or the center of some NOT-gate.</p>

<p>For faster processing, I also keep a list of pixel indeces for wire crossings, and pixel indeces for the inputs and outputs for not gates.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">size</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">capacity</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span>
<span class="p">}</span> <span class="n">uint32_list_t</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">uint32_list_add</span><span class="p">(</span><span class="n">uint32_list_t</span> <span class="o">*</span><span class="n">uint32_list</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">value</span><span class="p">);</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">width</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">height</span><span class="p">;</span>

  <span class="c1">// for every not-gate, there will be 2 indeces here, one for the in-index and</span>
  <span class="c1">// one for the out-index.</span>
  <span class="n">uint32_list_t</span> <span class="n">not_gate_index_pairs</span><span class="p">;</span>

  <span class="c1">// pixel-data with 1 element (byte) per pixel. Value is one of</span>
  <span class="c1">// the NODE_* constants.</span>
  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span>
<span class="p">}</span> <span class="n">nodemap_t</span><span class="p">;</span>

<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_NONE</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_WIRE</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_CROSSING</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_NOT_EAST</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_NOT_WEST</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_NOT_NORTH</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">NODE_NOT_SOUTH</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span>

<span class="c1">// Resize nodemap to have a certain width and height, and reallocate to fit</span>
<span class="c1">// that amount of data.</span>
<span class="kt">void</span> <span class="nf">nodemap_resize</span><span class="p">(</span><span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">width</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">height</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">nodemap_put</span><span class="p">(</span><span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">y</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">value</span><span class="p">);</span>

<span class="c1">// Parse wire-nodes and assign not-gates and crossings, populating</span>
<span class="c1">// not_gate_index_pairs.</span>
<span class="kt">void</span> <span class="nf">nodemap_parse</span><span class="p">(</span><span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">);</span>

<span class="n">bool</span> <span class="nf">rgb_is_high</span><span class="p">(</span><span class="n">rgb_t</span> <span class="n">rgb</span><span class="p">);</span>
</code></pre></div></div>

<p>To start compiling the image, I first scan the image data for conductive pixels and write the result to <code class="language-plaintext highlighter-rouge">nodemap_t.data</code> using <code class="language-plaintext highlighter-rouge">nodemap_put</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">RGB_HIGH</span> <span class="o">=</span> <span class="mi">224</span><span class="p">;</span>

<span class="kr">inline</span> <span class="n">bool</span> <span class="nf">rgb_is_high</span><span class="p">(</span><span class="n">rgb_t</span> <span class="n">rgb</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">rgb</span><span class="p">.</span><span class="n">r</span> <span class="o">&gt;=</span> <span class="n">RGB_HIGH</span> <span class="o">||</span> <span class="n">rgb</span><span class="p">.</span><span class="n">g</span> <span class="o">&gt;=</span> <span class="n">RGB_HIGH</span> <span class="o">||</span> <span class="n">rgb</span><span class="p">.</span><span class="n">b</span> <span class="o">&gt;=</span> <span class="n">RGB_HIGH</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Get a pointer to a specific pixel in the image data</span>
<span class="n">rgb_t</span> <span class="o">*</span><span class="nf">image_data_at</span><span class="p">(</span><span class="n">image_data_t</span> <span class="o">*</span><span class="n">image_data</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">y</span><span class="p">);</span>

<span class="kt">void</span> <span class="nf">image_data_to_nodemap</span><span class="p">(</span><span class="n">image_data_t</span> <span class="o">*</span><span class="n">image_data</span><span class="p">,</span> <span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">nodemap_resize</span><span class="p">(</span><span class="n">nodemap</span><span class="p">,</span> <span class="n">image_data</span><span class="o">-&gt;</span><span class="n">width</span><span class="p">,</span> <span class="n">image_data</span><span class="o">-&gt;</span><span class="n">height</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">image_data</span><span class="o">-&gt;</span><span class="n">height</span><span class="p">;</span> <span class="n">y</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">image_data</span><span class="o">-&gt;</span><span class="n">width</span><span class="p">;</span> <span class="n">x</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">nodemap_put</span><span class="p">(</span><span class="n">nodemap</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">rgb_is_high</span><span class="p">(</span><span class="o">*</span><span class="n">image_data_at</span><span class="p">(</span><span class="n">image_data</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)));</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="n">nodemap_parse</span><span class="p">(</span><span class="n">nodemap</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I scan the nodemap wire-data to find not-gates and crossings. A NOT-gate will have the same 4 conductive pixels around the center pixel as the wire crossing, so I can start looking at any location that has 4 conductive pixels around a non-conductive center pixel.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// For each pixel (except all pixels at the edge of the image). EG:</span>
<span class="c1">//    for (y = 1; y &lt; height - 1; y++)</span>
<span class="c1">//      for (x = 1; x &lt; width - 1; x++)</span>
<span class="kt">uint8_t</span> <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span> <span class="o">+</span> <span class="n">index</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">is_wire</span><span class="p">(</span><span class="n">self</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>

<span class="kt">uint8_t</span> <span class="o">*</span><span class="n">yn</span> <span class="o">=</span> <span class="n">self</span> <span class="o">-</span> <span class="n">width</span><span class="p">;</span>
<span class="kt">uint8_t</span> <span class="o">*</span><span class="n">yp</span> <span class="o">=</span> <span class="n">self</span> <span class="o">+</span> <span class="n">width</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">is_wire</span><span class="p">(</span><span class="n">yn</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="n">is_wire</span><span class="p">(</span><span class="n">yp</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="n">is_wire</span><span class="p">(</span><span class="n">self</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">||</span>
    <span class="o">!</span><span class="n">is_wire</span><span class="p">(</span><span class="n">self</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
  <span class="k">continue</span><span class="p">;</span>

<span class="c1">// these 4 variables are named based on whether they're relative to the</span>
<span class="c1">// YX of self or index: n = -1, p = +1. So pn is a y+1, x-1.</span>
<span class="n">bool</span> <span class="n">nn</span> <span class="o">=</span> <span class="n">is_wire</span><span class="p">(</span><span class="n">yn</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">bool</span> <span class="n">np</span> <span class="o">=</span> <span class="n">is_wire</span><span class="p">(</span><span class="n">yn</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">bool</span> <span class="n">pn</span> <span class="o">=</span> <span class="n">is_wire</span><span class="p">(</span><span class="n">yp</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">bool</span> <span class="n">pp</span> <span class="o">=</span> <span class="n">is_wire</span><span class="p">(</span><span class="n">yp</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">nn</span> <span class="o">&amp;&amp;</span> <span class="n">np</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pp</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Write nodemap-&gt;data[index] with the node-type (not-gate pointing south)</span>
  <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">NODE_NOT_SOUTH</span><span class="p">;</span>

  <span class="c1">// Add not-gate input and output index to not_gate_index_pairs</span>
  <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">not_gate_index_pairs</span><span class="p">,</span> <span class="n">index</span> <span class="o">-</span> <span class="n">width</span><span class="p">,</span> <span class="n">index</span> <span class="o">+</span> <span class="n">width</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">pn</span> <span class="o">&amp;&amp;</span> <span class="n">pp</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">nn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">np</span><span class="p">)</span> <span class="p">{</span>
  <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">NODE_NOT_NORTH</span><span class="p">;</span>
  <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">not_gate_index_pairs</span><span class="p">,</span> <span class="n">index</span> <span class="o">+</span> <span class="n">width</span><span class="p">,</span> <span class="n">index</span> <span class="o">-</span> <span class="n">width</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">nn</span> <span class="o">&amp;&amp;</span> <span class="n">pn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">np</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pp</span><span class="p">)</span> <span class="p">{</span>
  <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">NODE_NOT_EAST</span><span class="p">;</span>
  <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">not_gate_index_pairs</span><span class="p">,</span> <span class="n">index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">np</span> <span class="o">&amp;&amp;</span> <span class="n">pp</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">nn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pn</span><span class="p">)</span> <span class="p">{</span>
  <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">NODE_NOT_WEST</span><span class="p">;</span>
  <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">not_gate_index_pairs</span><span class="p">,</span> <span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="o">!</span><span class="n">nn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">np</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pn</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">pp</span><span class="p">)</span> <span class="p">{</span>
  <span class="o">*</span><span class="n">self</span> <span class="o">=</span> <span class="n">NODE_CROSSING</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next comes the hard part: Determining which pixels belong to the same wire.</p>

<h2 id="mapping-pixels-to-wire-ids">Mapping pixels to wire IDs.</h2>

<p>Next, I need to combine conductive pixels that share the same wire somehow. The plan is to assign each wire a unique ID, and give all pixels making a wire that same ID.</p>

<p>Like before, I’m storing the result in an image-like struct.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">width</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">height</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">max_id</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">data</span><span class="p">;</span>
<span class="p">}</span> <span class="n">wiremap_t</span><span class="p">;</span>
</code></pre></div></div>

<p>In this struct, every pixel stores the ID of the wire. If the ID is 0, it is part of no wire. <code class="language-plaintext highlighter-rouge">max_id</code> will store the highest wire-id. I’m going to loop through every pixel, and for every unassigned conductive pixel I find, I’m going to assign it a new ID. Then, all adjecent conductive pixels will be given the same ID.</p>

<p>I’m going to use a <a href="https://en.wikipedia.org/wiki/Flood_fill">flood fill algorithm</a> to find all adjecent pixels. I’m going to write a very simple algorithm where I basically walk through every adjecent pixel to see if the pixel is conductive. If I encounter a crossing, I jump it by advancing an extra pixel (2 pixels)</p>

<p>For testing, I’m going to use this slightly more complicated test image. For clarification, the expected result is shown as a colored image where each color represents a wire id.</p>

<p><img src="/assets/blog/test-8.png" alt="test-8.png" /> <img src="/assets/blog/test-8-colored.png" alt="test-8-colored.png" /></p>

<p>Let’s write the flood fill!</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">_fill</span><span class="p">(</span><span class="k">const</span> <span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">,</span> <span class="n">wiremap_t</span> <span class="o">*</span><span class="n">wiremap</span><span class="p">,</span>
                  <span class="n">uint32_list_t</span> <span class="o">*</span><span class="n">stack</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">x</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">y</span><span class="p">,</span>
                  <span class="kt">uint32_t</span> <span class="n">next_id</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">width</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">width</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">height</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">height</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">width_minus_one</span> <span class="o">=</span> <span class="n">width</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">height_minus_one</span> <span class="o">=</span> <span class="n">height</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>

  <span class="n">uint32_list_clear</span><span class="p">(</span><span class="n">stack</span><span class="p">);</span>
  <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
  <span class="k">while</span> <span class="p">(</span><span class="n">stack</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">y</span> <span class="o">=</span> <span class="n">uint32_list_pop</span><span class="p">(</span><span class="n">stack</span><span class="p">);</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">uint32_list_pop</span><span class="p">(</span><span class="n">stack</span><span class="p">);</span>

    <span class="kt">uint32_t</span> <span class="n">index</span> <span class="o">=</span> <span class="n">y</span> <span class="o">*</span> <span class="n">width</span> <span class="o">+</span> <span class="n">x</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">!=</span> <span class="n">NODE_WIRE</span> <span class="o">||</span> <span class="n">wiremap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span><span class="p">])</span> <span class="k">continue</span><span class="p">;</span>

    <span class="n">wiremap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="n">next_id</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">x</span> <span class="o">&lt;</span> <span class="n">width_minus_one</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">uint8_t</span> <span class="n">node</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_WIRE</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_CROSSING</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">x</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">uint8_t</span> <span class="n">node</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_WIRE</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_CROSSING</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span> <span class="o">-</span> <span class="mi">2</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">y</span> <span class="o">&lt;</span> <span class="n">height_minus_one</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">uint8_t</span> <span class="n">node</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="n">width</span><span class="p">];</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_WIRE</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_CROSSING</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">y</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">uint8_t</span> <span class="n">node</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">index</span> <span class="o">-</span> <span class="n">width</span><span class="p">];</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_WIRE</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">node</span> <span class="o">==</span> <span class="n">NODE_CROSSING</span><span class="p">)</span>
        <span class="n">uint32_list_add2</span><span class="p">(</span><span class="n">stack</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">-</span> <span class="mi">2</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">_fill</code> is called for every XY coordinate that has a conductive pixel and is not yet assigned an ID. <code class="language-plaintext highlighter-rouge">next_id</code> is an unused ID to be assigned to the current pixel, and any adjecent conductive pixel.</p>

<p><code class="language-plaintext highlighter-rouge">stack</code> is just an array where I keep track of pixels that need to be visited. I pass it as a param to reuse any allocated memory. Clearing the stack using <code class="language-plaintext highlighter-rouge">uint32_list_clear</code> doesn’t free memory, it just sets the <code class="language-plaintext highlighter-rouge">size</code> to <code class="language-plaintext highlighter-rouge">0</code>.</p>

<p>I’m sure it isn’t the most efficient implementation, but it works!</p>

<p>Now the wires have assigned IDs, I can combine the wire IDs with the NOT-gates.</p>

<h2 id="creating-not-gates-list">Creating NOT-gates list</h2>

<p>For the simulation, the state of a wire is determined by NOT-gates it is connected to. A wire is powered if there is any NOT-gate with an unpowered input.</p>

<p>Therefore, the only thing the simulation needs to be aware of, is the list of NOT-gates attached to a wire, and the powered/unpowered state of a wire.</p>

<p>The powered-state of a wire can simply be an array of booleans. To advance the simulation, I need to iterate through all NOT-gates and change the output of each NOT-gate based on the powered-state of the input wire.</p>

<p>To start with implementing this, I need a list of NOT-gates with the ID of their input and output wires. I’m also going to store the position of each NOT-gate output and input pixel as the index.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">in_id</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">out_id</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">in_index</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">out_index</span><span class="p">;</span>
<span class="p">}</span> <span class="n">not_gate_t</span><span class="p">;</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">size</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">capacity</span><span class="p">;</span>
  <span class="n">not_gate_t</span> <span class="o">*</span><span class="n">not_gates</span><span class="p">;</span>
<span class="p">}</span> <span class="n">not_gate_list_t</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">not_gate_list_initialize</span><span class="p">(</span><span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">not_gate_list_clear</span><span class="p">(</span><span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">not_gate_list_ensure_capacity</span><span class="p">(</span><span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">,</span>
                                   <span class="kt">uint32_t</span> <span class="n">required_capacity</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">not_gate_list_add</span><span class="p">(</span><span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">,</span> <span class="n">not_gate_t</span> <span class="n">not_gate</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">not_gate_list_free</span><span class="p">(</span><span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">);</span>
</code></pre></div></div>

<p>To populate the <code class="language-plaintext highlighter-rouge">not_gate_list</code>, I need the list of NOT-gates and the IDs of wires per pixel. The list of NOT-gates is stored in <code class="language-plaintext highlighter-rouge">nodemap</code>’s <code class="language-plaintext highlighter-rouge">not_gate_index_pairs</code>. The IDs of wires is stored in the <code class="language-plaintext highlighter-rouge">wiremap</code>.</p>

<p>I’m going to loop through <code class="language-plaintext highlighter-rouge">not_gate_index_pairs</code> and create a <code class="language-plaintext highlighter-rouge">not_gate_t</code> for each pair.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">nodemap_wiremap_to_not_gate_list</span><span class="p">(</span><span class="k">const</span> <span class="n">nodemap_t</span> <span class="o">*</span><span class="n">nodemap</span><span class="p">,</span>
                                      <span class="k">const</span> <span class="n">wiremap_t</span> <span class="o">*</span><span class="n">wiremap</span><span class="p">,</span>
                                      <span class="n">not_gate_list_t</span> <span class="o">*</span><span class="n">not_gate_list</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">size</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">not_gate_index_pairs</span><span class="p">.</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">not_gate_list_clear</span><span class="p">(</span><span class="n">not_gate_list</span><span class="p">);</span>
  <span class="n">not_gate_list_ensure_capacity</span><span class="p">(</span><span class="n">not_gate_list</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span>
  <span class="k">const</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">pair_data</span> <span class="o">=</span> <span class="n">nodemap</span><span class="o">-&gt;</span><span class="n">not_gate_index_pairs</span><span class="p">.</span><span class="n">data</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">not_gate_t</span> <span class="n">not_gate</span><span class="p">;</span>
    <span class="n">not_gate</span><span class="p">.</span><span class="n">in_index</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">pair_data</span><span class="o">++</span><span class="p">);</span>
    <span class="n">not_gate</span><span class="p">.</span><span class="n">in_id</span> <span class="o">=</span> <span class="n">wiremap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">not_gate</span><span class="p">.</span><span class="n">in_index</span><span class="p">];</span>
    <span class="n">not_gate</span><span class="p">.</span><span class="n">out_index</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">pair_data</span><span class="o">++</span><span class="p">);</span>
    <span class="n">not_gate</span><span class="p">.</span><span class="n">out_id</span> <span class="o">=</span> <span class="n">wiremap</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">[</span><span class="n">not_gate</span><span class="p">.</span><span class="n">out_index</span><span class="p">];</span>
    <span class="n">not_gate_list_add</span><span class="p">(</span><span class="n">not_gate_list</span><span class="p">,</span> <span class="n">not_gate</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This list is the last thing I need to finish the compile proces. Now I can use the <code class="language-plaintext highlighter-rouge">not_gate_list</code> to create a program.</p>

<h2 id="creating--running-a-program">Creating &amp; Running a program</h2>

<p>Loading the program involves clearing any existing program, loading the NOT-gates, randomizing the order of NOT-gates, and making sure the state is initialized (all zeroed)</p>

<p>For efficiency, the not-gates input IDs and output IDs are copied to their own arrays.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="kt">uint32_t</span> <span class="n">gate_io_t</span><span class="p">;</span>
<span class="k">typedef</span> <span class="kt">uint8_t</span> <span class="n">gate_state_t</span><span class="p">;</span>
<span class="k">typedef</span> <span class="k">enum</span> <span class="n">fixed_state</span> <span class="p">{</span>
  <span class="n">FIXED_NONE</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
  <span class="n">FIXED_LOW</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
  <span class="n">FIXED_HIGH</span> <span class="o">=</span> <span class="mi">2</span>
<span class="p">}</span> <span class="n">fixed_state_t</span><span class="p">;</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="n">program_state</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">id_state_size</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">id_state_capacity</span><span class="p">;</span>
  <span class="n">bool</span> <span class="o">*</span><span class="n">id_state</span><span class="p">;</span>

  <span class="c1">// The fixed-state status per ID. A fixed state forces the value to either</span>
  <span class="c1">// FALSE or TRUE of a state at the end of every iteration. If the fixed-state</span>
  <span class="c1">// is 0, no state is enforced.</span>
  <span class="n">fixed_state_t</span> <span class="o">*</span><span class="n">fixed_state_map</span><span class="p">;</span>

  <span class="c1">// Number of high src_gates per ID. If &gt;0, the id_state should be true.</span>
  <span class="c1">// fixed_state can override the final value.</span>
  <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">high_count</span><span class="p">;</span>

  <span class="c1">// NotGate sized arrays</span>
  <span class="kt">uint32_t</span> <span class="n">not_gate_size</span><span class="p">;</span>
  <span class="kt">uint32_t</span> <span class="n">not_gate_capacity</span><span class="p">;</span>

  <span class="n">not_gate_t</span> <span class="o">*</span><span class="n">not_gates</span><span class="p">;</span>
  <span class="n">gate_state_t</span> <span class="o">*</span><span class="n">gate_state</span><span class="p">;</span>
  <span class="kt">float</span> <span class="o">*</span><span class="n">slow_state</span><span class="p">;</span>
  <span class="n">gate_io_t</span> <span class="o">*</span><span class="n">in_ids</span><span class="p">;</span>
  <span class="n">gate_io_t</span> <span class="o">*</span><span class="n">out_ids</span><span class="p">;</span>

  <span class="c1">// A set of all state-ids that are affected by fixed_state. Basically, this</span>
  <span class="c1">// set includes all non-zero IDs in fixed_state_map.</span>
  <span class="n">uint32_hash_set_t</span> <span class="n">fixed_state_ids</span><span class="p">;</span>

  <span class="c1">// Seed for random. This value can change any time as random values are</span>
  <span class="c1">// created.</span>
  <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">random_seed</span><span class="p">;</span>
<span class="p">}</span> <span class="n">program_state_t</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">program_state_next</span><span class="p">(</span><span class="n">program_state_t</span> <span class="o">*</span><span class="n">program_state</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">not_gate_size</span> <span class="o">=</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">not_gate_size</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">not_gate_size</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
    <span class="n">_update_gate_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="o">!</span><span class="n">_get_gate_input</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">i</span><span class="p">));</span>
  <span class="n">_force_fixed_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Running the program involves checking the state of the input wire of each gate and changing the output if the input is changed. The input is checked using <code class="language-plaintext highlighter-rouge">_get_gate_input</code>, and the output is updated using <code class="language-plaintext highlighter-rouge">_update_gate_state</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">gate_state_t</span> <span class="nf">_get_gate_input</span><span class="p">(</span><span class="k">const</span> <span class="n">program_state_t</span> <span class="o">*</span><span class="n">program_state</span><span class="p">,</span>
                                    <span class="kt">uint32_t</span> <span class="n">index</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">in_id</span> <span class="o">=</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">in_ids</span><span class="p">[</span><span class="n">index</span><span class="p">];</span>
  <span class="kt">uint8_t</span> <span class="n">fixed_state</span> <span class="o">=</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">fixed_state_map</span><span class="p">[</span><span class="n">in_id</span><span class="p">];</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">fixed_state</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">fixed_state</span> <span class="o">==</span> <span class="n">FIXED_HIGH</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">high_count</span><span class="p">[</span><span class="n">in_id</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">_get_gate_input</code> is a function that should return the current state of the input wire for a gate.</p>

<p>If the state of the input wire is fixed by <code class="language-plaintext highlighter-rouge">fixed_state</code>, that state is used for the input wire. Otherwise, <code class="language-plaintext highlighter-rouge">high_count</code> is used to determine the number of gates with “high” (powered) outputs per wire. If at least one gate powers the output wire, the wire is powered and <code class="language-plaintext highlighter-rouge">get_gate_input</code> returns <code class="language-plaintext highlighter-rouge">true</code>.</p>

<p>The target state of a gate will be the opposite of the input; If the gate input was <code class="language-plaintext highlighter-rouge">false</code>, the <code class="language-plaintext highlighter-rouge">target_state</code> will be <code class="language-plaintext highlighter-rouge">true</code>, and vice-versa. <code class="language-plaintext highlighter-rouge">_update_gate_state</code> is called with the inverse of <code class="language-plaintext highlighter-rouge">_get_gate_input</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">_update_gate_state</span><span class="p">(</span><span class="n">program_state_t</span> <span class="o">*</span><span class="n">program_state</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">index</span><span class="p">,</span>
                               <span class="n">gate_state_t</span> <span class="n">target_state</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">gate_state_t</span> <span class="o">*</span><span class="n">gate_state</span> <span class="o">=</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">gate_state</span><span class="p">;</span>
  <span class="kt">float</span> <span class="o">*</span><span class="n">slow_state</span> <span class="o">=</span> <span class="n">program_state</span><span class="o">-&gt;</span><span class="n">slow_state</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">target_state</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">gate_state</span><span class="p">[</span><span class="n">index</span><span class="p">])</span> <span class="k">return</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">_set_gate_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">+=</span>
        <span class="n">random_range_f</span><span class="p">(</span><span class="o">&amp;</span><span class="n">program_state</span><span class="o">-&gt;</span><span class="n">random_seed</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">)</span> <span class="n">_set_gate_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">gate_state</span><span class="p">[</span><span class="n">index</span><span class="p">])</span> <span class="k">return</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">_set_gate_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">-=</span>
        <span class="n">random_range_f</span><span class="p">(</span><span class="o">&amp;</span><span class="n">program_state</span><span class="o">-&gt;</span><span class="n">random_seed</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">slow_state</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="p">)</span> <span class="n">_set_gate_state</span><span class="p">(</span><span class="n">program_state</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">_update_gate_state</code> checks if the target state is different from the current state, and will change <code class="language-plaintext highlighter-rouge">slow_state</code> by some random amount. If the <code class="language-plaintext highlighter-rouge">slow_state</code> exceeds the <code class="language-plaintext highlighter-rouge">0..1</code> range, the state-change will be confirmed using <code class="language-plaintext highlighter-rouge">_set_gate_state</code>.</p>

<p>The randomness allows the system to end up in a stable state, as the gates will not update simultaneously.</p>

<p>You can play around with (an outdated copy) of the simulation here: <a href="https://charperbonaroo.github.io/bls/#0">https://charperbonaroo.github.io/bls/#0</a></p>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[Compiling and simulating a circuit bitmap]]></summary></entry><entry><title type="html">Unity game development: iPad touch controls</title><link href="https://bonaroo.nl/2020/09/03/unity-touch-controls.html" rel="alternate" type="text/html" title="Unity game development: iPad touch controls" /><published>2020-09-03T00:00:00+00:00</published><updated>2020-09-03T00:00:00+00:00</updated><id>https://bonaroo.nl/2020/09/03/unity-touch-controls</id><content type="html" xml:base="https://bonaroo.nl/2020/09/03/unity-touch-controls.html"><![CDATA[<style>
.center-image, .right-image {
  max-width: 480px;
}

.center-video, .center-image, .right-image {
  margin: 1em auto;
  text-align: center;
}

.center-image img, .right-image img {
  margin: 0;
  width: 100%;
}

.center-video {
  max-width: 720px;
  position: relative;
}

.center-video video {
  display: block;
  width: 100%;
  height: 75%;
}

.center-image em, .right-image em, .center-video em {
  display: block;
  font-size: 90%;
}

.right-image {
  float: right;
}

.text {
  column-count: 1;
  column-rule: 1px solid #aaa;
  column-gap: 40px;
}
</style>

<h1 id="adding-touch-support-to-a-unity-project">Adding touch support to a Unity project</h1>

<div class="text">

  <p>I’m working on a small prototype game where you build a Spa Resort. In this game, you’re building saunas and baths for peeps (virtual people) to visit. The idea is to increase their happiness by introducing them to temperature differences.</p>

  <div class="center-image">
  <img src="/assets/blog/peep-enjoying-sauna-8b.png" alt="Peep enjoying a sauna" />
  <em>Peep enjoying a sauna</em>
</div>

  <p>In the current prototype, you can put down walls, heaters and chillers. The walls isolate and prevent peeps from moving through them. The heaters and chillers control temperature.</p>

  <p>I use WSAD to control the camera, and I use the mouse to put down walls, heaters and chillers. The game even runs on my iPad, but this is just silly.</p>

  <div class="center-image">
  <img src="/assets/blog/ipad-with-keyboard-and-mouse.jpg" alt="iPad with keyboard and trackpad" />
  <em>I'm a computer now</em>
</div>

  <h2 id="mouse--keyboard-support">Mouse &amp; Keyboard support</h2>

  <p>My game uses <code class="language-plaintext highlighter-rouge">Input.GetMouseButtonDown(0)</code> combined with <code class="language-plaintext highlighter-rouge">Input.mousePosition</code> to listen for mouse clicks. When the player clicks somewhere, some action is performed. Using a <strong>Unity Toggle Group</strong>, the player can choose what action is performed on clicking the mouse.</p>

  <p><img src="/assets/blog/toggle-group.png" alt="Toggle Group" style="float: right" width="160" height="102" /></p>

  <p>Since I can place down walls, chillers and heaters using the trackpad or by tapping the screen, it seems that Unity fakes a mouse click when the player taps the screen.</p>

  <p>Using the trackpad to move the iPad cursor does not notify my game about mouse movement: Only when tapping the screen, <code class="language-plaintext highlighter-rouge">Input.mousePosition</code> gets updated.</p>

  <p>I also notice tapping on my UI causes walls to be built below it. I use <code class="language-plaintext highlighter-rouge">EventSystem.current.IsPointerOverGameObject()</code> to ignore clicks on the UI, but this seems to be not working for touch events.</p>

  <p>Adding the following <strong>MonoBehaviour</strong> to my game confirms my suspicions.</p>

  <h4 id="inputloggingbehaviourcs">InputLoggingBehaviour.cs</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.EventSystems</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">InputLoggingBehaviour</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
  <span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="n">Debug</span><span class="p">.</span><span class="n">unityLogger</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"Input"</span><span class="p">,</span> <span class="n">Input</span><span class="p">.</span><span class="n">mousePosition</span><span class="p">);</span>
      <span class="n">Debug</span><span class="p">.</span><span class="n">unityLogger</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"Input"</span><span class="p">,</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">));</span>
      <span class="n">Debug</span><span class="p">.</span><span class="n">unityLogger</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">"Input"</span><span class="p">,</span> <span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">());</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <h4 id="ipad-log-after-tapping-screen-only-lines-matching-input">iPad log after tapping screen (only lines matching Input)</h4>
  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input: (81.0, 63.0, 0.0)
Input: True
Input: False
</code></pre></div>  </div>

  <p><code class="language-plaintext highlighter-rouge">IsPointerOverGameObject</code> does return <code class="language-plaintext highlighter-rouge">True</code> on my macbook, so I think it only works with a real mouse.</p>

  <p>For camera movement, I’m using Unity’s <code class="language-plaintext highlighter-rouge">Input.GetAxis("Horizontal")</code> and <code class="language-plaintext highlighter-rouge">Input.GetAxis("Vertical")</code>. This way I can move the camera using WSAD. This obviously is useless on an iPad, so I have to add some way to move the camera using touch controls.</p>

  <h2 id="touch-controls">Touch controls</h2>

  <p>Before I get started, this is the code I’m starting with.</p>

  <h4 id="surfacepointerbehaviourcs">SurfacePointerBehaviour.cs</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.EventSystems</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SurfacePointerBehaviour</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
  <span class="k">public</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetPointClicked</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
      <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">())</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="nf">GetSurfacePositionFromScreenPosition</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">mousePosition</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="n">Ray</span> <span class="n">ray</span><span class="p">;</span>
  <span class="k">private</span> <span class="n">Plane</span> <span class="n">plane</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Plane</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">up</span><span class="p">,</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">);</span>
  <span class="k">private</span> <span class="kt">float</span> <span class="n">distance</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>

  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetSurfacePositionFromScreenPosition</span><span class="p">(</span><span class="n">Vector3</span> <span class="n">screenPosition</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">ray</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="nf">ScreenPointToRay</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">plane</span><span class="p">.</span><span class="nf">Raycast</span><span class="p">(</span><span class="n">ray</span><span class="p">,</span> <span class="k">out</span> <span class="n">distance</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="kt">var</span> <span class="n">point</span> <span class="p">=</span> <span class="n">ray</span><span class="p">.</span><span class="nf">GetPoint</span><span class="p">(</span><span class="n">distance</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">x</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">FloorToInt</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">x</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">y</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">FloorToInt</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">z</span><span class="p">);</span>
      <span class="k">return</span> <span class="k">new</span> <span class="nf">Vector2Int</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>Basically, if any script is interested in knowing whether the player clicked some point on the surface (aka terrain, ground, floor), it uses <code class="language-plaintext highlighter-rouge">GetPointClicked()</code> to get the point where the player clicked on the surface.</p>

  <p>If the player clicked the GUI or didn’t click anything last frame, <code class="language-plaintext highlighter-rouge">GetPointClicked</code> should return <code class="language-plaintext highlighter-rouge">null</code>. I need to patch <code class="language-plaintext highlighter-rouge">GetPointClicked</code> to listen for touches as well.</p>

  <p>To get started, I first disable <code class="language-plaintext highlighter-rouge">Input.simulateMouseWithTouches</code>. This way I can leave all my mouse-related event handlers the way they are and write alternative handlers for touch events.</p>

  <p>For now, I’m going to implement it in a way that <code class="language-plaintext highlighter-rouge">GetPointClicked</code> returns the point of the first finger touching the screen, and only the first time the finger is pressed on the screen, like <code class="language-plaintext highlighter-rouge">GetPointClicked</code> only returns the first time the mouse button is pressed down.</p>

  <p>According to the <a href="https://docs.unity3d.com/ScriptReference/TouchPhase.html">TouchPhase documentation</a>, the initial point of contact has <code class="language-plaintext highlighter-rouge">phase = Began</code>, and a finger being lifted from screen has <code class="language-plaintext highlighter-rouge">phase = Ended</code>.</p>

  <h4 id="surfacepointerbehaviourcs-1">SurfacePointerBehaviour.cs</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
<span class="p">{</span>
  <span class="n">Input</span><span class="p">.</span><span class="n">simulateMouseWithTouches</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetPointClicked</span><span class="p">()</span>
<span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
    <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">())</span>
  <span class="p">{</span>
    <span class="k">return</span> <span class="nf">GetSurfacePositionFromScreenPosition</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">mousePosition</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
      <span class="kt">var</span> <span class="n">touch</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetTouch</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Began</span>
        <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
      <span class="p">{</span>
        <span class="k">return</span> <span class="nf">GetSurfacePositionFromScreenPosition</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>By passing <code class="language-plaintext highlighter-rouge">touch.fingerId</code> to <code class="language-plaintext highlighter-rouge">IsPointerOverGameObject</code>, <code class="language-plaintext highlighter-rouge">IsPointerOverGameObject</code> correctly returns whether this pointer (the finger) is touching the GUI or not.</p>

  <p>Now, the first touch not touching the GUI is returned by <code class="language-plaintext highlighter-rouge">GetPointClicked</code>. This already fixes being able to put down walls, heaters and chillers. Panning the camera remains.</p>

  <h2 id="drag-to-move-camera">Drag to move camera</h2>

  <p>To pan (move) the camera, I’ll allow the player to drag one finger on screen to move the camera. This already poses a problem: Currently, touching the screen instantly triggers a “click”, while we don’t know if the player intented to tap to interact or drag to move camera.</p>

  <p>For this reason, we need to wait before the player either releases or moves their finger. This is easily changed by using <code class="language-plaintext highlighter-rouge">TouchPhase.Ended</code> instead of <code class="language-plaintext highlighter-rouge">TouchPhase.Began</code>.</p>

  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">touch</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetTouch</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Ended</span>
  <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
<span class="p">{</span>
  <span class="k">return</span> <span class="nf">GetSurfacePositionFromScreenPosition</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>Next, I want to move the camera based on the finger’s movement. For each touch, the movement since the last frame is accessible through <code class="language-plaintext highlighter-rouge">touch.deltaPosition</code>. Let’s first record the start position.</p>

  <p>I want to ignore all touches that began on the UI, so I use <code class="language-plaintext highlighter-rouge">IsPointerOverGameObject</code> to ignore these. Because I won’t know what finger will be moving or tapping, I’m recording all fingers in a <code class="language-plaintext highlighter-rouge">Dictionary</code>, using the <code class="language-plaintext highlighter-rouge">fingerId</code> as a key.</p>

  <p>Because I suspect I’ll be writing quite some lines of code handling touch events, I’ll be moving these to a separate method called <code class="language-plaintext highlighter-rouge">HandleTouch</code>. This <code class="language-plaintext highlighter-rouge">HandleTouch</code> function will be invoked every touch.</p>

  <p>Because I don’t want the logic to be invoked again for every script using <code class="language-plaintext highlighter-rouge">GetPointClicked</code>, I’m moving the logic to the <code class="language-plaintext highlighter-rouge">Update</code> call and storing the results in private variables, one of which is returned by <code class="language-plaintext highlighter-rouge">GetPointClicked</code>.</p>

  <p>I also noticed <code class="language-plaintext highlighter-rouge">Input.mousePosition</code> returns a <code class="language-plaintext highlighter-rouge">Vector3</code> while <code class="language-plaintext highlighter-rouge">touch.position</code> returns a <code class="language-plaintext highlighter-rouge">Vector2</code>. Cool, I suppose. I changed <code class="language-plaintext highlighter-rouge">GetSurfacePositionFromScreenPosition</code> to accept a <code class="language-plaintext highlighter-rouge">Vector2</code> and reduced it’s method name to <code class="language-plaintext highlighter-rouge">GetSurfacePosition</code> because I think the old name was too long.</p>

  <p>The intermediate result is as follows:</p>

  <h4 id="surfacepointerbehaviourcs-partial">SurfacePointerBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.EventSystems</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SurfacePointerBehaviour</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
  <span class="k">private</span> <span class="k">readonly</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Vector2</span><span class="p">&gt;</span> <span class="n">fingerStartPositions</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Vector2</span><span class="p">&gt;();</span>
  <span class="k">private</span> <span class="k">readonly</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Vector2</span><span class="p">&gt;</span> <span class="n">fingerCurrentPositions</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Vector2</span><span class="p">&gt;();</span>
  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

  <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
  <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

  <span class="k">public</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetPointClicked</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">mouseClickPosition</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="n">mouseClickPosition</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">fingerTapPosition</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="n">fingerTapPosition</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="nf">UpdateMouseClickPosition</span><span class="p">();</span>
    <span class="nf">UpdateFingerTapPosition</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">UpdateMouseClickPosition</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
      <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">())</span>
    <span class="p">{</span>
      <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">mousePosition</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
      <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">UpdateFingerTapPosition</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
      <span class="p">{</span>
        <span class="nf">HandleTouch</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetTouch</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">HandleTouch</span><span class="p">(</span><span class="n">Touch</span> <span class="n">touch</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Began</span>
      <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="n">fingerStartPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
      <span class="n">fingerCurrentPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">fingerStartPositions</span><span class="p">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="n">fingerCurrentPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Moved</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="c1">// ???</span>
      <span class="p">}</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Ended</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="n">fingerStartPositions</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
        <span class="n">fingerCurrentPositions</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
        <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="n">Ray</span> <span class="n">ray</span><span class="p">;</span>
  <span class="k">private</span> <span class="k">readonly</span> <span class="n">Plane</span> <span class="n">plane</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Plane</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">up</span><span class="p">,</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">);</span>
  <span class="k">private</span> <span class="kt">float</span> <span class="n">distance</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>

  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">screenPosition</span><span class="p">)</span>
  <span class="p">{</span> <span class="cm">/* */</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>So basically I’ve refactored the class so it’s easier to modify. So far, I should have changed nothing functionally. Now let’s distinguish dragging from tapping.</p>

  <p>The distinction between a tap and a drag is the movement and duration. If a player moves their finger for more than, lets say, half-a-second and (or?) more than a finger’s width of pixels, I’m pretty sure the player meant to drag something instead of tapping something.</p>

  <p>Without thinking about the actual duration or distance (or both), let’s make it configurable and track both. When we decide a player’s finger is dragging and not tapping, I switch some boolean for that finger.</p>

  <p>I can already determine the distance of a finger by calculating the distance between a finger’s <code class="language-plaintext highlighter-rouge">fingerCurrentPositions</code> and <code class="language-plaintext highlighter-rouge">fingerStartPositions</code>. Let’s similarly track time. I only need to track time from the start of the touch, since I can calculate the duration of a touch by comparing the start time with the current time.</p>

  <p>I keep track of the start time in a dictionary called <code class="language-plaintext highlighter-rouge">fingerStartTimes</code>. When I determined a finger is dragging, I’ll add it to a set of ints called <code class="language-plaintext highlighter-rouge">draggingFingers</code>. I’ll keep the tresholds, called <code class="language-plaintext highlighter-rouge">dragMinimumDuration</code> and <code class="language-plaintext highlighter-rouge">dragMinimumDistance</code>, in public properties so they can be easily tweaked.</p>

  <p>Basically, when a finger is moved for at least <code class="language-plaintext highlighter-rouge">dragMinimumDistance</code> and <code class="language-plaintext highlighter-rouge">dragMinimumDuration</code>, the finger is added to <code class="language-plaintext highlighter-rouge">draggingFingers</code>. From this point forward, the first dragging finger is being used to control the camera.</p>

  <p>“The first” is also kept in a variable, called <code class="language-plaintext highlighter-rouge">primaryDraggingFingerId</code>, and can only be set when no other finger is dragging. This variable is cleared when that finger is released.</p>

  <p>This way, any other dragging fingers are ignored until all dragging fingers are released. Taps can still occur.</p>

  <h4 id="surfacepointerbehaviourcs-partial-1">SurfacePointerBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">HandleTouch</span><span class="p">(</span><span class="n">Touch</span> <span class="n">touch</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Began</span>
    <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
  <span class="p">{</span>
    <span class="n">fingerStartPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="n">fingerCurrentPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="n">fingerStartTimes</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">Time</span><span class="p">.</span><span class="n">time</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">fingerStartPositions</span><span class="p">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
  <span class="p">{</span>
    <span class="n">fingerCurrentPositions</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Moved</span> <span class="p">||</span> <span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Stationary</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nf">GetTouchDuration</span><span class="p">(</span><span class="n">touch</span><span class="p">)</span> <span class="p">&gt;=</span> <span class="n">dragMinimumDuration</span>
      <span class="p">&amp;&amp;</span> <span class="nf">GetTouchDistance</span><span class="p">(</span><span class="n">touch</span><span class="p">)</span> <span class="p">&gt;=</span> <span class="n">dragMinimumDistance</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="n">draggingFingers</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">primaryDraggingFingerId</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
          <span class="n">primaryDraggingFingerId</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Ended</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="n">fingerStartPositions</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
      <span class="n">fingerCurrentPositions</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
      <span class="n">fingerStartTimes</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">draggingFingers</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
      <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">primaryDraggingFingerId</span> <span class="p">==</span> <span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">)</span>
        <span class="p">{</span>
          <span class="n">primaryDraggingFingerId</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">else</span>
      <span class="p">{</span>
        <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <div class="right-image">
  <img src="/assets/blog/perspective-rects-32c.png" alt="2 equal sized blocks as seen by a perspective camera" />
  <em>Equal sized objects as seen by a perspective camera.</em>
</div>

  <p>I control my camera from a separate behaviour. I only want to pass minimum data between my <code class="language-plaintext highlighter-rouge">SurfacePointerBehaviour</code> and my camera behaviour, called <code class="language-plaintext highlighter-rouge">TopDownCameraRigBehaviour</code>.</p>

  <p>I want to make the dragging feel like you’re dragging the surface. The surface position you started the drag at must remain at your finger. For 2D games, this isn’t hard, because the distance on the surface is always linearly resolves to some distance on screen. However, for a 3D perspective camera, this changes based on the distance of the camera from the surface.</p>

  <p>The solution I’m thinking of is to expose the worldspace difference between the current finger position and the desired finger position.</p>

  <p>The worldspace distance is the distance in-game. The desired finger position is the point on in-game surface the finger started dragging. The current finger position is where the finger is currently touching the surface.</p>

  <p>The idea is that the exposed difference is always at least somewhat in the right direction and amplitude, until the camera is moved to the correct position where the exposed difference would be 0.</p>

  <p>I already have both the current and start position of a finger, but it’s the screenspace position. I first have to convert these positions to worldspace. I already have this <code class="language-plaintext highlighter-rouge">GetSurfacePosition</code> method, but it returns the worldspace coordinates floored to an int. For my camera dragging use-case, I don’t want floored coordinates.</p>

  <p>To fix this, I change <code class="language-plaintext highlighter-rouge">GetSurfacePosition</code> to expose a non-floored <code class="language-plaintext highlighter-rouge">Vector2</code>, and introduce a <code class="language-plaintext highlighter-rouge">GetFlooredSurfacePosition</code> to get a floored <code class="language-plaintext highlighter-rouge">Vector2Int</code>.</p>

  <h4 id="surfacepointerbehaviourcs-partial-2">SurfacePointerBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetFlooredSurfacePosition</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">screenPosition</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">Vector2</span><span class="p">?</span> <span class="n">surfacePosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">surfacePosition</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="k">return</span> <span class="n">Vector2Int</span><span class="p">.</span><span class="nf">FloorToInt</span><span class="p">((</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePosition</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">private</span> <span class="n">Vector2</span><span class="p">?</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">screenPosition</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">ray</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="nf">ScreenPointToRay</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">plane</span><span class="p">.</span><span class="nf">Raycast</span><span class="p">(</span><span class="n">ray</span><span class="p">,</span> <span class="k">out</span> <span class="n">distance</span><span class="p">))</span>
  <span class="p">{</span>
    <span class="kt">var</span> <span class="n">point</span> <span class="p">=</span> <span class="n">ray</span><span class="p">.</span><span class="nf">GetPoint</span><span class="p">(</span><span class="n">distance</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">Vector2</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">point</span><span class="p">.</span><span class="n">z</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>Because the camera might be moving at any point in time, I want to record the position of the finger on the surface in worldspace coordinates at the moment the finger began touching the screen. By the time we detect the finger is dragging, either the camera or finger might already have moved. Therefore, we need to keep the surface in a separate variable, and we need to keep track of it for every finger, since we don’t know yet which finger will be dragging. We need another dictionary. I call this dictionary <code class="language-plaintext highlighter-rouge">fingerStartSurfacePositions</code>.</p>

  <p>Like <code class="language-plaintext highlighter-rouge">fingerStartPositions</code>, it keeps the position of the finger per finger, but instead of keeping the position on screen in screenspace coordinates, I keep the position on surface in worldspace coordinates.</p>

  <p>If the primary finger is dragging, I subtract the start surface position of that finger from the current surface position of that finger. The result is target camera movement, stored in <code class="language-plaintext highlighter-rouge">dragSurfaceDelta</code>. I’ll expose a getter for that property.</p>

  <h4 id="surfacepointerbehaviourcs-partial-3">SurfacePointerBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">Vector2</span><span class="p">?</span> <span class="n">DragSurfaceDelta</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

<span class="c1">// inside UpdateFingerTapPosition</span>
<span class="n">DragSurfaceDelta</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">primaryDraggingFingerId</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">Vector2</span><span class="p">?</span> <span class="n">currentSurfacePosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">fingerCurrentPositions</span><span class="p">[(</span><span class="kt">int</span><span class="p">)</span><span class="n">primaryDraggingFingerId</span><span class="p">]);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">currentSurfacePosition</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">Vector2</span> <span class="n">startSurfacePosition</span> <span class="p">=</span> <span class="n">fingerStartSurfacePositions</span><span class="p">[(</span><span class="kt">int</span><span class="p">)</span><span class="n">primaryDraggingFingerId</span><span class="p">];</span>
    <span class="n">dragSurfaceDelta</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">currentSurfacePosition</span> <span class="p">-</span> <span class="n">startSurfacePosition</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>Now, for my camera rig behaviour:</p>

  <h4 id="topdowncamerarigbehaviourcs">TopDownCameraRigBehaviour.cs</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">TopDownCameraRigBehaviour</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
  <span class="k">public</span> <span class="kt">float</span> <span class="n">moveSpeed</span> <span class="p">=</span> <span class="m">10</span><span class="p">;</span>

  <span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="kt">float</span> <span class="n">horizontal</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetAxis</span><span class="p">(</span><span class="s">"Horizontal"</span><span class="p">);</span>
    <span class="kt">float</span> <span class="n">vertical</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetAxis</span><span class="p">(</span><span class="s">"Vertical"</span><span class="p">);</span>

    <span class="n">transform</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="k">new</span> <span class="nf">Vector3</span><span class="p">(</span><span class="n">horizontal</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">vertical</span><span class="p">)</span> <span class="p">*</span> <span class="n">moveSpeed</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>I need a reference to my pointer behaviour first. Then, when the player is dragging, all axis input needs to be ignored. Without testing, I changed my <code class="language-plaintext highlighter-rouge">Update</code> function to:</p>

  <h4 id="topdowncamerarigbehaviourcs-partial">TopDownCameraRigBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">surfacePointerBehaviour</span><span class="p">.</span><span class="n">DragSurfaceDelta</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">Vector2</span> <span class="n">dragDelta</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePointerBehaviour</span><span class="p">.</span><span class="n">DragSurfaceDelta</span><span class="p">;</span>
  <span class="n">transform</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="k">new</span> <span class="nf">Vector3</span><span class="p">(</span><span class="n">dragDelta</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">dragDelta</span><span class="p">.</span><span class="n">y</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
  <span class="kt">float</span> <span class="n">horizontal</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetAxis</span><span class="p">(</span><span class="s">"Horizontal"</span><span class="p">);</span>
  <span class="kt">float</span> <span class="n">vertical</span> <span class="p">=</span> <span class="n">Input</span><span class="p">.</span><span class="nf">GetAxis</span><span class="p">(</span><span class="s">"Vertical"</span><span class="p">);</span>
  <span class="n">transform</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="k">new</span> <span class="nf">Vector3</span><span class="p">(</span><span class="n">horizontal</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">vertical</span><span class="p">)</span> <span class="p">*</span> <span class="n">moveSpeed</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">deltaTime</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <p>Now I link the pointer behavior to the camera behaviour from the Unity editor and start my project, expecting the worst, hoping for the best.</p>

  <div class="center-video">
  <video width="720" height="540" controls="" preload="none">
    <source src="/assets/blog/camera-test-1-web.mp4" type="video/mp4" />
    <source src="/assets/blog/camera-test-1-web.webm" type="video/webm" />
  </video>
  <em>I only slightly moved my finger</em>
</div>

  <p>Well, at least dragging is detected properly.</p>

  <p>I can only assume what’s going wrong at this point: my <code class="language-plaintext highlighter-rouge">DragSurfaceDelta</code> variable doesn’t point to the right direction, and instead points away from the position the camera needs to be moved to.</p>

  <p>This might be partially due to the fact that my camera isn’t aligned with the surface, but instead is angled 45 degrees. To fix this, I need to consider the angle between the camera’s direction relative to the surface, and rotate my <code class="language-plaintext highlighter-rouge">DragSurfaceDelta</code> by that angle.</p>

  <p>I think I need to start by getting the global worldspace rotation of both the surface and the camera. According to the <a href="https://docs.unity3d.com/2020.2/Documentation/ScriptReference/Transform.html">Transform docs</a>, <code class="language-plaintext highlighter-rouge">transform.rotation</code> returns the global rotation, not the local rotation relative to the parent (which is called <code class="language-plaintext highlighter-rouge">transform.localRotation</code>).</p>

  <p>I don’t need the surface rotation, since I’m actually using an infinite plane generated at runtime without any rotation or translation to get the surface coordinates (<code class="language-plaintext highlighter-rouge">GetSurfacePosition</code> raycasts to a <code class="language-plaintext highlighter-rouge">new Plane(Vector3.up, Vector3.zero)</code>). I therefore know the surface isn’t rotated - or at least the coordinates returned by <code class="language-plaintext highlighter-rouge">GetSurfacePosition</code> aren’t rotated. Because the surface isn’t rotated, only the camera’s rotation needs to be considered.</p>

  <p>I first have to compensate for the camera’s rotation, then I can move the camera, and finally undo the camera rotation compensation.</p>

  <h4 id="topdowncamerarigbehaviourcs-partial-1">TopDownCameraRigBehaviour.cs (partial)</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Vector2</span> <span class="n">dragDelta</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePointerBehaviour</span><span class="p">.</span><span class="n">DragSurfaceDelta</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">angle</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">transform</span><span class="p">.</span><span class="n">rotation</span><span class="p">.</span><span class="n">eulerAngles</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">transform</span><span class="p">.</span><span class="nf">Rotate</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">up</span><span class="p">,</span> <span class="p">-</span><span class="n">angle</span><span class="p">);</span>
<span class="n">transform</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(-</span><span class="k">new</span> <span class="nf">Vector3</span><span class="p">(</span><span class="n">dragDelta</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">dragDelta</span><span class="p">.</span><span class="n">y</span><span class="p">));</span>
<span class="n">transform</span><span class="p">.</span><span class="nf">Rotate</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">up</span><span class="p">,</span> <span class="n">angle</span><span class="p">);</span>
</code></pre></div>  </div>

  <div class="center-video">
  <video width="720" height="540" controls="" preload="none">
    <source src="/assets/blog/camera-test-2-web.mp4" type="video/mp4" />
    <source src="/assets/blog/camera-test-2-web.webm" type="video/webm" />
  </video>
  <em>It's like the surface is glued to my finger</em>
</div>

  <p>This works surprisingly great and I’m really happy with the result. There was only the problem where there was a horrible lag of about half a second, but this was easily solved by removing the 0.5 seconds threshold.</p>

  <p>To finish it off, I did some refactoring to get rid of the dictionaries and instead use a class to keep track of individual fingers. The final <code class="language-plaintext highlighter-rouge">SurfacePointerBehaviour</code> is as follows:</p>

  <h4 id="topdowncamerarigbehaviourcs-1">TopDownCameraRigBehaviour.cs</h4>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">UnityEngine</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">UnityEngine.EventSystems</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Collections.Generic</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SurfacePointerBehaviour</span> <span class="p">:</span> <span class="n">MonoBehaviour</span>
<span class="p">{</span>
  <span class="k">private</span> <span class="k">class</span> <span class="nc">Finger</span>
  <span class="p">{</span>
    <span class="k">public</span> <span class="n">Vector2</span> <span class="n">startSurfacePosition</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">Vector2</span> <span class="n">currentSurfacePosition</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">Vector2</span> <span class="n">startScreenPosition</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">Vector2</span> <span class="n">currentScreenPosition</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">dragging</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">float</span> <span class="n">ScreenDistance</span> <span class="p">=&gt;</span> <span class="n">Vector2</span><span class="p">.</span><span class="nf">Distance</span><span class="p">(</span><span class="n">startScreenPosition</span><span class="p">,</span> <span class="n">currentScreenPosition</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">readonly</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Finger</span><span class="p">&gt;</span> <span class="n">fingers</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">Finger</span><span class="p">&gt;();</span>
  <span class="k">private</span> <span class="n">Finger</span> <span class="n">primaryFinger</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

  <span class="k">public</span> <span class="n">Vector2</span><span class="p">?</span> <span class="n">DragSurfaceDelta</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
  <span class="k">public</span> <span class="kt">float</span> <span class="n">dragMinimumDistance</span> <span class="p">=</span> <span class="m">20f</span><span class="p">;</span>

  <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">Input</span><span class="p">.</span><span class="n">simulateMouseWithTouches</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">public</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetPointClicked</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">return</span> <span class="n">mouseClickPosition</span> <span class="p">??</span> <span class="n">fingerTapPosition</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">void</span> <span class="nf">Update</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="nf">UpdateMouseClickPosition</span><span class="p">();</span>
    <span class="nf">UpdateFingerTapPosition</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">UpdateMouseClickPosition</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetMouseButtonDown</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
      <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">())</span>
    <span class="p">{</span>
      <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="nf">GetFlooredSurfacePosition</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">mousePosition</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
      <span class="n">mouseClickPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">UpdateFingerTapPosition</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="n">DragSurfaceDelta</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">Input</span><span class="p">.</span><span class="n">touchCount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
      <span class="p">{</span>
        <span class="nf">HandleTouch</span><span class="p">(</span><span class="n">Input</span><span class="p">.</span><span class="nf">GetTouch</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">primaryFinger</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="n">DragSurfaceDelta</span> <span class="p">=</span> <span class="n">primaryFinger</span><span class="p">.</span><span class="n">currentSurfacePosition</span> <span class="p">-</span> <span class="n">primaryFinger</span><span class="p">.</span><span class="n">startSurfacePosition</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">void</span> <span class="nf">HandleTouch</span><span class="p">(</span><span class="n">Touch</span> <span class="n">touch</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">Vector2</span> <span class="n">screenPosition</span> <span class="p">=</span> <span class="n">touch</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="n">Vector2</span><span class="p">?</span> <span class="n">surfacePosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Began</span>
      <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="n">EventSystem</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="nf">IsPointerOverGameObject</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">surfacePosition</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="n">Finger</span> <span class="n">finger</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Finger</span>
        <span class="p">{</span>
          <span class="n">currentSurfacePosition</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePosition</span><span class="p">,</span>
          <span class="n">startSurfacePosition</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePosition</span><span class="p">,</span>
          <span class="n">currentScreenPosition</span> <span class="p">=</span> <span class="n">screenPosition</span><span class="p">,</span>
          <span class="n">startScreenPosition</span> <span class="p">=</span> <span class="n">screenPosition</span><span class="p">,</span>
          <span class="n">dragging</span> <span class="p">=</span> <span class="k">false</span>
        <span class="p">};</span>
        <span class="n">fingers</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">]</span> <span class="p">=</span> <span class="n">finger</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">fingers</span><span class="p">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="n">Finger</span> <span class="n">finger</span> <span class="p">=</span> <span class="n">fingers</span><span class="p">[</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">];</span>
      <span class="n">finger</span><span class="p">.</span><span class="n">currentScreenPosition</span> <span class="p">=</span> <span class="n">screenPosition</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">surfacePosition</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="n">finger</span><span class="p">.</span><span class="n">currentSurfacePosition</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePosition</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Moved</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">finger</span><span class="p">.</span><span class="n">ScreenDistance</span> <span class="p">&gt;=</span> <span class="n">dragMinimumDistance</span><span class="p">)</span>
        <span class="p">{</span>
          <span class="n">finger</span><span class="p">.</span><span class="n">dragging</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">primaryFinger</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
          <span class="p">{</span>
            <span class="n">primaryFinger</span> <span class="p">=</span> <span class="n">finger</span><span class="p">;</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span>
      <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">phase</span> <span class="p">==</span> <span class="n">TouchPhase</span><span class="p">.</span><span class="n">Ended</span><span class="p">)</span>
      <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">primaryFinger</span> <span class="p">==</span> <span class="n">finger</span><span class="p">)</span>
        <span class="p">{</span>
          <span class="n">primaryFinger</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(!</span><span class="n">finger</span><span class="p">.</span><span class="n">dragging</span><span class="p">)</span>
        <span class="p">{</span>
          <span class="n">fingerTapPosition</span> <span class="p">=</span> <span class="n">Vector2Int</span><span class="p">.</span><span class="nf">FloorToInt</span><span class="p">(</span><span class="n">finger</span><span class="p">.</span><span class="n">startSurfacePosition</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">fingers</span><span class="p">.</span><span class="nf">Remove</span><span class="p">(</span><span class="n">touch</span><span class="p">.</span><span class="n">fingerId</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="n">Ray</span> <span class="n">ray</span><span class="p">;</span>
  <span class="k">private</span> <span class="k">readonly</span> <span class="n">Plane</span> <span class="n">plane</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Plane</span><span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="n">up</span><span class="p">,</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">);</span>
  <span class="k">private</span> <span class="kt">float</span> <span class="n">distance</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>

  <span class="k">private</span> <span class="n">Vector2Int</span><span class="p">?</span> <span class="nf">GetFlooredSurfacePosition</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">screenPosition</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">Vector2</span><span class="p">?</span> <span class="n">surfacePosition</span> <span class="p">=</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">surfacePosition</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="n">Vector2Int</span><span class="p">.</span><span class="nf">FloorToInt</span><span class="p">((</span><span class="n">Vector2</span><span class="p">)</span><span class="n">surfacePosition</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="n">Vector2</span><span class="p">?</span> <span class="nf">GetSurfacePosition</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">screenPosition</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">ray</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="nf">ScreenPointToRay</span><span class="p">(</span><span class="n">screenPosition</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">plane</span><span class="p">.</span><span class="nf">Raycast</span><span class="p">(</span><span class="n">ray</span><span class="p">,</span> <span class="k">out</span> <span class="n">distance</span><span class="p">))</span>
    <span class="p">{</span>
      <span class="kt">var</span> <span class="n">point</span> <span class="p">=</span> <span class="n">ray</span><span class="p">.</span><span class="nf">GetPoint</span><span class="p">(</span><span class="n">distance</span><span class="p">);</span>
      <span class="k">return</span> <span class="k">new</span> <span class="nf">Vector2</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">point</span><span class="p">.</span><span class="n">z</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">null</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

</div>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[In this blogpost I explain how I add touch controls to my unity game.]]></summary></entry><entry><title type="html">Contributing to JSDOM</title><link href="https://bonaroo.nl/2020/07/06/contributing-to-jsdom.html" rel="alternate" type="text/html" title="Contributing to JSDOM" /><published>2020-07-06T00:00:00+00:00</published><updated>2020-07-06T00:00:00+00:00</updated><id>https://bonaroo.nl/2020/07/06/contributing-to-jsdom</id><content type="html" xml:base="https://bonaroo.nl/2020/07/06/contributing-to-jsdom.html"><![CDATA[<h1 id="contributing-to-open-source-project-js-dom">Contributing to open source project JS-DOM</h1>

<p>We use JSDOM for testing clientside applications in NodeJS. JSDOM lowers the complexity of writing tests for clientside code by omitting the browser and replacing it with a fake one: JSDOM.</p>

<p>However, there’s one JSDOM dependency that concerned me a bit: <a href="https://www.npmjs.com/package/request">request</a>, with <a href="https://www.npmjs.com/package/request-promise-native">request-promise-native</a>. Request has been deprecated, and request-promise-native does nasty things using <a href="https://www.npmjs.com/package/stealthy-require">stealthy-require</a>. I’m not sure why anyone would use <code class="language-plaintext highlighter-rouge">stealthy-require</code>, but I trust there’s a good reason for to use it.</p>

<p><code class="language-plaintext highlighter-rouge">request</code> has already been a discussed to be replaced with something else in an issue <a href="https://github.com/jsdom/jsdom/issues/2792">#2792: Replace request with something better</a>. Since there were no pull requests for the issue, I decided to see if I can help out and fix it myself. In this blog post, I’ll describe my process.</p>

<h2 id="contributing-to-foreign-projects">Contributing to foreign projects</h2>

<p>Changing code inside a foreign project is commonly quite the challenge. There is usually a lot of code and a lot of things to consider, many you just don’t know about. This is why tests are really important.</p>

<p>For a complex project like JSDOM, without a comprehensive suite of tests, there is no way to be sure your changes might break something. Even with perfect code coverage, there is still no guarantee your changes don’t break something, but you can still be pretty sure your code at least runs in the cases presented by the tests.</p>

<h2 id="fork--clone">Fork &amp; Clone.</h2>

<p>I forked &amp; cloned the repository, and created a new branch to start my experimental replacement.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:tobyhinloopen/jsdom.git
<span class="nb">cd </span>jsdom
git checkout <span class="nt">-b</span> 2792-replace-request-with-node-fetch
</code></pre></div></div>

<p>Now let’s see if there are some tests I can run.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm i
npm ERR! code EUNSUPPORTEDPROTOCOL
npm ERR! Unsupported URL Type <span class="s2">"link:"</span>: <span class="nb">link</span>:./scripts/eslint-plugin

npm ERR! A <span class="nb">complete </span>log of this run can be found <span class="k">in</span>:
npm ERR!     /Users/hinloopen/.npm/_logs/2020-05-10T15_02_02_981Z-debug.log
</code></pre></div></div>

<p>Uh… ok. Let’s consult the README first. There is a <code class="language-plaintext highlighter-rouge">README.md</code> and <code class="language-plaintext highlighter-rouge">Contributing.md</code>. Both might be relevant.</p>

<p>In <code class="language-plaintext highlighter-rouge">Contributing.md</code>, it’s already mentioned they’re using <code class="language-plaintext highlighter-rouge">yarn</code>. Eager to start, I ignore the rest and use <code class="language-plaintext highlighter-rouge">yarn install</code> to install the dependencies.</p>

<p>Let’s run some tests without consulting the readme or contributing guidelines and see if they run.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn <span class="nb">test</span>
<span class="c"># ...</span>
1<span class="o">)</span> <span class="s2">"before all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>
2<span class="o">)</span> <span class="s2">"after all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>

0 passing <span class="o">(</span>16ms<span class="o">)</span>
2 failing

1<span class="o">)</span> <span class="s2">"before all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>:
    Error: Host entries not present <span class="k">for </span>web platform tests. See https://github.com/web-platform-tests/wpt#running-the-tests
    at /Users/hinloopen/Projects/Github/jsdom/test/web-platform-tests/start-wpt-server.js:62:13
    at async /Users/hinloopen/Projects/Github/jsdom/test/web-platform-tests/run-tuwpts.js:25:32
<span class="c"># ...</span>
</code></pre></div></div>

<p>Looks like the tests require more setup. Let’s consult the <a href="https://github.com/jsdom/jsdom/blob/master/Contributing.md#tests">readme</a> again. The readme refers to <a href="https://github.com/web-platform-tests/wpt/blob/master/README.md">The web-platform-tests Project</a>. It looks like this project allows you to run a test suite (that you have to provide yourself in some way) inside a set of browsers. You have to clone the repo and run the code.</p>

<p>I’ll just assume this web-platform-tests project starts some kind of server and you have to open a page in a real browser. Since we’re testing a fake browser (JSDOM), I also assume JSDOM somehow registers to WPT as a real browser, so it can the same tests in JSDOM, as if JSDOM was a browser. Let’s try it out.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/web-platform-tests/wpt.git
<span class="c"># ...</span>
<span class="nv">$ </span><span class="nb">cd </span>wpt
<span class="nv">$ </span>./wpt serve
<span class="c"># ...</span>
CRITICAL:web-platform-tests:Failed to start HTTP server on port 59514<span class="p">;</span> is something already using that port?
CRITICAL:web-platform-tests:Please ensure all the necessary WPT subdomains are mapped to a loopback device <span class="k">in</span> /etc/hosts.
</code></pre></div></div>

<p>Right. <a href="https://github.com/web-platform-tests/wpt/blob/master/README.md#running-the-tests">RTFM</a>. I added the setup instructions to <code class="language-plaintext highlighter-rouge">.envrc</code> in the WPT project folder.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>nano .envrc
python <span class="nt">-m</span> ensurepip <span class="nt">--user</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PATH</span><span class="s2">:</span><span class="nv">$HOME</span><span class="s2">/Library/Python/2.7/bin"</span>
pip <span class="nb">install</span> <span class="nt">--user</span> virtualenv
</code></pre></div></div>

<p>Additionally:</p>

<blockquote>
  <p>To get the tests running, you need to set up the test domains in your hosts file. (<a href="https://web-platform-tests.org/running-tests/from-local-system.html#hosts-file-setup">source</a>)</p>
</blockquote>

<p>Let’s do that.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./wpt make-hosts-file | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
<span class="c"># ...</span>
</code></pre></div></div>

<p>I think that command fails when a password is asked. I used <code class="language-plaintext highlighter-rouge">sudo ls</code> to make my system ask for a password so I can run another sudo command without asking for password. I’m sure there is a better way, but hey, it works.</p>

<p>After that, let’s retry <code class="language-plaintext highlighter-rouge">serve</code>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./wpt serve
<span class="c"># ...</span>
INFO:web-platform-tests:Starting http server on web-platform.test:8000
INFO:web-platform-tests:Starting http server on web-platform.test:59632
INFO:web-platform-tests:Starting https server on web-platform.test:8443
</code></pre></div></div>

<p>Hey, it works! Let’s open it with a browser!</p>

<p><img src="/assets/blog/wpt-browser.png" alt="Opening in browser" /></p>

<p>Well that’s not very interesting at all. Am I done now? Let’s get back to JSDOM and run the tests.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn <span class="nb">test</span>
<span class="c"># ...</span>
</code></pre></div></div>

<p>Cool! It’s running tests. <strong>Thousands of them</strong>. While the tests are running and are heating my macbook, let’s take a peak at our goal: Removing <code class="language-plaintext highlighter-rouge">request</code>. Let’s see where it is used.</p>

<h2 id="finding-usages-of-request">Finding usages of request</h2>

<p>The first, most naive way to find usages of request is to look for <code class="language-plaintext highlighter-rouge">require("request")</code> and <code class="language-plaintext highlighter-rouge">require("request-promise-native")</code>:</p>

<h4 id="libjsdomlivinghelperswrap-cookie-jar-for-requestjs">lib/jsdom/living/helpers/wrap-cookie-jar-for-request.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use strict</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request</span><span class="dl">"</span><span class="p">);</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">cookieJar</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">jarWrapper</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">jar</span><span class="p">();</span>
  <span class="nx">jarWrapper</span><span class="p">.</span><span class="nx">_jar</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">;</span>
  <span class="k">return</span> <span class="nx">jarWrapper</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="libjsdomlivingxhrxhr-utilsjs">lib/jsdom/living/xhr/xhr-utils.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">wrapCookieJarForRequest</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">../helpers/wrap-cookie-jar-for-request</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
  <span class="kd">function</span> <span class="nx">doRequest</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="nx">request</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>

      <span class="k">if</span> <span class="p">(</span><span class="nx">hasBody</span> <span class="o">&amp;&amp;</span> <span class="nx">flag</span><span class="p">.</span><span class="nx">formData</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nx">form</span><span class="p">();</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">entry</span> <span class="k">of</span> <span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">form</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">entry</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">options</span><span class="p">);</span>
        <span class="p">}</span>
      <span class="p">}</span>

      <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventEmitter</span><span class="p">();</span>
      <span class="nx">process</span><span class="p">.</span><span class="nx">nextTick</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">client</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">));</span>
      <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="c1">/// ...</span>
</code></pre></div></div>

<h4 id="testutiljs">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="cm">/**
 * Reads a static fixture file as utf8.
 * If running tests from node, the file will be read from the file system
 * If running tests using karma, a http request will be performed to retrieve the file using karma's server.
 * @param {string} relativePath Relative path within the test directory. For example "jsdom/files/test.html"
 */</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">readTestFixture</span> <span class="o">=</span> <span class="nx">relativePath</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">useRequest</span> <span class="o">=</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">inBrowserContext</span><span class="p">();</span>

  <span class="k">return</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">nodeResolverPromise</span><span class="p">(</span><span class="nx">nodeResolver</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">useRequest</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">exports</span><span class="p">.</span><span class="nx">getTestFixtureUrl</span><span class="p">(</span><span class="nx">relativePath</span><span class="p">),</span> <span class="p">{</span> <span class="na">timeout</span><span class="p">:</span> <span class="mi">5000</span> <span class="p">},</span> <span class="nx">nodeResolver</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="nx">relativePath</span><span class="p">),</span> <span class="p">{</span> <span class="na">encoding</span><span class="p">:</span> <span class="dl">"</span><span class="s2">utf8</span><span class="dl">"</span> <span class="p">},</span> <span class="nx">nodeResolver</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">})</span>
  <span class="c1">// request passes (error, response, content) to the callback</span>
  <span class="c1">// we are only interested in the `content`</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span> <span class="o">=&gt;</span> <span class="nx">useRequest</span> <span class="p">?</span> <span class="nx">result</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="p">:</span> <span class="nx">result</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="libjsdombrowserresourcesresource-loaderjs">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request-promise-native</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">wrapCookieJarForRequest</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">../../living/helpers/wrap-cookie-jar-for-request</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
  <span class="nx">fetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">parseURL</span><span class="p">(</span><span class="nx">urlString</span><span class="p">);</span>
    <span class="c1">// ...</span>
    <span class="k">switch</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">scheme</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// ...</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">requestOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRequestOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">request</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">requestOptions</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="c1">// ...</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<h4 id="testweb-platform-testsstart-wpt-serverjs">test/web-platform-tests/start-wpt-server.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">requestHead</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request-promise-native</span><span class="dl">"</span><span class="p">).</span><span class="nx">head</span><span class="p">;</span>
<span class="c1">// ...</span>
<span class="kd">function</span> <span class="nx">pollForServer</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">requestHead</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">strictSSL</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Looks good! Looking for <code class="language-plaintext highlighter-rouge">require('request')</code> yields no results, so I’ll assume there is either a strict merge policy or some kind of linter ensuring double quoted strings are used everywhere.</p>

<p>There might be other ways <code class="language-plaintext highlighter-rouge">request</code> or <code class="language-plaintext highlighter-rouge">request-promise-native</code> is required. One could have aliased the <code class="language-plaintext highlighter-rouge">require</code> to something else. Maybe someone used <code class="language-plaintext highlighter-rouge">require("re" + "quest")</code> to mess with me. Maybe someone’s using <code class="language-plaintext highlighter-rouge">import</code> somewhere.</p>

<p>Instead of hunting other possible dependencies, let’s try to fix the found dependencies first and re-run the tests.</p>

<h2 id="narrowing-down-the-tests">Narrowing down the tests</h2>

<p>Running all tests takes ages. However, I’m not sure how to narrow down the number of tests. While trying to figure out how to narrow down the number of tests, the test runner finally finished after 11 minutes.</p>

<p>Reading the contributing guidelines, it is mentioned you can run only JSDOM api tests, or even a set of tests for one specific function. Since the JSDOM API includes a <code class="language-plaintext highlighter-rouge">fromUrl</code> function, I’ll assume <code class="language-plaintext highlighter-rouge">fromUrl</code> fetches the document using <code class="language-plaintext highlighter-rouge">request</code>.</p>

<p>There is a test suite specifically for <code class="language-plaintext highlighter-rouge">fromUrl</code> and based on the contributing guidelines, I can run it using <code class="language-plaintext highlighter-rouge">yarn test-mocha test/api/from-url.js</code>. Let’s try that.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-mocha <span class="nb">test</span>/api/from-url.js
yarn run v1.22.4
<span class="nv">$ </span>mocha <span class="nb">test</span>/api/from-url.js


  API: JSDOM.fromURL<span class="o">()</span>
    ✓ should <span class="k">return </span>a rejected promise <span class="k">for </span>a bad URL
    ✓ should <span class="k">return </span>a rejected promise <span class="k">for </span>a 404
    ✓ should <span class="k">return </span>a rejected promise <span class="k">for </span>a 500
    ✓ should use the body of 200 responses <span class="o">(</span>54ms<span class="o">)</span>
    ✓ should use the body of 301 responses
    ✓ should be able to handle gzipped bodies
    ✓ should send a HTML-preferring Accept header
    ✓ should send an Accept-Language: en header
    user agent
      ✓ should use the default user agent as the User-Agent header when none is given
    referrer
      ✓ should reject when passing an invalid absolute URL <span class="k">for </span>referrer
      ✓ should not send a Referer header when no referrer option is given
      ✓ should use the supplied referrer option as a Referer header
      ✓ should canonicalize referrer URLs before using them as a Referer header
      ✓ should use the redirect <span class="nb">source </span>URL as the referrer, overriding a provided one
    inferring options from the response
      url
        ✓ should use the URL fetched <span class="k">for </span>a 200
        ✓ should preserve full request URL
        ✓ should use the ultimate response URL after a redirect
        ✓ should preserve fragments when processing redirects
        ✓ should disallow passing a URL manually
      contentType
        ✓ should use the content <span class="nb">type </span>fetched <span class="k">for </span>a 200
        ✓ should use the ultimate response content <span class="nb">type </span>after a redirect
        ✓ should disallow passing a content <span class="nb">type </span>manually
    cookie jar integration
      ✓ should send applicable cookies <span class="k">in </span>a supplied cookie jar
      ✓ should store cookies <span class="nb">set </span>by the server <span class="k">in </span>a supplied cookie jar
      ✓ should store cookies <span class="nb">set </span>by the server <span class="k">in </span>a newly-created cookie jar


  25 passing <span class="o">(</span>234ms<span class="o">)</span>

✨  Done <span class="k">in </span>1.09s.
</code></pre></div></div>

<p>Phew. That’s better. One second. Let’s first try to break these tests by changing the code that requires <code class="language-plaintext highlighter-rouge">request</code>. I’m hoping these tests touches the <code class="language-plaintext highlighter-rouge">request</code>-requires at some point.</p>

<p>The test messages also mention cookie jar. I’m hoping this is somehow related to <code class="language-plaintext highlighter-rouge">lib/jsdom/living/helpers/wrap-cookie-jar-for-request.js</code> so we can test our changes in that file using this test.</p>

<h2 id="removing-request-from-testutiljs">Removing request from test/util.js</h2>

<p>Before we can drop <code class="language-plaintext highlighter-rouge">request</code>, we need a replacement. I’ll be using <code class="language-plaintext highlighter-rouge">node-fetch</code>. <code class="language-plaintext highlighter-rouge">node-fetch</code> is a NodeJS implementation for the browser’s Fetch API. I like the idea of using a library that implements an existing standard because even if you don’t longer like or want to use the library, you can just replace the fetch library with some other fetch implementation.</p>

<p>Since JSDOM also runs in the browser, you can use the browser’s Fetch implementation in the browser. Isn’t that great?</p>

<p><code class="language-plaintext highlighter-rouge">npm install nod</code>– oh right, we’re using YARN now.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn <span class="nb">install </span>node-fetch
error <span class="sb">`</span><span class="nb">install</span><span class="sb">`</span> has been replaced with <span class="sb">`</span>add<span class="sb">`</span> to add new dependencies. Run <span class="s2">"yarn add node-fetch"</span> instead.
<span class="nv">$ </span>yarn add node-fetch
<span class="c"># ...</span>
✨  Done <span class="k">in </span>7.80s.
</code></pre></div></div>

<p>Ok. Now, let’s naively replace request with fetch somewhere. Let’s start with <code class="language-plaintext highlighter-rouge">test/util.js</code>, since I’ll assume it’s only used from tests. It is most likely the easiest one to replace.</p>

<h4 id="testutiljs-1">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">fetch</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">node-fetch</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">readTestFixture</span> <span class="o">=</span> <span class="nx">relativePath</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">useRequest</span> <span class="o">=</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">inBrowserContext</span><span class="p">();</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">useRequest</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">getTestFixtureUrl</span><span class="p">(</span><span class="nx">relativePath</span><span class="p">);</span>
    <span class="c1">// timeout is a node-fetch specific extention.</span>
    <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">timeout</span><span class="p">:</span> <span class="mi">5000</span> <span class="p">}).</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> fetching </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
    <span class="p">});</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">nodeResolverPromise</span><span class="p">(</span><span class="nx">nodeResolver</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="nx">relativePath</span><span class="p">),</span> <span class="p">{</span> <span class="na">encoding</span><span class="p">:</span> <span class="dl">"</span><span class="s2">utf8</span><span class="dl">"</span> <span class="p">},</span> <span class="nx">nodeResolver</span><span class="p">);</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Looks fine, I suppose. Let’s run the tests.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-mocha <span class="nb">test</span>/api/from-url.js
yarn run v1.22.4
<span class="nv">$ </span>mocha <span class="nb">test</span>/api/from-url.js
<span class="c"># ...</span>
  25 passing <span class="o">(</span>234ms<span class="o">)</span>
✨  Done <span class="k">in </span>1.02s.
</code></pre></div></div>

<p>All tests are passing, but I don’t know if the tests even touch my changes. Let’s just throw inside the method.</p>

<h4 id="testutiljs-2">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">readTestFixture</span> <span class="o">=</span> <span class="nx">relativePath</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">useRequest</span> <span class="o">=</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">inBrowserContext</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">useRequest</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">???</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-mocha <span class="nb">test</span>/api/from-url.js
yarn run v1.22.4
<span class="nv">$ </span>mocha <span class="nb">test</span>/api/from-url.js
<span class="c"># ...</span>
  25 passing <span class="o">(</span>234ms<span class="o">)</span>
✨  Done <span class="k">in </span>1.02s.
</code></pre></div></div>

<p>No thrown errors or failing tests, so it’s still not touching my changes. Let’s run all API tests for good measure. Otherwise, I’ll have to run all tests.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn test-api
<span class="c"># ...</span>
  419 passing <span class="o">(</span>4s<span class="o">)</span>

✨  Done <span class="k">in </span>4.56s.
</code></pre></div></div>

<p>Still no error. Let’s run all tests until something goes bad. While the tests are running forever, let’s CMD+F for <code class="language-plaintext highlighter-rouge">readTestFixture</code>.</p>

<p>It looks like all occurences are in <code class="language-plaintext highlighter-rouge">test/to-port-to-wpts</code>. CMD+F for <code class="language-plaintext highlighter-rouge">to-port-to-wpts</code> yields to this result in the readme:</p>

<blockquote>
  <p>Ideally we would only use Mocha for testing the JSDOM API itself, from the outside. Unfortunately, a lot of web platform features are still tested using Mocha, instead of web-platform-tests. These are located in the <a href="./to-port-to-wpts/"><code class="language-plaintext highlighter-rouge">to-port-to-wpts</code></a> subdirectory.</p>
</blockquote>

<p>So maybe running all mocha tests will trigger my intentional failure. While the main test suite is running, I run the mocha tests using <code class="language-plaintext highlighter-rouge">yarn test-mocha</code>, hoping it will run faster.</p>

<p>After a minute, I cancelled the mocha runner since it there seems to be no obvious speed improvement by invoking mocha this way.</p>

<p>What about <code class="language-plaintext highlighter-rouge">yarn test-mocha test/to-port-to-wpts/*.js</code>?</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-mocha <span class="nb">test</span>/to-port-to-wpts/<span class="k">*</span>.js

  379 passing <span class="o">(</span>6s<span class="o">)</span>
  1 pending

✨  Done <span class="k">in </span>9.78s.
</code></pre></div></div>

<p>That runs the tests, but the tests aren’t failing. Confused, I read the jsdoc comment above the function:</p>

<h4 id="testutiljs-3">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Reads a static fixture file as utf8.
 * If running tests from node, the file will be read from the file system
 * If running tests using karma, a http request will be performed to retrieve the file using karma's server.
 * @param {string} relativePath Relative path within the test directory. For example "jsdom/files/test.html"
 */</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">readTestFixture</span> <span class="o">=</span> <span class="nx">relativePath</span> <span class="o">=&gt;</span> <span class="p">{</span>
</code></pre></div></div>

<p>So my error will only be thrown when running from inside a browser. Well, I don’t need <code class="language-plaintext highlighter-rouge">node-fetch</code> inside a browser, do I? I can just use <code class="language-plaintext highlighter-rouge">window.fetch</code>, but I won’t get the timeout, since the <code class="language-plaintext highlighter-rouge">timeout</code> option isn’t supported on <code class="language-plaintext highlighter-rouge">window.fetch</code>.</p>

<p>How did <code class="language-plaintext highlighter-rouge">request</code> implement the timeout? I suppose it uses XMLHttpRequest in the background and aborts after a certain amount of time. Let’s ignore that for now and see if we can run the tests inside a browser. The jsdoc mentions <code class="language-plaintext highlighter-rouge">karma</code>. Let’s CMD+F <code class="language-plaintext highlighter-rouge">karma</code> in the readmes.</p>

<h4 id="contributingmd">Contributing.md</h4>

<blockquote>
  <p>The mocha test cases are executed in Chrome using <a href="https://karma-runner.github.io/">karma</a>. Currently, web platform tests are not executed in the browser yet.</p>

  <p><strong>To run all browser tests:</strong> <code class="language-plaintext highlighter-rouge">yarn test-browser</code></p>

  <p><strong>To run the karma tests in an iframe:</strong> <code class="language-plaintext highlighter-rouge">yarn test-browser-iframe</code></p>

  <p><strong>To run the karma tests in a web worker:</strong> <code class="language-plaintext highlighter-rouge">yarn test-browser-worker</code></p>
</blockquote>

<p>Sure. Let’s try that.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-browser
yarn run v1.22.4
<span class="nv">$ </span>yarn test-browser-iframe <span class="o">&amp;&amp;</span> yarn test-browser-worker
<span class="nv">$ </span>karma start <span class="nb">test</span>/karma.conf.js
<span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> ERROR
  Uncaught Error: ???
  at /var/folders/bf/29ljwt3s4dscb7tdd2z5zz0h0000gn/T/test/util.js:162:1 &lt;- /var/folders/bf/29ljwt3s4dscb7tdd2z5zz0h0000gn/T/91efe4665a6210ee2f5edcae3a8f463c.browserify.js:293540:5

  Error: ???
      at exports.readTestFixture <span class="o">(</span>/var/folders/bf/29ljwt3s4dscb7tdd2z5zz0h0000gn/T/test/util.js:162:1 &lt;- /var/folders/bf/29ljwt3s4dscb7tdd2z5zz0h0000gn/T/91efe4665a6210ee2f5edcae3a8f463c.browserify.js:293540:11<span class="o">)</span>
      <span class="o">[</span>...]

</code></pre></div></div>

<p>My <code class="language-plaintext highlighter-rouge">???</code> error is thrown! Now, let’s retry without the intentional failure.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-browser
<span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/namespaces should <span class="nb">set </span>namespaces <span class="k">in </span>HTML documents created by jsdom.env<span class="o">()</span> FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/namespaces should <span class="nb">set </span>namespace-related properties <span class="k">in </span>HTML documents created by innerHTML FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/namespaces should <span class="nb">set </span>namespace-related properties <span class="k">in </span>HTML-SVG documents created by jsdom.env<span class="o">()</span> FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/namespaces should <span class="nb">set </span>namespace-related properties <span class="k">in </span>HTML-SVG documents created by innerHTML FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/parsing real-world page with &lt; inside a text node <span class="o">(</span>GH-800<span class="o">)</span> FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/xml should ignore self-closing of tags <span class="k">in </span>html docs FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span> jsdom/xml should handle self-closing tags properly <span class="k">in </span>xml docs FAILED
	TypeError: Cannot <span class="nb">read </span>property <span class="s1">'then'</span> of undefined
	    <span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span>: Executed 1209 of 2460 <span class="o">(</span>7 FAILED<span class="o">)</span> <span class="o">(</span>skipped 1251<span class="o">)</span> <span class="o">(</span>7.437 secs / 6.708 secs<span class="o">)</span>
TOTAL: 7 FAILED, 1202 SUCCESS
error Command failed with <span class="nb">exit </span>code 1.
info Visit https://yarnpkg.com/en/docs/cli/run <span class="k">for </span>documentation about this command.
error Command failed with <span class="nb">exit </span>code 1.
info Visit https://yarnpkg.com/en/docs/cli/run <span class="k">for </span>documentation about this command.
</code></pre></div></div>

<p>Failures! <code class="language-plaintext highlighter-rouge">TypeError: Cannot read property 'then' of undefined</code>? Oh… i forgot to <code class="language-plaintext highlighter-rouge">return</code>. Oops.</p>

<h4 id="testutiljs-4">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">if</span> <span class="p">(</span><span class="nx">useRequest</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">exports</span><span class="p">.</span><span class="nx">getTestFixtureUrl</span><span class="p">(</span><span class="nx">relativePath</span><span class="p">);</span>
    <span class="c1">// timeout is a node-fetch specific extension</span>
    <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">timeout</span><span class="p">:</span> <span class="mi">5000</span> <span class="p">}).</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> fetching </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
    <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-browser
<span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span>: Executed 1209 of 2460 <span class="o">(</span>skipped 1251<span class="o">)</span> SUCCESS <span class="o">(</span>7.497 secs / 6.723 secs<span class="o">)</span>
TOTAL: 1209 SUCCESS
</code></pre></div></div>

<p>That’s great! Now, since it’s run inside a browser, let’s drop the <code class="language-plaintext highlighter-rouge">node-fetch</code> requirement and use the browser’s <code class="language-plaintext highlighter-rouge">fetch</code>.</p>

<h4 id="testutiljs-5">test/util.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">if</span> <span class="p">(</span><span class="nx">exports</span><span class="p">.</span><span class="nx">inBrowserContext</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">exports</span><span class="p">.</span><span class="nx">getTestFixtureUrl</span><span class="p">(</span><span class="nx">relativePath</span><span class="p">)).</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> fetching </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">location</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
    <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn test-browser
<span class="o">[</span>...]
HeadlessChrome 81.0.4044 <span class="o">(</span>Mac OS X 10.15.4<span class="o">)</span>: Executed 1209 of 2460 <span class="o">(</span>skipped 1251<span class="o">)</span> SUCCESS <span class="o">(</span>7.561 secs / 6.812 secs<span class="o">)</span>
TOTAL: 1209 SUCCESS
</code></pre></div></div>

<p>Great. The best dependency is the one not being used, am I right?</p>

<h2 id="removing-request-from-testweb-platform-testsstart-wpt-serverjs">Removing request from test/web-platform-tests/start-wpt-server.js</h2>

<p>The second <code class="language-plaintext highlighter-rouge">request</code> usage by tests is inside <code class="language-plaintext highlighter-rouge">test/web-platform-tests/start-wpt-server.js</code>.</p>

<h4 id="testweb-platform-testsstart-wpt-serverjs-1">test/web-platform-tests/start-wpt-server.js</h4>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">requestHead</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request-promise-native</span><span class="dl">"</span><span class="p">).</span><span class="nx">head</span><span class="p">;</span>
<span class="c1">// ...</span>
<span class="kd">function</span> <span class="nx">pollForServer</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">requestHead</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">strictSSL</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`WPT server at </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2"> is up!`</span><span class="p">);</span>
      <span class="k">return</span> <span class="nx">url</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`WPT server at </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2"> is not up yet (</span><span class="p">${</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">); trying again`</span><span class="p">);</span>
      <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">pollForServer</span><span class="p">(</span><span class="nx">url</span><span class="p">)),</span> <span class="mi">500</span><span class="p">);</span>
      <span class="p">});</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Based on the name of the file and some of the error messages, it looks like this code is used to check whether WPT is running. This code is used at the start of the test runner. That should be easy enough to test. Let’s replace <code class="language-plaintext highlighter-rouge">request</code> with <code class="language-plaintext highlighter-rouge">node-fetch</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">strictSSL</code> option is no part of the Fetch standard, but stack overflow tells me I can use <a href="https://stackoverflow.com/a/59944400"><code class="language-plaintext highlighter-rouge">rejectUnauthorized: false</code></a> instead.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetch</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">node-fetch</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">https</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>

<span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span>
  <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">});</span>

<span class="kd">function</span> <span class="nx">pollForServer</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">agent</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">?</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
    <span class="p">:</span> <span class="kc">null</span><span class="p">;</span>
  <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">HEAD</span><span class="dl">"</span><span class="p">,</span> <span class="nx">agent</span> <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(({</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">status</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">status</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`WPT server at </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2"> is up!`</span><span class="p">);</span>
      <span class="k">return</span> <span class="nx">url</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`WPT server at </span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2"> is not up yet (</span><span class="p">${</span><span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">); trying again`</span><span class="p">);</span>
      <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">pollForServer</span><span class="p">(</span><span class="nx">url</span><span class="p">)),</span> <span class="mi">500</span><span class="p">);</span>
      <span class="p">});</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’ve added <code class="language-plaintext highlighter-rouge">throw new Error("Foo")</code> (not shown above) to intentionally fail at first. Let’s run the tests and see if they fail. I’ll assume they fail early, so I’ll run all tests.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn <span class="nb">test</span>
<span class="o">[</span>...]
  1<span class="o">)</span> <span class="s2">"before all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>
  2<span class="o">)</span> <span class="s2">"after all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>

  0 passing <span class="o">(</span>22ms<span class="o">)</span>
  2 failing

  1<span class="o">)</span> <span class="s2">"before all"</span> hook: <span class="nv">$mochaNoSugar</span> <span class="k">in</span> <span class="s2">"{root}"</span>:
     Error: foo
</code></pre></div></div>

<p>I was right. Let’s kill it and retry without the intentional failure.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn <span class="nb">test</span>
<span class="o">[</span>...]
</code></pre></div></div>

<p>The tests are running again. I let them run, but I assume my change is fine.</p>

<h2 id="removing-request-from-libjsdombrowserresourcesresource-loaderjs">Removing request from lib/jsdom/browser/resources/resource-loader.js</h2>

<p>Now that the test utilities are fixed, let’s get our hands dirty on the lib code. There are only 2 files where <code class="language-plaintext highlighter-rouge">request</code> is actually invoked. The 3rd is only a helper:</p>

<h4 id="libjsdomlivinghelperswrap-cookie-jar-for-requestjs-1">lib/jsdom/living/helpers/wrap-cookie-jar-for-request.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use strict</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request</span><span class="dl">"</span><span class="p">);</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">cookieJar</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">jarWrapper</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">jar</span><span class="p">();</span>
  <span class="nx">jarWrapper</span><span class="p">.</span><span class="nx">_jar</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">;</span>
  <span class="k">return</span> <span class="nx">jarWrapper</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Since this helper is a dependency of the other 2 files, I’ll look at the helper last. Let’s first look at <code class="language-plaintext highlighter-rouge">resource-loader</code>.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-1">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">request-promise-native</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">wrapCookieJarForRequest</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">../../living/helpers/wrap-cookie-jar-for-request</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// ...</span>
  <span class="nx">_getRequestOptions</span><span class="p">({</span> <span class="nx">cookieJar</span><span class="p">,</span> <span class="nx">referrer</span><span class="p">,</span> <span class="nx">accept</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span> <span class="p">})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">requestOptions</span> <span class="o">=</span> <span class="p">{</span>
      <span class="na">encoding</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
      <span class="na">gzip</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">jar</span><span class="p">:</span> <span class="nx">wrapCookieJarForRequest</span><span class="p">(</span><span class="nx">cookieJar</span><span class="p">),</span>
      <span class="na">strictSSL</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span><span class="p">,</span>
      <span class="na">proxy</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">,</span>
      <span class="na">forever</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">User-Agent</span><span class="dl">"</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_userAgent</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">Accept-Language</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">en</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">Accept</span><span class="p">:</span> <span class="nx">accept</span>
      <span class="p">}</span>
    <span class="p">};</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">referrer</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">requestOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referer</span> <span class="o">=</span> <span class="nx">referrer</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">requestOptions</span><span class="p">;</span>
  <span class="p">}</span>
<span class="c1">// ...</span>
  <span class="nx">fetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">parseURL</span><span class="p">(</span><span class="nx">urlString</span><span class="p">);</span>
    <span class="c1">// ...</span>
    <span class="k">switch</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">scheme</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// ...</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">requestOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getRequestOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">request</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">requestOptions</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="c1">// ...</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Seems easy enough. Let’s convert the request options to fetch options.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">encoding: null</code>: This causes request to return a buffer. With <code class="language-plaintext highlighter-rouge">node-fetch</code>, we might be able to use <code class="language-plaintext highlighter-rouge">response.arrayBuffer()</code> for that.</li>
  <li><code class="language-plaintext highlighter-rouge">jar: wrapCookieJarForRequest(cookieJar)</code>: Somehow cookies are reused this way. The <code class="language-plaintext highlighter-rouge">cookieJar</code> variable is converted to a request-compatible cookie jar to allow keeping track of cookies. I don’t know if <code class="language-plaintext highlighter-rouge">fetch</code> has features like this. I suppose we can just manually read/write the cookies.</li>
  <li><code class="language-plaintext highlighter-rouge">strictSSL: this._strictSSL</code>: Just like before, use the HTTPS agent with <code class="language-plaintext highlighter-rouge">rejectUnauthorized</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">proxy: this._proxy</code>: Enables proxy. There is no obvious way to implement this in <code class="language-plaintext highlighter-rouge">node-fetch</code>. I also don’t know what’s in <code class="language-plaintext highlighter-rouge">this._proxy</code>. I might need to use <a href="https://www.npmjs.com/package/https-proxy-agent"><code class="language-plaintext highlighter-rouge">https-proxy-agent</code></a> for this.</li>
  <li><code class="language-plaintext highlighter-rouge">forever: true</code>: Sets keepAlive on the HTTPS agent. Since we’re replacing the agent anyway, we might as well set <code class="language-plaintext highlighter-rouge">keepAlive: true</code> for both http and https agents.</li>
</ul>

<p>Let’s make a first attempt to implement resource-loader’s fetch function using fetch instead of request. Because I don’t know how to implement the proxy or cookies, I’ll ignore those for now.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-2">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">_getFetchOptions</span><span class="p">({</span> <span class="nx">cookieJar</span><span class="p">,</span> <span class="nx">referrer</span><span class="p">,</span> <span class="nx">accept</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span> <span class="p">})</span> <span class="p">{</span>
  <span class="cm">/** @type RequestInit */</span>
  <span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="p">{};</span>

  <span class="c1">// I don't know what these variables hold exactly - let's log them!</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">cookieJar</span><span class="dl">"</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">this._proxy</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">);</span>

  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">User-Agent</span><span class="dl">"</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_userAgent</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Accept-Language</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">en</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Accept</span><span class="p">:</span> <span class="nx">accept</span><span class="p">,</span>
  <span class="p">};</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">});</span>

    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referrer</span> <span class="o">=</span> <span class="nx">referrer</span><span class="p">;</span>
    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// ...</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">:</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">))</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
    <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s run the tests and see the mess I’ve created. I get a lot of failures from the tests, as expected. Some are related to cookies. The <code class="language-plaintext highlighter-rouge">console.log</code>s look like this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cookieJar CookieJar <span class="o">{</span> enableLooseMode: <span class="nb">true</span>, store: <span class="o">{</span> idx: <span class="o">{}</span> <span class="o">}</span> <span class="o">}</span>
this._proxy undefined

cookieJar CookieJar <span class="o">{</span> enableLooseMode: <span class="nb">true</span>, store: <span class="o">{</span> idx: <span class="o">{}</span> <span class="o">}</span> <span class="o">}</span>
this._proxy http://127.0.0.1:51388
</code></pre></div></div>

<p>So the proxy is just a URL. I’m not sure how to implement the proxy from fetch, if it is even possible. I suppose I can use a proxy agent on the server, but I don’t know an alternative for the browser.</p>

<p>The cookie jar is still a mystery. Since <code class="language-plaintext highlighter-rouge">package.json</code> mentions <code class="language-plaintext highlighter-rouge">tough-cookie</code>, I’ll just assume the cookie jar is from that library. I’m just going to assume this is also only used server-side, since the browser’s fetch handles cookies automatically.</p>

<p>To add <code class="language-plaintext highlighter-rouge">tough-cookie</code>’s cookie-jar to <code class="language-plaintext highlighter-rouge">node-fetch</code>, I’m going to use a library called <a href="https://www.npmjs.com/package/fetch-cookie"><code class="language-plaintext highlighter-rouge">fetch-cookie</code></a>. <code class="language-plaintext highlighter-rouge">fetch-cookie</code> <a href="https://www.npmjs.com/package/fetch-cookie?activeTab=dependencies">has no other dependencies except for <code class="language-plaintext highlighter-rouge">tough-cookie</code></a> so it can be used independently from Fetch implementations. <code class="language-plaintext highlighter-rouge">fetch-cookie</code> is also pretty small: <a href="https://github.com/valeriangalliat/fetch-cookie/blob/master/index.js">about 50 lines of code</a>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add fetch-cookie
</code></pre></div></div>

<h4 id="libjsdombrowserresourcesresource-loaderjs-3">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">_getFetchOptions</span><span class="p">({</span> <span class="nx">cookieJar</span><span class="p">,</span> <span class="nx">referrer</span><span class="p">,</span> <span class="nx">accept</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span> <span class="p">})</span> <span class="p">{</span>
  <span class="cm">/** @type RequestInit */</span>
  <span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="p">{};</span>

  <span class="c1">// I don't know what these variables hold exactly - let's log them!</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">cookieJar</span><span class="dl">"</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">this._proxy</span><span class="dl">"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">);</span>

  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">User-Agent</span><span class="dl">"</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_userAgent</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Accept-Language</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">en</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Accept-Encoding</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gzip</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Accept</span><span class="p">:</span> <span class="nx">accept</span><span class="p">,</span>
  <span class="p">};</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">});</span>

    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referrer</span> <span class="o">=</span> <span class="nx">referrer</span><span class="p">;</span>
    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// ...</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">:</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">cookieJar</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">;</span>
  <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span><span class="p">;</span>
  <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">args</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">{});</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">args</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">ignoreError</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span><span class="p">(...</span><span class="nx">args</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">targetFetch</span> <span class="o">=</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">fetch</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I got a lot of errors when handling the cookies. Turns out, when adding cookies, the <code class="language-plaintext highlighter-rouge">request</code> library sets <code class="language-plaintext highlighter-rouge">ignoreError</code> on <code class="language-plaintext highlighter-rouge">true</code> by default (like a browser would do), but <code class="language-plaintext highlighter-rouge">fetch-cookie</code> doesn’t allow you to change the options when setting cookies.</p>

<p>To “fix” this, I hijacked the <code class="language-plaintext highlighter-rouge">setCookie</code> function to silence the errors, only to get different errors. I’ll find a proper fix later.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1) Cookie processing
      document.cookie
        reflects back cookies set from the server while requesting the page:
    TypeError: Cannot read property 'headers' of undefined
    at /Users/hinloopen/Projects/Github/jsdom/lib/api.js:138:28
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
</code></pre></div></div>

<p>Let’s see what’s inside <code class="language-plaintext highlighter-rouge">lib/api.js</code>:</p>

<h4 id="libapijs">lib/api.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">req</span> <span class="o">=</span> <span class="nx">resourceLoaderForInitialRequest</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">cookieJar</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">,</span>
  <span class="na">referrer</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">referrer</span>
<span class="p">});</span>

<span class="k">return</span> <span class="nx">req</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">body</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">response</span><span class="p">;</span>

  <span class="nx">options</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">url</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">href</span> <span class="o">+</span> <span class="nx">originalHash</span><span class="p">,</span>
    <span class="na">contentType</span><span class="p">:</span> <span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">referrer</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">getHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">referer</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="k">new</span> <span class="nx">JSDOM</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>So that’s interesting. Apparently, the promise returned by <code class="language-plaintext highlighter-rouge">request-promise</code> not only has a <code class="language-plaintext highlighter-rouge">.then</code> method, it also has a <code class="language-plaintext highlighter-rouge">.response</code> property containing the response. I didn’t know that, and I don’t see it documented anywhere on the <a href="https://www.npmjs.com/package/request-promise"><code class="language-plaintext highlighter-rouge">request-promise</code> readme</a>. I would just have used <code class="language-plaintext highlighter-rouge">resolveWithFullResponse</code> but whatever.</p>

<p>Let’s see if we can replicate this behavior.</p>

<p>We need to return a promise-like object that has a <code class="language-plaintext highlighter-rouge">.then</code> and a <code class="language-plaintext highlighter-rouge">.catch</code> (like a promise), but it also needs to have a <code class="language-plaintext highlighter-rouge">.response</code> getter, <code class="language-plaintext highlighter-rouge">.href</code> getter, and a <code class="language-plaintext highlighter-rouge">.getHeader</code> function.</p>

<p>Again, quick and dirty, let’s make it work the easiest way possible.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-4">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">cookieJar</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">;</span>
<span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span><span class="p">;</span>
<span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="kd">const</span> <span class="nx">targetFetch</span> <span class="o">=</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">fetch</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fetchResult</span> <span class="o">=</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
<span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">href</span><span class="p">:</span> <span class="nx">urlString</span><span class="p">,</span>
  <span class="na">then</span><span class="p">:</span> <span class="nx">fetchResult</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">}).</span><span class="nx">then</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">fetchResult</span><span class="p">),</span>
  <span class="na">catch</span><span class="p">:</span> <span class="nx">fetchResult</span><span class="p">.</span><span class="k">catch</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">fetchResult</span><span class="p">),</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
</code></pre></div></div>

<p>The previously failing test now succeeds, but many others still fail. Let’s fix the next one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1) Cookie processing
       should share cookies when a cookie jar is shared:
     TypeError: Cannot read property 'innerHTML' of null
      at /Users/hinloopen/Projects/Github/jsdom/test/api/cookies.js:288:75
      at processTicksAndRejections (internal/process/task_queues.js:93:5)
</code></pre></div></div>

<h4 id="testapicookiesjs">test/api/cookies.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">should share cookies when a cookie jar is shared</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">cookieJar</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CookieJar</span><span class="p">();</span>

  <span class="k">return</span> <span class="nx">JSDOM</span><span class="p">.</span><span class="nx">fromURL</span><span class="p">(</span><span class="nx">testHost</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/TestPath/set-cookie-from-server</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">cookieJar</span> <span class="p">}).</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">JSDOM</span><span class="p">.</span><span class="nx">fromURL</span><span class="p">(</span><span class="nx">testHost</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/TestPath/html-get-cookie-header</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">cookieJar</span> <span class="p">});</span>
  <span class="p">}).</span><span class="nx">then</span><span class="p">(({</span> <span class="nb">window</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">cookieHeader</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">.cookie-header</span><span class="dl">"</span><span class="p">).</span><span class="nx">innerHTML</span><span class="p">;</span>

    <span class="nx">assertCookies</span><span class="p">(</span><span class="nx">cookieHeader</span><span class="p">,</span> <span class="p">[</span>
      <span class="dl">"</span><span class="s2">Test1=Basic</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test2=PathMatch</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test6=HttpOnly</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test9=Duplicate</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test10={</span><span class="se">\"</span><span class="s2">prop1</span><span class="se">\"</span><span class="s2">:5,</span><span class="se">\"</span><span class="s2">prop2</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">value</span><span class="se">\"</span><span class="s2">}</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Malformed</span><span class="dl">"</span>
    <span class="p">]);</span>

    <span class="nx">assertCookies</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="p">,</span> <span class="p">[</span>
      <span class="dl">"</span><span class="s2">Test1=Basic</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test2=PathMatch</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test9=Duplicate</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Test10={</span><span class="se">\"</span><span class="s2">prop1</span><span class="se">\"</span><span class="s2">:5,</span><span class="se">\"</span><span class="s2">prop2</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">value</span><span class="se">\"</span><span class="s2">}</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">Malformed</span><span class="dl">"</span>
    <span class="p">]);</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p>So the <code class="language-plaintext highlighter-rouge">.cookie-header</code> element couldn’t be found in the <code class="language-plaintext highlighter-rouge">/html-get-cookie-header</code> page. Maybe there is a hint somewhere in the document’s HTML. Let’s log <code class="language-plaintext highlighter-rouge">window.document.body.innerHTML</code> using <code class="language-plaintext highlighter-rouge">console.log({ html: window.document.body.innerHTML });</code></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nl">html</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[object Response]</span><span class="dl">'</span> <span class="p">}</span>
</code></pre></div></div>

<p>I strongly suspect somewhere inside my new fetch implementation, the HTML body’s <code class="language-plaintext highlighter-rouge">toString</code> returns <code class="language-plaintext highlighter-rouge">"[object Response]"</code>. Let’s check our implementation again.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-5">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fetchPromise</span> <span class="o">=</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">);</span>
<span class="p">};</span>

<span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">href</span><span class="p">:</span> <span class="nx">urlString</span><span class="p">,</span>
  <span class="nx">then</span><span class="p">,</span>
  <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">},</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
</code></pre></div></div>

<p>Now we get, yet again, different errors. One includes <code class="language-plaintext highlighter-rouge">The "buf" argument must be one of type Buffer, TypedArray, or DataView. Received type object</code>. I suspect this has to do with the <code class="language-plaintext highlighter-rouge">ArrayBuffer</code> returned by <code class="language-plaintext highlighter-rouge">node-fetch</code>: This is NOT the same as a NodeJS <code class="language-plaintext highlighter-rouge">Buffer</code>. Let’s make it a <code class="language-plaintext highlighter-rouge">Buffer</code> for NodeJS only:</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-6">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">arrayBuffer</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">Buffer</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">arrayBuffer</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The next error I encounter is this one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1) API: resource loading configuration
       set to "usable"
         canceling requests
           should abort a script request (with no events) when stopping the window:
     TypeError: openedRequest.abort is not a function
      at RequestManager.close (lib/jsdom/browser/resources/request-manager.js:25:21)
      at Window.stop (lib/jsdom/browser/Window.js:608:15)
      at /Users/hinloopen/Projects/Github/jsdom/test/api/resources.js:559:20
      at processTicksAndRejections (internal/process/task_queues.js:93:5)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.abort</code> is not a function. Is <code class="language-plaintext highlighter-rouge">openedRequest</code> our fetch result?</p>

<h4 id="libjsdombrowserresourcesrequest-managerjs">lib/jsdom/browser/resources/request-manager.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Manage all the request and it is able to abort
 * all pending request.
 */</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">class</span> <span class="nx">RequestManager</span> <span class="p">{</span>
  <span class="c1">// ...</span>
  <span class="nx">close</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">openedRequest</span> <span class="k">of</span> <span class="k">this</span><span class="p">.</span><span class="nx">openedRequests</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">openedRequest</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">openedRequests</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="p">}</span>
  <span class="c1">// ...</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Let’s implement <code class="language-plaintext highlighter-rouge">.abort</code>, make it do nothing, and see if the error changes.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-7">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">abort</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">TODO ABORT</span><span class="dl">"</span><span class="p">);</span> <span class="p">},</span>
  <span class="na">href</span><span class="p">:</span> <span class="nx">urlString</span><span class="p">,</span>
  <span class="nx">then</span><span class="p">,</span>
  <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">},</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TODO ABORT
Error: Could not load script: "http://127.0.0.1:58978/"
  1) API: resource loading configuration
       set to "usable"
         canceling requests
           should abort a script request (with no events) when stopping the window:

      The error event must not fire
      + expected - actual

      -true
      +false

      at /Users/hinloopen/Projects/Github/jsdom/test/api/resources.js:920:12
      at async Promise.all (index 0)
      at async /Users/hinloopen/Projects/Github/jsdom/test/api/resources.js:561:9
</code></pre></div></div>

<p>Right, time to properly implement <code class="language-plaintext highlighter-rouge">.abort</code>. Can we even implement <code class="language-plaintext highlighter-rouge">.abort</code> using the browser’s Fetch API? According to MDN, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort">it is experimental technology</a>. Browser support might be incomplete, but I suspect it’s only used in NodeJS anyway.</p>

<p><code class="language-plaintext highlighter-rouge">node-fetch</code> also supports aborting requests, and it is implemented in <a href="https://www.npmjs.com/package/node-fetch#request-cancellation-with-abortsignal">the same way</a>! It requires an <code class="language-plaintext highlighter-rouge">AbortController</code> implementation - <code class="language-plaintext highlighter-rouge">abort-controller</code> is suggested.</p>

<h4 id="sh">sh</h4>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add abort-controller
</code></pre></div></div>

<h4 id="libjsdombrowserresourcesresource-loaderjs-8">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AbortController</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">abort-controller</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// ...</span>
<span class="kd">const</span> <span class="nx">targetFetch</span> <span class="o">=</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">fetch</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">abortController</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
<span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">signal</span> <span class="o">=</span> <span class="nx">abortController</span><span class="p">.</span><span class="nx">signal</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">fetchPromise</span> <span class="o">=</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">arrayBuffer</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">typeof</span> <span class="nx">Buffer</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">arrayBuffer</span> <span class="p">:</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">))</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">);</span>
<span class="p">};</span>

<span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">abort</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">abortController</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span> <span class="p">},</span>
  <span class="na">href</span><span class="p">:</span> <span class="nx">urlString</span><span class="p">,</span>
  <span class="nx">then</span><span class="p">,</span>
  <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">},</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Using abort still throws an error, causing the test to fail:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error: Could not load script: "http://127.0.0.1:61567/"

# ...

  type: 'aborted',
  message: 'The user aborted a request.'

# ...

  1) API: resource loading configuration
       set to "usable"
         canceling requests
           should abort a script request (with no events) when stopping the window:

      The error event must not fire
      + expected - actual

      -true
      +false
</code></pre></div></div>

<p>I’m not sure how <code class="language-plaintext highlighter-rouge">request</code> would have handled the abort, but based on this failure, it wasn’t by throwing an error. I can’t find any documentation about it. The source seems to just cancel the request and destroy the response without throwing an error. Maybe the promise just never resolves?</p>

<p>Let’s implement it that way, see if it works.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-9">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">arrayBuffer</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">typeof</span> <span class="nx">Buffer</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">arrayBuffer</span> <span class="p">:</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">))</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">return</span> <span class="nx">onfulfilled</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span> <span class="p">})</span>
  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">return</span> <span class="nx">onrejected</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span> <span class="p">});</span>
<span class="p">};</span>

<span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">abort</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nx">abortController</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
  <span class="p">},</span>
  <span class="na">href</span><span class="p">:</span> <span class="nx">urlString</span><span class="p">,</span>
  <span class="nx">then</span><span class="p">,</span>
  <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span>
  <span class="p">},</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>A lot of green tests this round! Looking good. Still, there are tens of failing tests, some mentioning the proxy. Others mentioning the <code class="language-plaintext highlighter-rouge">Referer</code> header.</p>

<p>It looks like I assigned the referrer to a header named <code class="language-plaintext highlighter-rouge">Referrer</code> instead of <code class="language-plaintext highlighter-rouge">Referer</code>. Let’s fix that and look at the next error.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-10">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// inside _getFetchOptions</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">});</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">referrer</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referer</span> <span class="o">=</span> <span class="nx">referrer</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The other two errors are going to be a problem, and are related to redirects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1) Cookie processing
       sent with requests
         should gather cookies from redirects (GH-1089):

      AssertionError: expected [ 'Test3=Redirect3' ] to deeply equal [ Array(3) ]
      + expected - actual

       [
      +  "Test1=Redirect1"
      +  "Test2=Redirect2"
         "Test3=Redirect3"
       ]

      at assertCookies (test/api/cookies.js:383:10)
      at /Users/hinloopen/Projects/Github/jsdom/test/api/cookies.js:247:9
      at processTicksAndRejections (internal/process/task_queues.js:93:5)

  2) API: JSDOM.fromURL()
       referrer
         should use the redirect source URL as the referrer, overriding a provided one:

      AssertionError: expected 'http://example.com/' to equal 'http://127.0.0.1:55863/1'
      + expected - actual

      -http://example.com/
      +http://127.0.0.1:55863/1

      at /Users/hinloopen/Projects/Github/jsdom/test/api/from-url.js:135:14
      at processTicksAndRejections (internal/process/task_queues.js:93:5)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fetch</code> uses transparent redirects, and it appears that <code class="language-plaintext highlighter-rouge">fetch-cookie</code> doesn’t store cookies around redirects. Reading the documentation, there is actually <a href="https://www.npmjs.com/package/fetch-cookie#cookies-on-redirects">a fix</a> for that. Let’s apply that fix.</p>

<p>It looks like it is as easy as changing the require to <code class="language-plaintext highlighter-rouge">const fetchCookie = require('fetch-cookie/node-fetch');</code>. Let’s do that, and re-run the tests.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1) API: JSDOM.fromURL()
       referrer
         should use the redirect source URL as the referrer, overriding a provided one:

      AssertionError: expected 'http://example.com/' to equal 'http://127.0.0.1:56188/1'
      + expected - actual

      -http://example.com/
      +http://127.0.0.1:56188/1
</code></pre></div></div>

<p>The other error is gone. Now let’s see how we fix this one. I can make an educated guess what’s being tested here, but let’s look at the source.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">should use the redirect source URL as the referrer, overriding a provided one</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">requestURL</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">redirectServer</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;p&gt;Hello&lt;/p&gt;</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span> <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">dom</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JSDOM</span><span class="p">.</span><span class="nx">fromURL</span><span class="p">(</span><span class="nx">requestURL</span><span class="p">,</span> <span class="p">{</span> <span class="na">referrer</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://example.com/</span><span class="dl">"</span> <span class="p">});</span>
  <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">referrer</span><span class="p">,</span> <span class="nx">requestURL</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>So… it’s checking <code class="language-plaintext highlighter-rouge">document.referrer</code>. I’ve no idea where this is assigned and I don’t want to find out. Instead, since this test is testing <code class="language-plaintext highlighter-rouge">JSDOM.fromURL</code> specifically, let’s see if <code class="language-plaintext highlighter-rouge">JSDOM.fromURL</code> assigns the <code class="language-plaintext highlighter-rouge">referrer</code> somewhere.</p>

<h4 id="libapijs-1">lib/api.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="nx">fromURL</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">().</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Remove the hash while sending this through the research loader fetch().</span>
    <span class="c1">// It gets added back a few lines down when constructing the JSDOM object.</span>
    <span class="kd">const</span> <span class="nx">parsedURL</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">originalHash</span> <span class="o">=</span> <span class="nx">parsedURL</span><span class="p">.</span><span class="nx">hash</span><span class="p">;</span>
    <span class="nx">parsedURL</span><span class="p">.</span><span class="nx">hash</span> <span class="o">=</span> <span class="dl">""</span><span class="p">;</span>
    <span class="nx">url</span> <span class="o">=</span> <span class="nx">parsedURL</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>

    <span class="nx">options</span> <span class="o">=</span> <span class="nx">normalizeFromURLOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">resourceLoader</span> <span class="o">=</span> <span class="nx">resourcesToResourceLoader</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">resources</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">resourceLoaderForInitialRequest</span> <span class="o">=</span> <span class="nx">resourceLoader</span><span class="p">.</span><span class="kd">constructor</span> <span class="o">===</span> <span class="nx">NoOpResourceLoader</span> <span class="p">?</span>
      <span class="k">new</span> <span class="nx">ResourceLoader</span><span class="p">()</span> <span class="p">:</span>
      <span class="nx">resourceLoader</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">req</span> <span class="o">=</span> <span class="nx">resourceLoaderForInitialRequest</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">cookieJar</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">,</span>
      <span class="na">referrer</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">referrer</span>
    <span class="p">});</span>

    <span class="k">return</span> <span class="nx">req</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">body</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">response</span><span class="p">;</span>

      <span class="nx">options</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">url</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">href</span> <span class="o">+</span> <span class="nx">originalHash</span><span class="p">,</span>
        <span class="na">contentType</span><span class="p">:</span> <span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">],</span>
        <span class="na">referrer</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">getHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">referer</span><span class="dl">"</span><span class="p">)</span>
      <span class="p">});</span>

      <span class="k">return</span> <span class="k">new</span> <span class="nx">JSDOM</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Interesting - it uses this <code class="language-plaintext highlighter-rouge">req.getHeader("referer")</code>. <code class="language-plaintext highlighter-rouge">req</code> is the object I’m returning, so it actually calls my <code class="language-plaintext highlighter-rouge">getHeader</code> function. This function returns the header of the first request.</p>

<p>This is a problem: Because the request was redirected, a new request was started. However, my <code class="language-plaintext highlighter-rouge">getHeader</code> fetches the header of the first request, not the last request in the redirect chain.</p>

<p>This is also an issue for <code class="language-plaintext highlighter-rouge">req.href</code>, which returns the first request URL, not the last, but I haven’t confirmed a failing test for this problem.</p>

<p>Let’s see if we can peek into the redirect requests. Since <code class="language-plaintext highlighter-rouge">fetch-cookie</code> also fixed this problem for assigning cookies, I bet their fix shows how you can peek into redirect requests. Let’s take a look at <a href="https://github.com/valeriangalliat/fetch-cookie/blob/master/node-fetch.js"><code class="language-plaintext highlighter-rouge">fetch-cookie/node-fetch</code></a></p>

<h4 id="fetch-cookies-node-fetchjs">fetch-cookie’s node-fetch.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">nodeFetchCookieDecorator</span> <span class="p">(</span><span class="nx">nodeFetch</span><span class="p">,</span> <span class="nx">jar</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">fetchCookie</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./</span><span class="dl">'</span><span class="p">)(</span><span class="nx">nodeFetch</span><span class="p">,</span> <span class="nx">jar</span><span class="p">)</span>

  <span class="k">return</span> <span class="kd">function</span> <span class="nx">nodeFetchCookie</span> <span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">userOptions</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">opts</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">userOptions</span><span class="p">,</span> <span class="p">{</span> <span class="na">redirect</span><span class="p">:</span> <span class="dl">'</span><span class="s1">manual</span><span class="dl">'</span> <span class="p">})</span>

    <span class="c1">// Forward identical options to wrapped node-fetch but tell to not handle redirection.</span>
    <span class="k">return</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">opts</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">isRedirect</span> <span class="o">=</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">303</span> <span class="o">||</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">301</span> <span class="o">||</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">302</span> <span class="o">||</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">307</span><span class="p">)</span>

        <span class="c1">// Interpret the proprietary "redirect" option in the same way that node-fetch does.</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">isRedirect</span> <span class="o">&amp;&amp;</span> <span class="nx">userOptions</span><span class="p">.</span><span class="nx">redirect</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">manual</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">userOptions</span><span class="p">.</span><span class="nx">follow</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">statusOpts</span> <span class="o">=</span> <span class="p">{</span>
            <span class="c1">// Since the "follow" flag is not relevant for node-fetch in this case,</span>
            <span class="c1">// we'll hijack it for our internal bookkeeping.</span>
            <span class="na">follow</span><span class="p">:</span> <span class="nx">userOptions</span><span class="p">.</span><span class="nx">follow</span> <span class="o">!==</span> <span class="kc">undefined</span> <span class="p">?</span> <span class="nx">userOptions</span><span class="p">.</span><span class="nx">follow</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">:</span> <span class="kc">undefined</span>
          <span class="p">}</span>

          <span class="k">if</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="mi">307</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">statusOpts</span><span class="p">.</span><span class="nx">method</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span>
            <span class="nx">statusOpts</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="kc">null</span>
          <span class="p">}</span>

          <span class="kd">const</span> <span class="nx">redirectOpts</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">userOptions</span><span class="p">,</span> <span class="nx">statusOpts</span><span class="p">)</span>

          <span class="k">return</span> <span class="nx">nodeFetchCookie</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">location</span><span class="dl">'</span><span class="p">),</span> <span class="nx">redirectOpts</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="k">return</span> <span class="nx">res</span>
        <span class="p">}</span>
      <span class="p">})</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So basically, their fix is to set the redirect-mode to <code class="language-plaintext highlighter-rouge">manual</code> and just call <code class="language-plaintext highlighter-rouge">fetch</code> again for every redirect. Because it calls <code class="language-plaintext highlighter-rouge">fetch</code> for every redirect, the cookies can be assigned and extracted every request by <code class="language-plaintext highlighter-rouge">fetch-cookie</code>.</p>

<p>The easiest way to keep track of all the redirect requests without also interfering with <code class="language-plaintext highlighter-rouge">fetch-cookie</code>’s fix is by wrapping the <code class="language-plaintext highlighter-rouge">node-fetch</code> instance, keeping track of the last request.</p>

<p>Let’s try that.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-11">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">_getFetchOptions</span><span class="p">({</span> <span class="nx">accept</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">*/*</span><span class="dl">"</span> <span class="p">})</span> <span class="p">{</span>
  <span class="cm">/** @type RequestInit */</span>
  <span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="p">{};</span>

  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">User-Agent</span><span class="dl">"</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_userAgent</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Accept-Language</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">en</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">Accept-Encoding</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gzip</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Accept</span><span class="p">:</span> <span class="nx">accept</span><span class="p">,</span>
  <span class="p">};</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">});</span>
    <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">fetchOptions</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// inside fetch(urlString, options = {})</span>
<span class="kd">let</span> <span class="nx">lastUrl</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">referrer</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">lastOpts</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">myFetch</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">opts</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">lastUrl</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">opts</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referer</span> <span class="o">=</span> <span class="nx">lastUrl</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="nx">lastUrl</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
  <span class="nx">lastOpts</span> <span class="o">=</span> <span class="nx">opts</span><span class="p">;</span>
  <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">opts</span><span class="p">);</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">targetFetch</span> <span class="o">=</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">myFetch</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">abortController</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
<span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">signal</span> <span class="o">=</span> <span class="nx">abortController</span><span class="p">.</span><span class="nx">signal</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">fetchPromise</span> <span class="o">=</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
    <span class="nx">result</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">lastUrl</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">arrayBuffer</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">typeof</span> <span class="nx">Buffer</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">arrayBuffer</span> <span class="p">:</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">))</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">return</span> <span class="nx">onfulfilled</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span> <span class="p">})</span>
  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">onrejected</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">});</span>
<span class="p">};</span>

<span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">abort</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nx">abortController</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
  <span class="p">},</span>
  <span class="na">href</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="nx">then</span><span class="p">,</span>
  <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span>
  <span class="p">},</span>
  <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">lastOpts</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
</code></pre></div></div>

<p>So we now have <code class="language-plaintext highlighter-rouge">fetch</code>, <code class="language-plaintext highlighter-rouge">myFetch</code> and <code class="language-plaintext highlighter-rouge">targetFetch</code>. Bad variable names aside, the redirect-related failures seem solved. Let’s run the tests and look at the next errors.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ...</span>
      with a Content-Type header specifying csiso88598e
        1<span class="o">)</span> should sniff no-bom-charset-http-equiv-no-quotes.html as ISO-8859-8
        2<span class="o">)</span> should sniff no-bom-charset-http-equiv-tis-620.html as ISO-8859-8
        3<span class="o">)</span> should sniff no-bom-charset-koi8.html as ISO-8859-8
        4<span class="o">)</span> should sniff no-bom-charset-utf-16.html as ISO-8859-8
        5<span class="o">)</span> should sniff no-bom-charset-utf-16be.html as ISO-8859-8
        6<span class="o">)</span> should sniff no-bom-charset-utf-16le.html as ISO-8859-8
        7<span class="o">)</span> should sniff no-bom-no-charset.html as ISO-8859-8
<span class="c"># ...</span>
  2<span class="o">)</span> API: encoding detection
       fromURL
         with a Content-Type header specifying csiso88598e
           should sniff no-bom-charset-http-equiv-tis-620.html as ISO-8859-8:

      AssertionError: expected <span class="s1">'windows-874'</span> to equal <span class="s1">'ISO-8859-8'</span>
      + expected - actual

      <span class="nt">-windows-874</span>
      +ISO-8859-8
<span class="c"># ...</span>
</code></pre></div></div>

<p>I have questions. Maybe the test provides some details.</p>

<h4 id="testapiencodingjs">test/api/encoding.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">fromURL</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">skipIfBrowser</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">server</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">host</span><span class="p">;</span>
  <span class="nx">before</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">createServer</span><span class="p">((</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">[,</span> <span class="nx">fixture</span><span class="p">,</span> <span class="nx">query</span><span class="p">]</span> <span class="o">=</span> <span class="sr">/^</span><span class="se">\/([^</span><span class="sr">?</span><span class="se">]</span><span class="sr">+</span><span class="se">)(\?</span><span class="sr">.*</span><span class="se">)?</span><span class="sr">$/</span><span class="p">.</span><span class="nx">exec</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>

      <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span> <span class="p">};</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">query</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">?charset=csiso88598e</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">]</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">text/html;charset=csiso88598e</span><span class="dl">"</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="nx">headers</span><span class="p">);</span>
      <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="nx">fixturePath</span><span class="p">(</span><span class="nx">fixture</span><span class="p">)).</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">res</span><span class="p">);</span>
    <span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">s</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">server</span> <span class="o">=</span> <span class="nx">s</span><span class="p">;</span>
      <span class="nx">host</span> <span class="o">=</span> <span class="s2">`http://127.0.0.1:</span><span class="p">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">address</span><span class="p">().</span><span class="nx">port</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    <span class="p">});</span>
  <span class="p">});</span>

  <span class="nx">after</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">server</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>

  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">with no Content-Type header given</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">encodingFixture</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">encodingFixtures</span><span class="p">))</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">body</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">encodingFixtures</span><span class="p">[</span><span class="nx">encodingFixture</span><span class="p">];</span>

      <span class="nx">it</span><span class="p">(</span><span class="s2">`should sniff </span><span class="p">${</span><span class="nx">encodingFixture</span><span class="p">}</span><span class="s2"> as </span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">JSDOM</span><span class="p">.</span><span class="nx">fromURL</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">host</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">encodingFixture</span><span class="p">}</span><span class="s2">`</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">dom</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">characterSet</span><span class="p">,</span> <span class="nx">name</span><span class="p">);</span>
          <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">textContent</span><span class="p">,</span> <span class="nx">body</span><span class="p">);</span>
        <span class="p">});</span>
      <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">});</span>

  <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">with a Content-Type header specifying csiso88598e</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">encodingFixture</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">encodingFixtures</span><span class="p">))</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">nameWhenOverridden</span><span class="p">,</span> <span class="nx">bodyWhenOverridden</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">encodingFixtures</span><span class="p">[</span><span class="nx">encodingFixture</span><span class="p">];</span>

      <span class="nx">it</span><span class="p">(</span><span class="s2">`should sniff </span><span class="p">${</span><span class="nx">encodingFixture</span><span class="p">}</span><span class="s2"> as </span><span class="p">${</span><span class="nx">nameWhenOverridden</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">JSDOM</span><span class="p">.</span><span class="nx">fromURL</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">host</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">encodingFixture</span><span class="p">}</span><span class="s2">?charset=csiso88598e`</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">dom</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">characterSet</span><span class="p">,</span> <span class="nx">nameWhenOverridden</span><span class="p">);</span>
          <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">contentType</span><span class="p">,</span> <span class="dl">"</span><span class="s2">text/html</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// encoding should be stripped</span>

          <span class="k">if</span> <span class="p">(</span><span class="nx">bodyWhenOverridden</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">dom</span><span class="p">.</span><span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">textContent</span><span class="p">,</span> <span class="nx">bodyWhenOverridden</span><span class="p">);</span>
          <span class="p">}</span>
        <span class="p">});</span>
      <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Looking at other tests, this <code class="language-plaintext highlighter-rouge">csiso88598e</code> content-type is also tested when invoking the constructir directly, and the expectations are similar, and these tests are not failing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>constructor, given binary data
  with a contentType option specifying csiso88598e
    Buffer
      ✓ should sniff no-bom-charset-http-equiv-no-quotes.html as ISO-8859-8
      ✓ should sniff no-bom-charset-http-equiv-tis-620.html as ISO-8859-8
      ✓ should sniff no-bom-charset-koi8.html as ISO-8859-8
      ✓ should sniff no-bom-charset-utf-16.html as ISO-8859-8
      ✓ should sniff no-bom-charset-utf-16be.html as ISO-8859-8
      ✓ should sniff no-bom-charset-utf-16le.html as ISO-8859-8
      ✓ should sniff no-bom-no-charset.html as ISO-8859-8
      ✓ should sniff utf-8-bom.html as UTF-8
      ✓ should sniff utf-16be-bom.html as UTF-16BE
      ✓ should sniff utf-16le-bom.html as UTF-16LE

fromURL
  with no Content-Type header given
    ✓ should sniff no-bom-charset-http-equiv-no-quotes.html as ISO-8859-5 (48ms)
    ✓ should sniff no-bom-charset-http-equiv-tis-620.html as windows-874
    ✓ should sniff no-bom-charset-koi8.html as KOI8-R
    ✓ should sniff no-bom-charset-utf-16.html as UTF-8
    ✓ should sniff no-bom-charset-utf-16be.html as UTF-8
    ✓ should sniff no-bom-charset-utf-16le.html as UTF-8
    ✓ should sniff no-bom-no-charset.html as windows-1252
    ✓ should sniff utf-8-bom.html as UTF-8
    ✓ should sniff utf-16be-bom.html as UTF-16BE
    ✓ should sniff utf-16le-bom.html as UTF-16LE
  with a Content-Type header specifying csiso88598e
    1) should sniff no-bom-charset-http-equiv-no-quotes.html as ISO-8859-8
    2) should sniff no-bom-charset-http-equiv-tis-620.html as ISO-8859-8
    3) should sniff no-bom-charset-koi8.html as ISO-8859-8
    4) should sniff no-bom-charset-utf-16.html as ISO-8859-8
    5) should sniff no-bom-charset-utf-16be.html as ISO-8859-8
    6) should sniff no-bom-charset-utf-16le.html as ISO-8859-8
    7) should sniff no-bom-no-charset.html as ISO-8859-8
</code></pre></div></div>

<p>Correctly handling this <code class="language-plaintext highlighter-rouge">csiso88598e</code> content-type should be done by the constructor. Looking at the source and the tests, the constructor accepts a <code class="language-plaintext highlighter-rouge">contentType</code> option that, when equal to <code class="language-plaintext highlighter-rouge">csiso88598e</code>, parses the response as <code class="language-plaintext highlighter-rouge">ISO-8859-8</code>.</p>

<p>Additionally, the test-server returns a <code class="language-plaintext highlighter-rouge">Content-Type: text/html;charset=csiso88598e</code> header. This content-type should be passed to the JSDOM constructor from <code class="language-plaintext highlighter-rouge">fromURL</code>:</p>

<h4 id="libapijs-2">lib/api.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="nx">fromURL</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">().</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">req</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">body</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">response</span><span class="p">;</span>

      <span class="nx">options</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">url</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">href</span> <span class="o">+</span> <span class="nx">originalHash</span><span class="p">,</span>
        <span class="na">contentType</span><span class="p">:</span> <span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">],</span>
        <span class="na">referrer</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">getHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">referer</span><span class="dl">"</span><span class="p">)</span>
      <span class="p">});</span>

      <span class="k">return</span> <span class="k">new</span> <span class="nx">JSDOM</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
    <span class="p">});</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s take a look at <code class="language-plaintext highlighter-rouge">res.headers</code> inside one of the failing tests using <code class="language-plaintext highlighter-rouge">console.log(res.headers, res.headers["content-type"]);</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Headers</span> <span class="p">{</span>
  <span class="p">[</span><span class="nb">Symbol</span><span class="p">(</span><span class="nx">map</span><span class="p">)]:</span> <span class="p">[</span><span class="nb">Object</span><span class="p">:</span> <span class="kc">null</span> <span class="nx">prototype</span><span class="p">]</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">text/html;charset=csiso88598e</span><span class="dl">'</span> <span class="p">],</span>
    <span class="nx">date</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">Mon, 29 Jun 2020 20:44:07 GMT</span><span class="dl">'</span> <span class="p">],</span>
    <span class="nx">connection</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">keep-alive</span><span class="dl">'</span> <span class="p">],</span>
    <span class="dl">'</span><span class="s1">transfer-encoding</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">chunked</span><span class="dl">'</span> <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span> <span class="kc">undefined</span>
</code></pre></div></div>

<p>So the content-type is there, but <code class="language-plaintext highlighter-rouge">res.headers["content-type"]</code> is undefined. That’s because <code class="language-plaintext highlighter-rouge">res.headers</code> is not a regular object, but instead is a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers">Headers object</a>. Apperently, you cannot use the <code class="language-plaintext highlighter-rouge">[]</code> operator to access the <code class="language-plaintext highlighter-rouge">Header</code>’s properties. Instead, you should use <code class="language-plaintext highlighter-rouge">.get</code>.</p>

<p>For backwards compatibility, let’s change <code class="language-plaintext highlighter-rouge">response</code> to have a <code class="language-plaintext highlighter-rouge">headers</code> property that’s just a plain JS object.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-12">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// inside `then`</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">status</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="p">]</span> <span class="k">of</span> <span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">headers</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">value</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">status</span><span class="p">,</span>
  <span class="nx">headers</span><span class="p">,</span>
<span class="p">};</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">lastUrl</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
</code></pre></div></div>

<p>All encoding-related tests are now green. Let’s see what’s next. There a lot less failures now, so waiting for a failing test now takes minutes.</p>

<p>There are some interesting failures. A common one is a maximum call stack size exceeded error in <code class="language-plaintext highlighter-rouge">setCookie</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RangeError: Maximum call stack size exceeded
    at Array.values (&lt;anonymous&gt;)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resource-loader.js:148:28)
    at CookieJar.cookieJar.setCookie [as __setCookie] (/Users/hinloopen/Projects/Github/jsdom/lib/jsdom/browser/resources/resou
</code></pre></div></div>

<p>Another one is mentioning the proxy, which I have not yet implemented:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  1) API: resource loading configuration
       With a custom resource loader
         should be able to customize the proxy option:

      AssertionError: expected 1 to equal 3
      + expected - actual

      -1
      +3
</code></pre></div></div>

<p>A timeout:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  2) web-platform-tests
       cors
         credentials-flag.htm:
     Error: Error: test harness should not timeout: cors/credentials-flag.htm
</code></pre></div></div>

<p>And cookies being sent for preflight requests:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  31) web-platform-tests
       xhr
         access-control-preflight-request-must-not-contain-cookie.htm:
     Failed in "Preflight request must not contain any cookie header":
assert_unreached: Unexpected error. Reached unreachable code
</code></pre></div></div>

<p>There might also be some other errors in between, but the logs are full with the setCookie stacktraces, so let’s first fix that one.</p>

<p>It seems that the cookieJar keeps being patched over and over again, which was not my intention. Fixing this should fix the stack-level-too-deep error, and it might also fix the timeout error.</p>

<p>Let’s add a check to ensure the cookieJar is only patched once:</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-13">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// inside `fetch(urlString, options = {})`</span>
<span class="kd">const</span> <span class="nx">cookieJar</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span><span class="p">;</span>
  <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">args</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">{});</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">args</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">ignoreError</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span><span class="p">(...</span><span class="nx">args</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4917 passing (11m)
563 pending
1 failing

1) API: resource loading configuration
      With a custom resource loader
        should be able to customize the proxy option:

    AssertionError: expected 1 to equal 3
    + expected - actual

    -1
    +3

    at /Users/hinloopen/Projects/Github/jsdom/test/api/resources.js:666:16
    at runMicrotasks (&lt;anonymous&gt;)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
</code></pre></div></div>

<p>4917 passing tests, 1 failing. Only the proxy implementation remains.</p>

<h3 id="implementing-proxy">Implementing proxy</h3>

<p>It seems that one can replace the <code class="language-plaintext highlighter-rouge">node-fetch</code> HTTP(s) agents with a proxy agent using <a href="https://www.npmjs.com/package/https-proxy-agent">https-proxy-agent</a> as mentioned by <a href="https://github.com/node-fetch/node-fetch/issues/79#issuecomment-184594701">jimliang</a>.</p>

<p>Looking at the <a href="https://github.com/TooTallNate/node-https-proxy-agent/blob/master/package.json#L33">dependencies of <code class="language-plaintext highlighter-rouge">https-proxy-agent</code></a>, it seems there are two: <a href="http://npmjs.com/package/agent-base">agent-base</a> and <a href="https://www.npmjs.com/package/debug">debug</a>.</p>

<p>I feel like this <code class="language-plaintext highlighter-rouge">debug</code> dependency should have been optional, but who am I to judge. The <code class="language-plaintext highlighter-rouge">agent-base</code> dependency seems sensible.</p>

<p>I also noticed there is a <a href="https://github.com/TooTallNate/node-http-proxy-agent"><code class="language-plaintext highlighter-rouge">http-proxy-agent</code> variant</a>, without the <code class="language-plaintext highlighter-rouge">https</code>. I’m not sure if we need both. I’m hoping the <code class="language-plaintext highlighter-rouge">https</code> one just supports both HTTP and HTTPS so I don’t have to install both.</p>

<p>Let’s try <code class="language-plaintext highlighter-rouge">https-proxy-agent</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add https-proxy-agent
</code></pre></div></div>

<h4 id="libjsdombrowserresourcesresource-loaderjs-14">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">HttpsProxyAgent</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">https-proxy-agent</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// _getFetchOptions({ accept = "*/*" }) {</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">proxyAgent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">HttpsProxyAgent</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">({</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">});</span>
  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">proxyAgent</span> <span class="p">?</span> <span class="nx">proxyAgent</span> <span class="p">:</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s run the tests, see if this works.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># (with .only on "should be able to customize the proxy option")</span>
0 passing <span class="o">(</span>6s<span class="o">)</span>
1 failing

1<span class="o">)</span> API: resource loading configuration
      With a custom resource loader
        should be able to customize the proxy option:
    Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure <span class="s2">"done()"</span> is called<span class="p">;</span> <span class="k">if </span>returning a Promise, ensure it resolves. <span class="o">(</span>/Users/hinloopen/Projects/Github/jsdom/test/index.js<span class="o">)</span>
    at listOnTimeout <span class="o">(</span>internal/timers.js:531:17<span class="o">)</span>
    at processTimers <span class="o">(</span>internal/timers.js:475:7<span class="o">)</span>
</code></pre></div></div>

<p>Timeout? That’s not helpful at all. Since the proxy is HTTP, let’s blindly try <code class="language-plaintext highlighter-rouge">http-proxy-agent</code>. Additionally, the <code class="language-plaintext highlighter-rouge">keepAlive</code> and <code class="language-plaintext highlighter-rouge">rejectUnauthorized</code> options are not passed to the proxy agent. Let’s add them. Both proxy agents accept either a URL or an object <code class="language-plaintext highlighter-rouge">post</code>, <code class="language-plaintext highlighter-rouge">hostname</code>, <code class="language-plaintext highlighter-rouge">protocol</code>: The output of <code class="language-plaintext highlighter-rouge">url.parse</code>. I’m <strong>assuming</strong> the remaining options are passed to <code class="language-plaintext highlighter-rouge">http(s).Agent</code>.</p>

<p>Let’s combine all my assumptions and see if we get anything other than a timeout. Let’s also increase the timeout, just in case something is just being slow.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add http-proxy-agent
</code></pre></div></div>

<h4 id="libjsdombrowserresourcesresource-loaderjs-15">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">url</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">HttpProxyAgent</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">http-proxy-agent</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">HttpsProxyAgent</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">https-proxy-agent</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// _getFetchOptions({ accept = "*/*" }) {</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">agentOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">};</span>
  <span class="kd">const</span> <span class="nx">proxyOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">agentOpts</span><span class="p">,</span> <span class="p">...(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span> <span class="p">?</span> <span class="nx">url</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">)</span> <span class="p">:</span> <span class="p">{})</span> <span class="p">};</span>
  <span class="kd">const</span> <span class="nx">httpAgent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">HttpProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">httpsAgent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">HttpsProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">);</span>
  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">http:</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">httpAgent</span> <span class="p">:</span> <span class="nx">httpsAgent</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># npm t -- --timeout 9999</span>
<span class="c"># (with .only on "should be able to customize the proxy option")</span>
this._proxy http://127.0.0.1:63767
this._proxy http://127.0.0.1:63767
      ✓ should be able to customize the proxy option <span class="o">(</span>80ms<span class="o">)</span>


  1 passing <span class="o">(</span>4s<span class="o">)</span>
</code></pre></div></div>

<p>Success!</p>

<p>Let’s do a minor cleanup to create agents on-demand, and re-run all tests to make sure everything still works.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-16">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 *
 * @param {string} protocol "http:" or "https:"
 */</span>
<span class="nx">_getAgent</span><span class="p">(</span><span class="nx">protocol</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">isHttps</span> <span class="o">=</span> <span class="nx">protocol</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">https:</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">agentOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">};</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">agentOpts</span><span class="p">.</span><span class="nx">rejectUnauthorized</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">proxyOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">url</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">),</span> <span class="p">...</span><span class="nx">agentOpts</span> <span class="p">};</span>
    <span class="k">return</span> <span class="nx">isHttps</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">HttpsProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">HttpProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">isHttps</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// inside _getFetchOptions({ accept = "*/*" }) {</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">agent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getAgent</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">protocol</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All tests are gean. Great. This is the final result. I intent to clean it up after the remaining <code class="language-plaintext highlighter-rouge">request</code> dependencies are removed.</p>

<h4 id="libjsdombrowserresourcesresource-loaderjs-17">lib/jsdom/browser/resources/resource-loader.js</h4>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 *
 * @param {string} protocol "http:" or "https:"
 */</span>
<span class="nx">_getAgent</span><span class="p">(</span><span class="nx">protocol</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">isHttps</span> <span class="o">=</span> <span class="nx">protocol</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">https:</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">agentOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="na">keepAlive</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">rejectUnauthorized</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span> <span class="p">};</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">agentOpts</span><span class="p">.</span><span class="nx">rejectUnauthorized</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_strictSSL</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">proxyOpts</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">url</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">_proxy</span><span class="p">),</span> <span class="p">...</span><span class="nx">agentOpts</span> <span class="p">};</span>
    <span class="k">return</span> <span class="nx">isHttps</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">HttpsProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">HttpProxyAgent</span><span class="p">(</span><span class="nx">proxyOpts</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">isHttps</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">https</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Agent</span><span class="p">(</span><span class="nx">agentOpts</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// inside _getFetchOptions({ accept = "*/*" }) {</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">:</span>
<span class="k">case</span> <span class="dl">"</span><span class="s2">https</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">cookieJar</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">cookieJar</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span> <span class="o">=</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span><span class="p">;</span>
    <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">setCookie</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">args</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">{});</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">args</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">ignoreError</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="nx">cookieJar</span><span class="p">.</span><span class="nx">__setCookie</span><span class="p">(...</span><span class="nx">args</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="kd">let</span> <span class="nx">lastUrl</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">referrer</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">lastOpts</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">myFetch</span> <span class="o">=</span> <span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">opts</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">lastUrl</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">IS_BROWSER</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">opts</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">referer</span> <span class="o">=</span> <span class="nx">lastUrl</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">lastUrl</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
    <span class="nx">lastOpts</span> <span class="o">=</span> <span class="nx">opts</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">opts</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">targetFetch</span> <span class="o">=</span> <span class="nx">fetchCookie</span><span class="p">(</span><span class="nx">myFetch</span><span class="p">,</span> <span class="nx">cookieJar</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">fetchOptions</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_getFetchOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">abortController</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
  <span class="nx">fetchOptions</span><span class="p">.</span><span class="nx">signal</span> <span class="o">=</span> <span class="nx">abortController</span><span class="p">.</span><span class="nx">signal</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">fetchPromise</span> <span class="o">=</span> <span class="nx">targetFetch</span><span class="p">(</span><span class="nx">urlString</span><span class="p">,</span> <span class="nx">fetchOptions</span><span class="p">);</span>

  <span class="kd">let</span> <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">result</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">then</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onfulfilled</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">fetchPromise</span><span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">status</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">response</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Unexpected status=</span><span class="p">${</span><span class="nx">status</span><span class="p">}</span><span class="s2"> for </span><span class="p">${</span><span class="nx">urlString</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">{};</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="p">[</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="p">]</span> <span class="k">of</span> <span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">headers</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">value</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="nx">result</span><span class="p">.</span><span class="nx">response</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">status</span><span class="p">,</span>
        <span class="nx">headers</span><span class="p">,</span>
      <span class="p">};</span>
      <span class="nx">result</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">lastUrl</span><span class="p">;</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">arrayBuffer</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">typeof</span> <span class="nx">Buffer</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">undefined</span><span class="dl">"</span> <span class="p">?</span> <span class="nx">arrayBuffer</span> <span class="p">:</span> <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">arrayBuffer</span><span class="p">))</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">return</span> <span class="nx">onfulfilled</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span> <span class="p">})</span>
    <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">aborted</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="nx">onrejected</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">};</span>

  <span class="nx">result</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">response</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
    <span class="na">abort</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
      <span class="nx">aborted</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
      <span class="nx">abortController</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
    <span class="p">},</span>
    <span class="na">href</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
    <span class="nx">then</span><span class="p">,</span>
    <span class="na">catch</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">onrejected</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">then</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span> <span class="nx">onrejected</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="nx">getHeader</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">lastOpts</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span>
    <span class="p">}</span>
  <span class="p">};</span>

  <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Because this post has become fairly large, I’ll continue this post in a part 2. To be continued…</p>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[We use JSDOM for testing clientside applications in NodeJS. JSDOM lowers the complexity of writing tests for clientside code by omitting the browser and replacing it with a fake one: JSDOM. However, there's one JSDOM dependency that concerned me a bit: [request](https://www.npmjs.com/package/request), with [request-promise-native](https://www.npmjs.com/package/request-promise-native) `request` has already been a discussed to be replaced with something else in an issue [#2792: Replace request with something better](https://github.com/jsdom/jsdom/issues/2792). Since there were no pull requests for the issue, I decided to see if I can help out and fix it myself. In this blog post, I'll describe my process.]]></summary></entry><entry><title type="html">Hello, Developer;</title><link href="https://bonaroo.nl/2020/05/05/hello-developer.html" rel="alternate" type="text/html" title="Hello, Developer;" /><published>2020-05-05T00:00:00+00:00</published><updated>2020-05-05T00:00:00+00:00</updated><id>https://bonaroo.nl/2020/05/05/hello-developer</id><content type="html" xml:base="https://bonaroo.nl/2020/05/05/hello-developer.html"><![CDATA[<h1 id="hello-developer">Hello, Developer;</h1>

<p><strong>Welcome to Bonaroo. You’re here to help us help our clients build great software. This guide covers everything you need to get going, from communication to code.</strong></p>

<h2 id="before-you-start">Before you start</h2>

<h3 id="communication">Communication</h3>

<p>We use Slack for everything. We don’t have a fixed office. Some of us work from home, some from the office, some from wherever. Slack keeps us connected regardless.</p>

<ul>
  <li>Confirm you have access to our Slack</li>
  <li>Confirm you’re happy with the terms and agreements</li>
</ul>

<h3 id="work-hours--schedule">Work hours &amp; schedule</h3>

<p>We don’t do fixed schedules. Most of us work weekdays roughly between 9:00 and 18:00 Amsterdam time (CET/CEST). But we care about output, not hours. If you prefer working nights or weekends, that’s fine.</p>

<p>Just let us know your usual availability so we can plan when to assign work and when to expect questions.</p>

<h2 id="getting-started">Getting started</h2>

<p>You’re here to work on a specific project. Your first goal: get it running locally.</p>

<p>The project README should have everything you need. If it doesn’t, that’s a bug in the README. Let us know and we’ll fix it.</p>

<ul>
  <li>Get access to the project repository</li>
  <li>Read the README</li>
  <li>Get the project running locally</li>
  <li>Verify all tests pass</li>
</ul>

<p>If you get stuck, ask. No question is too small.</p>

<h3 id="understanding-the-project">Understanding the project</h3>

<p>Our documentation tends to be minimal. We try to make the code and context speak for themselves. That said, we usually bring in extra hands when we’re short on time, so gaps exist.</p>

<p>Take some time to explore the codebase, the UI, and the README. Ask questions whenever something isn’t clear. We’d rather answer questions up front than debug misunderstandings later.</p>

<p>It also helps to understand the bigger picture:</p>

<ul>
  <li>Who are the users of this project?</li>
  <li>Who is the client, and what problem are we solving for them?</li>
</ul>

<p>The client isn’t always the end user, but their goals shape the project.</p>

<h2 id="working-on-issues">Working on issues</h2>

<h3 id="no-issues-assigned-yet">No issues assigned yet?</h3>

<p>Let us know. We’ll get you set up.</p>

<h3 id="youve-been-assigned-an-issue">You’ve been assigned an issue</h3>

<p>Start by making sure you understand it. Issues vary wildly: some are detailed specs, some are a one-liner copied from a client email. They might describe a problem without a solution, or propose a solution without explaining the problem. Ideally they do both.</p>

<ul>
  <li>Make sure you understand the problem</li>
  <li>Make sure you see a path to a solution</li>
</ul>

<p>If anything is unclear, ask the issue author. If the issue doesn’t suggest a solution, comment your proposed approach before you start coding.</p>

<h3 id="document-as-you-go">Document as you go</h3>

<p>The issue is the single source of truth for that problem. If you discover relevant information elsewhere (a Slack thread, a conversation, a piece of documentation), add it to the issue. Future you (and future us) will be grateful.</p>

<p>For small, straightforward fixes, just solve it and move on.</p>

<h2 id="tracking-time">Tracking time</h2>

<p>Track your time using the method we discussed with you. For each block of time, include:</p>

<ul>
  <li>The project</li>
  <li>The issue (e.g. <code class="language-plaintext highlighter-rouge">#55</code>)</li>
  <li>A brief summary of the work (e.g. <code class="language-plaintext highlighter-rouge">creating a users index</code>)</li>
</ul>

<p>If you weren’t working on a specific issue, it’s especially important to note what you did.</p>

<p>We use time tracking for billing, but also to understand where time goes and how work progresses.</p>

<h2 id="repository-guidelines">Repository guidelines</h2>

<p>We follow standard commit conventions. Unless told otherwise, <a href="https://www.gnu.org/software/gnuastro/manual/html_node/Commit-guidelines.html">these guidelines</a> apply.</p>

<h3 id="the-workflow">The workflow</h3>

<ol>
  <li><strong>Start from an issue.</strong> Every piece of code ties back to an issue.</li>
  <li><strong>Branch from the latest main branch.</strong> Name your branch with the issue ID and a short summary: <code class="language-plaintext highlighter-rouge">55-create-user-index</code>.</li>
  <li><strong>Commit early, open a PR.</strong> After your first commit, open a Pull Request. Commit messages should finish the sentence “This commit will…”, e.g. <code class="language-plaintext highlighter-rouge">add user index</code>.</li>
  <li><strong>Mark it as work in progress</strong> until your solution is complete.</li>
  <li><strong>Request review.</strong> Once you’re done, remove the WIP status and we’ll review it.</li>
  <li><strong>Address feedback.</strong> If changes are requested, update the PR based on the discussion.</li>
</ol>

<p>While waiting for review, feel free to pick up another issue.</p>

<h3 id="github">GitHub</h3>

<p>Open a PR after your first commit. Add “WIP” to the title while it’s in progress. Remove “WIP” when it’s ready for review. Some repos use a WIP label or bot instead, we’ll let you know.</p>

<h3 id="gitlab">GitLab</h3>

<p>Use “Create Merge Request” to create both a branch and MR. Prefix the title with “WIP: “ while in progress. Remove the prefix when you’re ready for review.</p>

<div class="services-cta" style="margin-top: 3rem;">
  <p class="services-cta__text">Questions or want to know more? We'd love to hear from you.</p>
  <a href="/contact.html" class="btn btn-action btn-green">Get in touch</a>
</div>]]></content><author><name>Toby</name></author><summary type="html"><![CDATA[Welcome aboard. Here's everything you need to get started.]]></summary></entry></feed>