<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Paul Armstrong</title>
        <link>https://paularmstrong.dev</link>
        <description>Paul Armstrong is a senior web application developer, specializing in JavaScript, performance, and developer experience</description>
        <lastBuildDate>Tue, 24 Dec 2024 22:51:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Paul Armstrong</title>
            <url>https://paularmstrong.dev/img/favicon-32x32.png</url>
            <link>https://paularmstrong.dev</link>
        </image>
        <copyright>&amp;copy;2024 Paul Armstrong. All rights reserved.</copyright>
        <atom:link href="https://paularmstrong.dev/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Code editors are very personal]]></title>
            <link>https://paularmstrong.dev/blog/2024/03/02/2024-03-02-code-editors-are-personal/</link>
            <guid>https://paularmstrong.dev/blog/2024/03/02/2024-03-02-code-editors-are-personal/</guid>
            <pubDate>Sat, 02 Mar 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I just spent far too long trying to bend <a href="https://zed.dev">Zed</a> to my will. It reminded me of how I needed multiple weeks on and off configuring and reconfiguring VSCode to work for me. It is rare that the defaults provided in a code editor work for me. So I tried, and I’ve given up for a few very important (to me) reasons…</p>
<p><a href="https://paularmstrong.dev/blog/2024/03/02/2024-03-02-code-editors-are-personal/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import VSCode from ’../../images/blog/2024-03/vscode.png’;</p>
<p>I just spent far too long trying to bend <a href="https://zed.dev">Zed</a> to my will. It reminded me of how I needed multiple weeks on and off configuring and reconfiguring VSCode to work for me. It is rare that the defaults provided in a code editor work for me. So I tried, and I’ve given up for a few very important (to me) reasons.</p>
<p>But before that, all of this takes me back to my history with code editors. The main theme that’s stuck with me across each one is that I want it to be a <em>code editor</em>. Not an IDE. I don’t want a terminal, I don’t want to run tests, I don’t want a million and one features that are better handled by other tools and apps.</p>
<h2 id="code-editor-timeline">Code editor timeline</h2>
<p>An approximate timeline, as near as I can remember would look something like this:</p>
<div class="bustout-sm">
```mermaid
timeline
  1990s : Windows Notepad
        : Geocities
        : Adobe GoLive
        : ResEdit
  2002 : BBEdit
  2005 : TextMate
  2011 : Sublime Text
       : Xcode<br>(for a short stint)
  2021 : VSCode
```
</div>
<h3 id="1990s---script-kiddie">1990s - Script kiddie</h3>
<p>I started nerding out with various things when I was too young to even know what I was doing, but I had fun doing it. I worked on all kinds of various things, but nothing noteworthy. I do remember having a Windows computer at home as our main machine for a stint on which I used Windows Notepad.</p>
<h3 id="2002---bbedit">2002 - BBEdit</h3>
<p>About this time I started ditching Adobe GoLive and going all in with BBEdit. I landed a job on my college campus building websites for our student union, student groups, and more. I got really good at writing HTML by hand. CSS was really just starting to proliferate and I was hooked.</p>
<h3 id="2005---textmate">2005 - TextMate</h3>
<p>TextMate was already available, but we didn’t make the switch until it was really getting popular. TextMate did everything I needed and nothing more.</p>
<h3 id="2011---sublime-text">2011 - Sublime Text</h3>
<p>I’ve spent more time using Sublime Text than any other editor. I would be really interested in knowing the full stats of hours spent in the application over the course of ~10 years.</p>
<p>I loved Sublime Text. It was fast, boosted my productivity, and simple – nothing about Sublime Text ever got in my way.</p>
<h3 id="2021---vscode">2021 - VSCode</h3>
<p>I <em>begrudgingly</em> switched to VSCode. I never wanted to. In fact, over the course of at least three years, I tried to sit down and force myself to use it and configure it to my liking for a week at a time on at least four different occassions.</p>
<p>VSCode was slow. It does far too much for a “code editor” and is nearly a full-blown IDE… and at this point, with the right extensions, it <em>is</em> a full IDE.</p>
<p>I really only switched to VSCode for a few reasons:</p>
<ol>
<li>The Sublime Text LSP for TypeScript sucked (and it still sucks). “Go to definition” rarely, if ever works.</li>
<li>SublimeLinter is very difficult to configure correctly.</li>
<li>As a principal-level leader who needed to spend a lot of time pair-programming and teaching others, it was too difficult to not work in the same editor everyone else was.</li>
</ol>
<p>I finally got VSCode to a point where it typically is not noticeably slow and 90% of its features are tucked away, hidden, or completely disabled. These are the things that are either unnecessary or poorly implemented for me:</p>
<ul>
<li>Terminal: tends to be problematic or have integration issues</li>
<li>Debugger: takes too long to set up and configure for every project. I just use Node’s <code>--inspect --inspect-brk</code> flags and pop open an inspector in a chromium-based browser</li>
<li><em>All the tooltips</em>: VSCode is way too aggressive about showing overlay hints and tooltips while I’m typing. I’m still typing. I actually type pretty darn fast, but these tooltips appear even faster and they’re always wrong.</li>
<li>Disabled/hidden: Most of the UI panels other than the explorer view</li>
<li>The search UI is horrible. I’ve moved it over to a panel, but it’s missing the ability to do regex replacements. You <em>have to</em> use the side bar’s find tab for this.</li>
</ul>
<div class="bustout-sm">
  <img src="{VSCode}" alt="A minimal approach of using VSCode with most panes and features disabled.">
</div>
<h2 id="trying-something-new">Trying something new</h2>
<p>I recently tried to go back to Sublime Text, but I just couldn’t figure out how to get by the same first two issues that led me to switching to VSCode in the first place. So I threw that out the window.</p>
<p>I have the following hard requirements for an editor:</p>
<ol>
<li>
<p>Ability for custom keyboard shortcut snippets.</p>
<p>I’ve used the same custom snippet since my days with BBEdit: <kbd>CMD+CTRL+,</kbd> will add <code>&#x3C;div>&#x3C;/div></code> and highlight <code>div</code> to change to any other HTML tag. I must use this snippet hundreds of times per day and I cannot live without it.</p>
</li>
<li>
<p>Two &#x26; three pane views.</p>
<p>The panes need to stay open even when there is no file open in them. As visible in my VSCode screenshot above, keeping the panes open prevents any sort of unwrap/reflow of text in the open pane.</p>
</li>
<li>
<p>Stop shoving AI at me.</p>
<p>I prefer to write things myself and know what I’m actually doing instead of hoping that an LLM connected the dots between keywords and syntax correctly.</p>
</li>
<li>
<p>“Go to definition” must actually work.</p>
<p>Including going to definitions from <code>node_modules</code> (which Sublime Text doesn’t support).</p>
</li>
</ol>
<h3 id="zed">Zed</h3>
<p>Non-starters:</p>
<ul>
<li>Missing custom snippets. The inability to add my snippets in Zed made it a non-starter (and I spent a good hour scouring through discussions and issues trying to find a solution).</li>
<li>Extra panes disappear when empty and there’s no way to prevent that.</li>
</ul>
<p>Annoying, but I can live with:</p>
<ul>
<li>
<p>I absolutely hate that the settings are only in JSON. This is actually the same as Sublime Text, but annoyingly, you have to manually open both the defaults and your custom settings in order to know what is actually possible.</p>
</li>
<li>
<p>Minimal/lack of documentation on how to configure key bindings.</p>
<p>I can eventually get used to different key bindings for things, but the syntax currently requires I know the internals of Zed and its code to perform actions.</p>
</li>
<li>
<p>AI - at least it can be disabled, but how do I <em>know</em> that what I’m writing isn’t being fed to some machine to learn from?</p>
</li>
</ul>
<h3 id="lapce">Lapce</h3>
<p>Honestly, I didn’t spend enough time here and I probably should, but…</p>
<p>Non-starters:</p>
<ul>
<li>Like Zed, no snippet support.</li>
<li>Extra panes disappear when empty and there’s no way to prevent that.</li>
</ul>
<p>Annoying and I don’t want to deal with:</p>
<ul>
<li>It feels like it’s trying to be VSCode with all the features, bells &#x26; whistles, but I can’t make them go away.</li>
<li>No native OS X menubar</li>
</ul>
<p>Super annoying, but I can live with:</p>
<ul>
<li>I closed a couple panels upon first open and I can’t figure out how to get them back.</li>
<li>What the heck goes in the right panel? It’s just blank and click/right-click does nothing in it.</li>
<li>It took me a good 5 minutes to figure out how to open a folder/project in the editor.</li>
</ul>
<hr>
<h2 id="sitting-for-now">Sitting for now</h2>
<p>I guess I’ll be sitting with VSCode for now. Despite my annoyances – at least most of them can be hidden away or disabled.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Introducing oneRepo: the JavaScript & TypeScript monorepo toolchain for safe, strict, & fast development]]></title>
            <link>https://paularmstrong.dev/blog/2024/02/27/2024-02-27-introducing-onerepo-the-javascript-typescript-monorepo-toolchain-for-safe-strict-fast-development/</link>
            <guid>https://paularmstrong.dev/blog/2024/02/27/2024-02-27-introducing-onerepo-the-javascript-typescript-monorepo-toolchain-for-safe-strict-fast-development/</guid>
            <pubDate>Tue, 27 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I am thrilled to announce the first major version of oneRepo. Many years of experience creating monorepo tooling for high performance frontend, JavaScript, and TypeScript teams at major organizations has led to the creation of this command-line interface, an API, and a toolchain for better, safer, and faster monorepos.</p>
<p><a href="https://paularmstrong.dev/blog/2024/02/27/2024-02-27-introducing-onerepo-the-javascript-typescript-monorepo-toolchain-for-safe-strict-fast-development/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import Aurora from ’../../components/Aurora.astro’;
import { Image } from ‘astro:assets’;
import oneRepoLogo from ’../../images/blog/2024-02/onerepo.png’;
import oneRepoOg from ’../../images/blog/2024-02/onerepo-og.png’;
import Button from ’../../components/Button.astro’;
import { Icon } from ‘astro-icon/components’;</p>
<p>About a year ago, I asked a friend what their thoughts were about me writing, for the fourth time, a monorepo toolchain – but this time, open sourcing it. They told me I’m doing the same thing that every JavaScript developer does – creates <em>yet another</em> way to do the same thing.</p>
<p>That hit pretty hard, even though I had thought about it a bit already, it was the first an external voice was reinforcing my fears. I was talking about doing the exact thing that I get annoyed about with the ecosystem. Why are there no less than 6 published modules to accomplish <em>the same thing</em>? Why do people keep creating their own instead of contributing back to the already established modules?</p>
<p>I sat with these thoughts for a good while. I hated what I wanted to do… but one thing kept bringing me back – none of the tools available were designed for the same developer experience, usability, extendability, and strict safety that I had become accustomed to.</p>
<hr>
<aurora bands="{30}">
<img src="{oneRepoLogo}" alt="oneRepo logo" width="{1002}" loading="eager">
<h2 id="introducing-onerepo">Introducing oneRepo</h2>
<p>oneRepo is a command-line interface, an API, and a toolchain for streamlining development with JavaScript and TypeScript monorepos. It’s more than a build system, more than (and not) a build enhancer using cache; it’s a full suite of tools to help teams work faster, smarter, and safer with apps and their source dependencies within monorepos of all sizes.</p>
<p>I’m thrilled to announce that <a href="https://onerepo.tools">oneRepo</a> has officially reached version 1.0.0!</p>
<h2 id="features">Features</h2>
<p>Other monorepo tooling ends up being overly complicated or lacking in functionality, making it challenging for distributed organizations to maintain healthy interdependent code within a monorepo.</p>
<p>oneRepo is a full suite of tools for managing JavaScript and TypeScript monorepos, with the goal of enabling speed and confidence for all changes:</p>
<h3 id="-automating-tasks"><icon name="heroicons:rocket-launch-solid"> Automating tasks</icon></h3>
<p>oneRepo simplifies automation by handling common tasks for you and your team. Say goodbye to spending excessive time tinkering with tooling and hello to focusing on your applications.</p>
<p>Check out the oneRepo <a href="https://onerepo.tools/core/tasks/">task system</a>.</p>
<h3 id="strict-safety--checks"><icon name="heroicons:check-circle-solid">Strict safety &#x26; checks</icon></h3>
<p>Gone are the days of manually configuring file glob patterns for Workspace integrity and decision making for whether or not checks and tasks must run. oneRepo automatically and accurately determines which Workspaces, tasks, and checks are necessary for any given change.</p>
<p>Learn more about oneRepo’s built-in <a href="https://onerepo.tools/core/graph/#validating-configurations">validation checks</a>.</p>
<h3 id="-stop-recompiling-dependencies"><icon name="heroicons:puzzle-piece-solid"> Stop recompiling dependencies</icon></h3>
<p>With oneRepo, you can utilize shared Workspaces as source-level dependencies. This means that all import/require chains within the monorepo originate from the source files, eliminating the need to rebuild shared packages to capture changes.</p>
<p>See how oneRepo prevents recompiling for every change using <a href="https://onerepo.tools/docs/source-dependencies/">source dependencies</a>.</p>
<h3 id="-not-another-language"><icon name="heroicons:code-square-solid"> Not <em>another</em> language</icon></h3>
<p>oneRepo and its APIs, plugins, and configurations are all written in JavaScript (and TypeScript). There’s no need to learn a new language or decipher YAML/JSON schema DSLs to configure your tooling.</p>
<p>Use oneRepo’s full featured API to <a href="https://onerepo.tools/docs/commands/">write your own commands</a>.</p>
<h3 id="-human-readable-output"><icon name="heroicons:book-open-solid"> Human-readable output</icon></h3>
<p>Logging output from every command and tool in oneRepo is carefully grouped, documented, and prevented from overlapping parallel executions. Every line includes context, timing, and log-type information.</p>
<p>Clear, concise, and obvious <a href="https://onerepo.tools/docs/log-output/">log output</a> for <i>humans</i>.</p>
<h3 id="no-upsells">No upsells</h3>
<p>Once you’ve installed oneRepo, you’re all set. There’s no need to pay extra for additional features; simply bring your own infrastructure and enjoy the full feature set.</p>
<hr>
<h2 id="a-personal-history">A personal history</h2>
<p>I have a pretty deep history with monorepos at this point. My first exposure to them was at Twitter, starting in 2015. I was hired on to the small team that was re-writing the entire web stack from Scala into React. Using Node.js and purely JavaScript was very new to Twitter. There were no user-facing services written fully in JavaScript at the time. We were pioneering something new. However, Twitter’s <em>source</em> monorepo was primarily focused on serving the dominant language, Scala.</p>
<p>All of Twitter’s tooling, including an entire engineering team (larger than the entire web team), was focused on ensuring micro-service interopability, scalability, and speed of development – for Scala. Sure, there were some other languages thrown in there that had some optimizations as well, but none of them worked anything like JavaScript.</p>
<p>Problems arose quickly as we started to scale our application’s codebase, shared (internal) dependencies, and team. One of the worst issues we had, that was unique to working with JavaScript within <em>source</em>, was that it would take 30-60 seconds to create a new git branch.</p>
<p>Yes. One entire minute wait just to <code>git checkout -b …</code>.</p>
<p>After what seemed like months of back and forth with the team responsible for our monorepo tooling: the issue was having a large number of ignored files within the repository. Because JavaScript projects typically include one or more <code>node_modules</code> folders that are ignored – with <em>thousands</em> of files in them.</p>
<h3 id="problems">Problems</h3>
<p>The branching issue was just the tip of the iceberg of the issues the web team was facing. Some of the other major issues we were facing:</p>
<ol>
<li>We weren’t sharing code with any other services outside of our own application and sub-packages. Similarly, no other team depended on any of our code. We were just a small fish in a huge ocean.</li>
<li>The tooling couldn’t determine the dependency graph of our application and sub-packages – we could not do any deterministic test or CI tasks based on what changed across Workspaces.</li>
<li>Poor discoverability of commands, shared modules, patterns, and documentation.</li>
<li>Knowing when to run <code>npm install</code> after pulling the main branch.</li>
<li>Speed of all aspects of the development cycle (see previously mentioned issue with switching brances).</li>
<li>And many more lost to my memory at this point…</li>
</ol>
<p>We were essentially a <em>client application</em>. The iOS and Android <em>client applications</em> had their own repositories to avoid the exact same problems we were experiencing, being a unique language with non-shared code that didn’t fit into the rest of the stack.</p>
<h3 id="solutions">Solutions</h3>
<p>So what’d we do about it? In 2017 I started pushing hard to allow the web team to move out of the <em>source</em> repo. It was a very uphill battle for many months. At most, I counted one VP, four Directors, at least five Senior Managers, and at least a dozen engineers in a meeting while trying to get the <em>source</em> monorepo team to allow us to move out. Why we needed their permission is still over my head, even though this was my project from the start.</p>
<p>Eventually, we hired a new Director for that team and I had a one-on-one meeting with her. I was prepared with the same engineer productivity stats that I had presented time and time again.</p>
<blockquote>
<p>We could save nearly two hours of wait time <em>per developer</em>, <em>per week</em>.</p>
</blockquote>
<p>We had nearly 30 developers working on the new web stack at the time. I’ll never forget her response to me:</p>
<blockquote>
<p>What? That’s <em>material</em> time. Why are we wasting our time now talking about this? Just go and do it.</p>
</blockquote>
<p>She told me she’d deal with her team and shield us from the fallout. I <em>finally</em> was able to get our team in motion in 2018 to build what we needed.</p>
<p><em>My experience building the <code>web</code> monorepo tooling became the foundation for what is now available as <a href="https://onerepo.tools">oneRepo</a>.</em></p>
<aside type="info" title="Spoiler">
  We actually recorded timing metrics across many aspects of what was happening throughout the repo both on developer
  machines and in our automated CI runs. By the time I left Twitter in 201, we had hit or broke those time savings _and_
  had upwards of 150 individual contributors to the repository each week.
</aside>
<h3 id="rewriting">Rewriting</h3>
<p>After Twitter, I ended up writing nearly the same monorepo tooling internally two more times. Once at Zillow Rentals and once at Microsoft for Startups. In between those two times, I also started writing it with the intention of being able to open source it. I didn’t get very far, as work and life took much more out of me and I wasn’t able to devote enough time to making it be something usable.</p>
<hr>
<h2 id="another-monorepo-tool">Another monorepo tool</h2>
<p>Back to the beginning of 2023. I had started a new position with the express directive to pull a disorganized frontend organization together, help them grow, and figure out how we can work faster.</p>
<p>Our CTO came at me with this simplified question that he wanted answered:</p>
<blockquote>
<p>Why does it take so dang long to put a button on a page?</p>
</blockquote>
<p>The answer was not one of skill, laziness, or anything that could be related to one or many persons. Instead, the answer was that we had over 126 individual frontend repositories. Of those, there was a varying amount of code sharing through a private npm registry, often with varying versions and duplications of the same packages.</p>
<p>It was a completely different root problem than we had at Twitter, but with a similar result – it took forever to do anything and teams were all working in silos, rarely sharing code and knowledge.</p>
<p>I set out to solve this problem first. Yes, there were many problems – 126 frontends is also a major issue, but that’s not our focus here…, but the first step was to figure out what we have, what we need or don’t, and how we can share and merge.</p>
<p>By now you’ve probably guessed what the solution would be: merge our frontend repositories down to a single <em>monorepo</em>.</p>
<h2 id="choosing-the-right-tools">Choosing the right tools</h2>
<p>But what monorepo tooling would we choose? I did my due diligence <em>again</em> to look at Nx, Turbo, and Bazel. With each tool I had the same issues:</p>
<ul>
<li>❌ Reading and following log output was very difficult for the varying debugging skill levels we had.</li>
<li>❌ They piggy-backed on <em>npm scripts</em>, which meant:
<ul>
<li>We would still have to write custom tooling.</li>
<li>It would be too easy to reject standards and allow Workspaces to create one-off different ways of doing the same things.</li>
<li>It is difficult to determine what script does what – there’s little to no <code>--help</code> documentation</li>
<li>Finding the source of the scripts requires deep knowledge.</li>
</ul>
</li>
<li>❌ They rely heavily on caching input &#x26; output to give a perception of speed. Setting the cache determinism is a manual process that is really easy to mis-configure, resulting in false results during all lifecycles of code.</li>
<li>❌ They use custom DSLs, often YAML to configure.</li>
<li>❌ They upsell cost-prohibitive paid services.</li>
<li>❌ They do not enforce strict correctness and standards across Workspace boundaries.</li>
</ul>
<p>Really, my list goes on. Maybe you can make the tools work differently and satisfy some of my issues, but not without a lot of extra work and requiring deeper knowledge of the tools – or building entire extra systems to work in tandem.</p>
<h2 id="open-sourcing">Open sourcing</h2>
<p>Instead of any of the available tools, I got the go ahead to finish working on oneRepo as open source code, still under my ownership, but with the primary intention of enabling it internally to serivce our teams.</p>
<p>While this was very exciting to me, knowing I already had a good start and the experience to back up making it robust enough for many different teams, it was still nagging me that I was writing <em>yet another</em> tool to throw out in the public to choose from.</p>
<p>Eventually, my desire to have tooling that’s easy to use, easy to read, performant, <em>and</em> strict overcame my hesitations. And anyway, the worst case scenario is that no one likes what I’ve made and no one uses it. Well that’s actually easier for me, because it means less to support.</p>
<hr>
<h2 id="onerepo">oneRepo</h2>
<a href="https://onerepo.tools" target="_blank">
  <img src="{oneRepoOg}" width="{1200}" alt="oneRepo - Easy, strict, safe, and fast JavaScript &#x26; TypeScript monorepo toolchain for high performance teams.">
</a>
<p>First and foremost, oneRepo was created with the understanding that everyone cannot be expected to know everything. Every aspect of oneRepo should feel familiar, or at the very least, obvious how to learn and debug.</p>
<ul>
<li>
<p><strong><em>One</em> entry point</strong></p>
<p>oneRepo is a command-line interface: <code>one</code>. It is the <em>one</em> entrypoint for all of your <em>one</em> repository’s management.</p>
</li>
<li>
<p><strong>Help output</strong></p>
<p>Any time you’re not sure what to do with the CLI or what something does, help is just a couple keystrokes away with <code>--help</code> (or <code>-h</code>)</p>
</li>
<li>
<p><strong>Tab completion</strong></p>
<p>Yargs provides us with out of the box tab-completion. Just run <code>one install</code> and you’ll be good to <kbd>TAB</kbd> to completion any time.</p>
</li>
<li>
<p><strong>Generated docs</strong></p>
<p>Use the first-party plugin <a href="https://onerepo.tools/plugins/docgen/">@onerepo/plugin-docgen</a> to generate and share documentation as Markdown or other formats. It’s super easy to use and makes <a href="https://onerepo.tools/plugins/docgen/example/">clear and readable documentation</a>.</p>
</li>
</ul>
<h3 id="empowering-developers">Empowering developers</h3>
<p>The source of all commands, custom or built-in, are written in the same language as the rest of your repository. Each command is represented by a distinct file, easy to find within the <a href="https://onerepo.tools/docs/config/#commandsdirectory"><code>commands</code> directory</a>. This enables everyone on your team that works with JavaScript to be able to learn and contribute to the repo’s tooling without always needing to learn a new language or interrupt a specialist.</p>
<h3 id="clear-output">Clear output</h3>
<p>One big frustration that I always have with other tooling is the log output is difficult for humans to read. My experience debugging with others, particularly more junior developers, has been that they expect errors to be obvious, front and center, without all of the other context.</p>
<p>Particularly when running tasks in parallel, most monorepo tooling will buffer everything immediately to the output, interleaving tasks together, making it difficult to follow what is happening and where there may be issues or all is fine.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ansi"><code><span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> +++</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> dependencies:</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> + @internal-tests/todo-list 0.0.0-development</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> + @types/node 16.11.41</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> + typescript 4.7.3</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span></span>
<span class="line"><span style="color:#34d058">@internal-kit/ts:setup:test:</span><span style="color:#e1e4e8"> Progress: resolved 117, reused 110, downloaded 1, added 0</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Progress: resolved 3, reused 2, downloaded 1, added 3, done</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span></span>
<span class="line"><span style="color:#34d058">@internal-kit/ts:setup:test:</span><span style="color:#e1e4e8"> Progress: resolved 219, reused 208, downloaded 1, added 0</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#34d058;font-weight:bold">  PASS </span><span style="color:#e1e4e8"> src/__test__/usingCli.test.ts</span></span>
<span class="line"><span style="color:#34d058">@internal-kit/ts:setup:test:</span><span style="color:#e1e4e8"> Progress: resolved 310, reused 292, downloaded 1, added 0</span></span>
<span class="line"><span style="color:#34d058">@internal-kit/ts:setup:test:</span><span style="color:#e1e4e8"> Progress: 421, reused 402, downloaded 1, added 0</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#34d058;font-weight:bold">  PASS </span><span style="color:#e1e4e8"> src/__test__/usingAsLibrary.test.ts</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Test Suites:  </span><span style="color:#34d058;font-weight:bold">2 passed</span><span style="color:#e1e4e8">, 2 total</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Tests:        </span><span style="color:#34d058;font-weight:bold">2 passed</span><span style="color:#e1e4e8">, 2 total</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Snapshots:    </span><span style="color:#34d058;font-weight:bold">8 passed</span><span style="color:#e1e4e8">, 8 total</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Time:         2.9122s, estimated 3 s</span></span>
<span class="line"><span style="color:#34d058">@internal-tests/todo-list:test:</span><span style="color:#e1e4e8"> Ran all test suites.</span></span>
<span class="line"></span></code></pre>
<p>oneRepo solves for this by waiting grouping output by individual task. While running, by default, only the most recent or most important information will be shown. Output will <em>never</em> be interwoven between individual tasks. And without manually requesting more verbose output, superfluous information will be hidden:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ansi"><code><span class="line"><span style="color:#e1e4e8"> ┌ Run tests for @internal-tests/todo-list-cli</span></span>
<span class="line"><span style="color:#e1e4e8"> └ </span><span style="color:#34d058">✔</span><span style="color:#e1e4e880"> 3s</span></span>
<span class="line"><span style="color:#e1e4e8"> ┌ Run tests for @internal-kit/ts</span></span>
<span class="line"><span style="color:#e1e4e8"> │ </span><span style="color:#e1e4e880">Progress: 421, reused 402, downloaded 1, added 0</span></span>
<span class="line"><span style="color:#e1e4e8"> └ ⠙</span></span>
<span class="line"></span></code></pre>
<aside type="info" title="More on logging">
  Not only does oneRepo make it easy to control
  [verbosity](https://onerepo.tools/docs/log-output/#controlling-verbosity) to get more or less
  [output](https://onerepo.tools/docs/log-output/#controlling-verbosity) but it also standardizes [where logs are
  sent](https://onerepo.tools/docs/log-output/#key-details) for when you need to control the data flow.
</aside>
<h3 id="integrated-cli">Integrated CLI</h3>
<p>Other monorepo tooling requires you to decide how to write commands and tasks yourself. Either you’re delegating directly to third-parties through <a href="https://nx.dev/reference/nx-json">large DSL configurations</a> or you’re writing custom scripts as one-off solutions with mashed-together setups for logging, type safety, error handling – or none of the above. There are too many choices to make and that ends up with repos having scripts that are difficult to find and trace in the event of an issue.</p>
<p>Furthermore, I’ve been <a href="/blog/2024/02/23/2024-02-23-rust-based-javascript-linters-have-a-major-issue/">frustrated recently</a> by all of the new fancy tools popping up that are written in Rust/Zig/other-fancy-new-C-compiled-languages. They are written for <em>JavaScript</em>, but fall short of offering a way for <em>JavaScript</em> interfaces and enhancements.</p>
<p>oneRepo on the other hand is a fully JavaScript and TypeScript-compatible command-line interface and PI that’s made for adding any extra scripting or commands you can think of. But no need to worry about adding Rust and learning it to your JS monorepo – plugins and CLI commands are written in JS/TS. This makes it easy for you and your team to write <a href="https://onerepo.tools/docs/commands/">custom commands</a> directly for your Repository and Workspace’s needs.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { Builder, Handler } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'onerepo'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#79B8FF"> command</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'new-branch'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#79B8FF"> description</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Create a new branch using our team’s standard naming format.'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> builder</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Builder</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">yargs</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> yargs.</span><span style="color:#B392F0">usage</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`$0 ${</span><span style="color:#E1E4E8">command</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> handler</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Handler</span><span style="color:#F97583"> =</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">argv</span><span style="color:#E1E4E8">, { </span><span style="color:#FFAB70">graph</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">logger</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D">  // Everything you need is easily handled here</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<h3 id="no-cache">No cache</h3>
<p>Avoiding extra work by utilizing cached responses based on a set of input files is a great time saver in theory. The problem is that the responsibility of knowing exactly what files and Workspaces affect the cache for each individual task lies on you and your team.</p>
<p>oneRepo purposefully <em>does not</em> use a cache to avoid false positives and promote strictness over minor speed improvements. Checkl out some extra details on common pitfalls and <a href="https://onerepo.tools/concepts/why-onerepo/#cache-inconsistency">cache inconsistency</a> on the oneRepo docs.</p>
<h2 id="more-to-come">More to come</h2>
<p>I tend to freeze up trying to promote my own open source projects. Writing the code is so much easier than selling people on the idea of using something new and different. And as usual, I’ve written an overly verbose blog post about something I’m passionate about.</p>
<p>But that’s just it. I haven’t been able to stop being passionate about monorepo tooling for years – and I finally have something that’s <em>open source</em> that I can use and share with others. Improvements will continue to roll and I’m excited to see what others do and accomplish with <a href="https://onerepo.tools/">oneRepo</a>.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#B392F0">npx</span><span style="color:#79B8FF"> --package=onerepo</span><span style="color:#9ECBFF"> one</span><span style="color:#9ECBFF"> install</span></span>
<span class="line"></span></code></pre></aurora>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/onerepo-hero.BpR32NEn.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Rust-based JavaScript linters have a major issue]]></title>
            <link>https://paularmstrong.dev/blog/2024/02/23/2024-02-23-rust-based-javascript-linters-have-a-major-issue/</link>
            <guid>https://paularmstrong.dev/blog/2024/02/23/2024-02-23-rust-based-javascript-linters-have-a-major-issue/</guid>
            <pubDate>Fri, 23 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>It cannot be denied just how <em>fast</em> Rust-based linters for JavaScript are. The major tools available now are <a href="https://biomejs.dev">Biome</a> and <a href="https://oxc-project.github.io/">Oxc</a>, both running at nearly 100x the speed of ESLint. But they have one major drawback and I don’t think they’re going to take over ESLint without it…</p>
<p><a href="https://paularmstrong.dev/blog/2024/02/23/2024-02-23-rust-based-javascript-linters-have-a-major-issue/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>It cannot be denied just how <em>fast</em> Rust-based linters for JavaScript are. The major tools available now are <a href="https://biomejs.dev">Biome</a> and <a href="https://oxc-project.github.io/">Oxc</a>, both running at nearly 100x the speed of ESLint. But they have one major drawback and I don’t think they’re going to take over ESLint without it…</p>
<p class="text-3xl font-bold text-red-500">❌ No support for custom rules and plugins</p>
<p>That’s it. While Biome intends to <a href="https://biomejs.dev/blog/roadmap-2024/#plugins">explore (supporting) plugins</a>, there are no concrete plans in either tool – nor are they likely to support plugins via JavaScript/TypeScript.</p>
<p><em>Why is this a problem?</em></p>
<p>While I understand that the intention for both of these projects is to be opinionated, at the same time, those opinions have been formed slowly over time by the large community of JavaScript developers. Blocking the ability to support new ideas – or even blocking behind another language barrier – will result in stagnating progress.</p>
<p>Furthermore, individual teams often have very sutom needs to help prevent problematic patterns and catch errors that are unique to their software. I’ve written tens of custom ESLint rules over the last decade for teams that helped us prevent real problems that otherwise could go unnoticed until they persisted in production for weeks.</p>
<hr>
<p>I’m hopeful that these Rust-based tools succeed, because they <em>are</em> fast. But I can’t imagine they will without a JS-based plugin &#x26; custom rule API.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[There can be only one Array<T>]]></title>
            <link>https://paularmstrong.dev/blog/2024/02/05/typescript-array-syntax-correct-format/</link>
            <guid>https://paularmstrong.dev/blog/2024/02/05/typescript-array-syntax-correct-format/</guid>
            <pubDate>Mon, 05 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>This little hill I will forever defend. The shorthand syntax for Arrays in TypeScript is unnecessary and unclear.</p>
<p><a href="https://paularmstrong.dev/blog/2024/02/05/typescript-array-syntax-correct-format/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>This is that one little syntax hill that I will forever defend. The shorthand syntax for Arrays in TypeScript is unnecessary and unclear.</p>
<div class="grid grid-cols-2 gap-8">
```ts title="Proper Array syntax is good"
Array<t>
```
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#79B8FF">T</span><span style="color:#E1E4E8">[]</span></span>
<span class="line"></span></code></pre>
</t></div>
<h2 id="why-you-should-use-the-array-type">Why you should use the Array type</h2>
<ul>
<li>
<p>Immediately clear when reading left-to-right that the type is an <code>Array</code>.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> MyStuff</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;{ </span><span style="color:#FFAB70">some</span><span style="color:#F97583">:</span><span style="color:#B392F0"> BigObject</span><span style="color:#E1E4E8"> }>;</span></span>
<span class="line"></span></code></pre>
</li>
<li>
<p>Matches the syntax for <code>Set&#x3C;T></code> and <code>Map&#x3C;K, V></code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> MyArr</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">T</span><span style="color:#E1E4E8">>;</span></span>
<span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> MySet</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> Set</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">T</span><span style="color:#E1E4E8">>;</span></span>
<span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> MyMap</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> Map</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">K</span><span style="color:#E1E4E8">, </span><span style="color:#B392F0">V</span><span style="color:#E1E4E8">>;</span></span>
<span class="line"></span></code></pre>
</li>
</ul>
<h2 id="why-you-should-not-use-array-shorthand">Why you should not use Array shorthand</h2>
<ul>
<li>
<p>Easily overused on complex types</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">type</span><span style="color:#B392F0"> MyStuff</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> { </span><span style="color:#FFAB70">some</span><span style="color:#F97583">:</span><span style="color:#B392F0"> BigObject</span><span style="color:#E1E4E8"> }[];</span></span>
<span class="line"></span></code></pre>
<p>You’re probably going to argue that <code>{ some: BigObject }</code> should be further extracted out, and you’re right, but remember that people are inherently lazy and won’t always do that.</p>
</li>
<li>
<p>Easily mistaken between tuple types, but they are not identical:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#E1E4E8">[string] </span><span style="color:#F97583">!==</span><span style="color:#E1E4E8"> string[]</span></span>
<span class="line"></span></code></pre>
</li>
</ul>
<h2 id="automate-the-good-syntax">Automate the good syntax</h2>
<p>Arguably, it’s hard to enforce a single way when two different syntaxes have the exact same meaning. Luckily, there’s an auto-fixing ESLint rule for that: Use <a href="https://typescript-eslint.io/rules/array-type/#generic"><code>@typescript-eslint/array-type</code></a> set to <code>"generic"</code>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#79B8FF">module</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">exports</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  rules: {</span></span>
<span class="line"><span style="color:#9ECBFF">    '@typescript-eslint/array-type'</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">'error'</span><span style="color:#E1E4E8">, { default: </span><span style="color:#9ECBFF">'generic'</span><span style="color:#E1E4E8">, readonly: </span><span style="color:#9ECBFF">'generic'</span><span style="color:#E1E4E8"> }],</span></span>
<span class="line"><span style="color:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Review: Things I wanted to see in JavaScript and Frontend development in 2023]]></title>
            <link>https://paularmstrong.dev/blog/2024/01/25/javascript-and-frontend-things-to-see-in-2023-review/</link>
            <guid>https://paularmstrong.dev/blog/2024/01/25/javascript-and-frontend-things-to-see-in-2023-review/</guid>
            <pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Taking a look back at the things I was hoping to see more of in 2023 for JavaScript and frontend development.</p>
<p><a href="https://paularmstrong.dev/blog/2024/01/25/javascript-and-frontend-things-to-see-in-2023-review/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>Last year, I posted “<a href="/blog/2023/01/20/javascript-and-frontend-things-to-see-in-2023/">Six things I want to see in JavaScript and Frontend development in 2023</a>”. It’s been just over one year since that post and I thought it’d be fun to do a quick recap and grade each one.</p>
<p>Unfortunately, I have to give us all overall <strong>GPA 1.0</strong> due to way too many failures.</p>
<ol>
<li>
<p><strong>Usage of Axios declines or goes EOL</strong></p>
<p><strong>Grade: <span class="text-red-500">F</span></strong></p>
<p>I am sorely disappointed. What I’ve learned from working with various teams is that Axios tends to be what people use when they’re uninformed, not up to date with standards, and just copying examples from the Internet.</p>
</li>
<li>
<p><strong>Successful use of the <a href="https://nodejs.org/docs/latest-v18.x/api/test.html">Node.js Test Runner</a></strong></p>
<p><strong>Grade: <span class="text-orange-500">D</span></strong></p>
<p>I haven’t really seen anyone pick this up in a full context yet. I would still love to see this happen. I did actually think about it, but there’s still too much complexity when you’re writing TypeScript and have other complex needs.</p>
</li>
<li>
<p><strong>Less divisive stances on CSS solutions</strong></p>
<p><strong>Grade: <span class="text-red-500">F</span></strong></p>
<p>Nope. Everyone is still an armchair expert that will tell you whatever you’re using is terrible and you should use their favorite solution instead.</p>
</li>
<li>
<p><strong>Less venture capital backed open source</strong></p>
<p><strong>Grade: <span class="text-yellow-500">C</span></strong></p>
<p>I honestly don’t know. Maybe that’s a good thing. I haven’t seen a lot of new big open source stuff popping up with VC funding, but I have heard of a lot of startups getting going.</p>
</li>
<li>
<p><strong>Decline of weak Eslint rules and configs</strong></p>
<p><strong>Grade: <span class="text-green-500">B</span></strong></p>
<p>We’re getting there! A few projects like <a href="https://biomejs.dev/linter/">Biome</a> and <a href="https://oxc-project.github.io/blog/2023-12-12-announcing-oxlint.html">Oxlint</a> are even making alternatives to ESLint without all the fluff that are nearly hundreds of times faster.</p>
<p>ESLint itself is <a href="https://eslint.org/blog/2023/10/deprecating-formatting-rules/">deprecating formatting rules</a> in favor of allowing a proper formatter to do them for you.</p>
<p>Overall, things are better, but we’re not quite there yet.</p>
</li>
<li>
<p><strong>React either gets it’s crap together or gets out of the way</strong></p>
<p><strong>Grade: <span class="text-red-500">F</span></strong></p>
<p>Nope. React is worse than ever and continually seen as a frustration point.</p>
</li>
</ol>
<p>Oh well, we failed miserably – but there are some things to look forward to yet.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[App Defaults 2023]]></title>
            <link>https://paularmstrong.dev/blog/2023/11/27/apps-tools-for-2023/</link>
            <guid>https://paularmstrong.dev/blog/2023/11/27/apps-tools-for-2023/</guid>
            <pubDate>Mon, 27 Nov 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Here’s a little trend for 2023 that’s going around: listing out the default apps (and tools!) that I’ve used in 2023. I enjoy this little go around, as I’ve been getting to discover new apps that I might want to try.</p>
<p><a href="https://paularmstrong.dev/blog/2023/11/27/apps-tools-for-2023/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>Here’s a little trend for 2023 that’s going around: listing out the default apps (and tools!) that I’ve used in 2023. I enjoy this little go around, as I’ve been getting to discover new apps that I might want to try.</p>
<aside title="See also…">
<p>There’s also this big list of of other blog posts that have done similar lists. Check out a big list of <a href="https://defaults.rknight.me/">similar blog lists on “App Defaults”</a>.</p>
</aside>
<p>I also did <a href="/blog/2022/10/18/current-tool-chest/">something similar</a> last year, but in a much more limited capacity.</p>
<ul>
<li>
<p><span id="mail">📨 Mail client:</span></p>
<ul>
<li>Personal: <a href="https://apps.apple.com/us/app/mail/id1108187098">Apple Mail</a> – I’m transitioning my personal life away from GMail and Google services. Who knows if Apple is actually <em>better</em> from a privacy standpoint, but I already pay for iCloud and own my own domain. It has worked will for the past half of this year.</li>
<li>Work: <a href="https://www.google.com/gmail/about/">Gmail</a>, but <a href="https://support.apple.com/en-us/104996">saved to the Dock</a> through Safari.</li>
</ul>
</li>
<li>
<p>📮 Mail server: (see also <a href="#mail">Mail client</a>)</p>
<ul>
<li>Personal: <a href="https://support.apple.com/guide/icloud/mail-on-icloudcom-overview-mm6b1a17e3/icloud">iCloud Mail</a></li>
<li>Work: <a href="https://www.google.com/gmail/about/">Gmail</a></li>
</ul>
</li>
<li>
<p>📆 Calendar: (see also <a href="#mail">Mail client</a>)</p>
<ul>
<li>Personal: Apple Calendar – fits with the whole <em>moving away from Google</em> thing.</li>
<li>Work: Google Calendar – Again, I don’t have a choice on this, but I <em>do</em> have the app <a href="https://support.apple.com/en-us/104996">saved to Dock</a></li>
</ul>
</li>
<li>
<p><span id="notes">📝 Notes:</span> <a href="https://notion.so">Notion</a></p>
<p>I had a lot of hesitation with Notion until I was forced to use it through work. I would prefer an open source alternative, but none have as powerful of database features as Notion does and I’m making heavy use of them.</p>
<p>I was previously trying to use <a href="#cloud-files">Nextcloud</a> to handle notes and many other things, but the web UI is slow and painfully cumbersome.</p>
</li>
<li>
<p>✅ To-Do:</p>
<ul>
<li>Personal: <a href="#notion">Notion</a> – with heavy use of database tables and ability to cross-link items to other pages in Notion really helps me keep track of what I’ve got going on.</li>
<li>Open source: <a href="https://github.com/paularmstrong">GitHub issues</a></li>
<li>Work: <a href="https://www.shortcut.com/">Shortcut</a> – I dislike this tool almost as much as JIRA. It gets too complicated as soon as managers, project managers, compliance, and legal start adding requirements.</li>
</ul>
</li>
<li>
<p>📷 Photo Shooting: iPhone 13 Pro</p>
</li>
<li>
<p>🎨 Photo Editing: None</p>
</li>
<li>
<p><span id="cloud-files">📁 Cloud file storage:</span> <a href="https://nextcloud.com/">NextCloud</a></p>
<p>I keep this privately hosted. It’s nice to know that my files are actually <em>my</em> files. I also have quite a few local backups.</p>
</li>
<li>
<p>📖 RSS: <a href="https://github.com/nextcloud/news">NextCloud News</a></p>
</li>
<li>
<p>🙍🏻‍♂️ Contacts: <a href="https://apps.apple.com/us/app/contacts/id1069512615">Apple Contacts</a></p>
</li>
<li>
<p><span id="browser">🌐 Browser:</span> <a href="https://arc.net/gift/628133c8">Arc</a></p>
</li>
<li>
<p>💬 Chat:</p>
<ul>
<li><a href="https://apps.apple.com/us/app/messages/id1146560473">Messages</a> – SMS/texts and iMessage</li>
<li><a href="https://slack.com">Slack</a> – Both for work and the local BendJS meetup group.</li>
<li><a href="https://www.signal.org/">Signal</a> – Because privacy matters</li>
</ul>
</li>
<li>
<p>🔖 Bookmarks: <a href="#browser">Arc</a></p>
</li>
<li>
<p>📑 Read It Later: <a href="#browser">Arc</a>, recently <a href="#notes">Notion</a> <a href="https://www.notion.so/help/web-clipper">Web Clipper</a></p>
</li>
<li>
<p>📜 Word Processing: <a href="https://code.visualstudio.com/">VSCode</a>, Markdown, <a href="#notes">Notion</a></p>
</li>
<li>
<p>📈 Spreadsheets: If I really need to do some complex manipulation, Google Sheets, but I’m equally or more likely to write a script with Node.js and process JSON files.</p>
</li>
<li>
<p>📊 Presentations:</p>
<ul>
<li>Personal: I haven’t given any talks or presentations in a few years. I’d like to get back into it, but I haven’t found the right fit.</li>
<li>Work: Google Sheets</li>
</ul>
</li>
<li>
<p>🛒 Shopping Lists: <a href="#notes">Notion</a></p>
</li>
<li>
<p>🍴 Meal Planning: <a href="#notes">Notion</a></p>
</li>
<li>
<p>💰 Budgeting and Personal Finance: N/A (😬)</p>
</li>
<li>
<p>📰 News: RSS, Reddit, Mastodon</p>
</li>
<li>
<p>🎵 Music: <a href="https://www.plex.tv/plexamp/">Plexamp</a></p>
<p>I have a very large library of music collected over more than two decades. I just can’t let go of the super rare albums and EPs that will never be on any of the cloud services.</p>
</li>
<li>
<p>🎤 Podcasts: None. I can’t pay attention to podcasts and do anything else at the same time.</p>
</li>
<li>
<p>🔐 Password Management: <a href="https://1password.com/">1Password</a></p>
</li>
<li>
<p>🧑‍💻 Code Editor: <a href="https://code.visualstudio.com/">VSCode</a></p>
<p>I reluctantly gave up trying to continue using <a href="https://www.sublimetext.com/">Sublime Text</a>. It took me 4 tries over nearly as many months to get VSCode configured in a way that didn’t feel overly intrusive. There are still a lot of quirks about it that I don’t like, but being on the same editor as those I work with has been really helpful.</p>
</li>
<li>
<p>✈️ VPN: <a href="https://openvpn.net/">OpenVPN</a></p>
</li>
</ul>
<h2 id="bonus">Bonus</h2>
<ul>
<li>
<p>🚀 Launcher: <a href="https://www.alfredapp.com/">Alfred</a></p>
<p>I bought a “Mega Supporter” Powerpack license years ago. I’ve had free upgrades for years and never a single complaint.</p>
</li>
<li>
<p>🎥 Screen recording: <a href="https://www.araelium.com/screenflick-mac-screen-recorder">Screenflick</a></p>
</li>
<li>
<p>☕️ No sleep: <a href="https://keepingyouawake.app/">KeepingYouAwake</a></p>
<p>Don’t tell your IT/security team about this. When you work from home and work requires and resets your screensaver/sleep timeout down to 5 minutes, waking up and logging into your computer can be a nuisance when you make frequent kitchen &#x26; bathroom breaks. This little gem can keep your computer active for hours – no mouse jiggler required.</p>
</li>
<li>
<p>🪟 Window organization: <a href="https://manytricks.com/moom/">Moom</a></p>
<p>I’ve bound a bunch of custom keyboard shortcuts for organizing windows across my screen(s). This has been a staple of my productivity toolchain for years.</p>
</li>
</ul>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[We use type safety not on preference, but because we want to make money]]></title>
            <link>https://paularmstrong.dev/blog/2023/09/14/we-use-type-safety-not-on-preference-but-because-we-want-to-make-money/</link>
            <guid>https://paularmstrong.dev/blog/2023/09/14/we-use-type-safety-not-on-preference-but-because-we-want-to-make-money/</guid>
            <pubDate>Thu, 14 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>A short recounting of the thing that changed my mind forever on strict type checking for JavaScript.</p>
<p><a href="https://paularmstrong.dev/blog/2023/09/14/we-use-type-safety-not-on-preference-but-because-we-want-to-make-money/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>Before TypeScript was a viable option for most existing projects, there was <a href="https://flow.org/">flow</a> (there still is <em>flow</em>, it’s just not widely used outside of Meta these days). And before flow, there was just plain JavaScript. We wrote our React applications in JavaScript because that was all we had.</p>
<p>The company I worked for displayed timelines of user-generated content using our React application. Each entry on that timeline we referred to as a “Tweet”. And in this timeline of Tweets, we had <em>promoted content</em>, or as many people would call them, “advertisements”. Selling advertisements was how the company made money.</p>
<p>In order to appropriately bill the advertiser, we needed to have a reliable count of how many times their advertisement was displayed for each and every user. It also helped us know that the advertisement was displayed the correct number of times per the sale contract with the advertiser.</p>
<p>Each one of these promoted Tweets needed to have an attribution property, something like <code>promotedContentId</code>. This property was then used when logging visible Tweets that it was in fact displayed. It was a relational ID that told the backend systems exactly which Tweet from which advertiser was being displayed.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#E1E4E8">  entries.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">entry</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#79B8FF">TimelineEntry</span></span>
<span class="line"><span style="color:#B392F0">        entry</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{entry}</span></span>
<span class="line"><span style="color:#B392F0">        id</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{entry.id}</span></span>
<span class="line"><span style="color:#B392F0">        promotedContemtId</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{entry.promoted </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> entry.promoted.id </span><span style="color:#F97583">:</span><span style="color:#79B8FF"> undefined</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#B392F0">        userId</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{entry.user.id}</span></span>
<span class="line"><span style="color:#E1E4E8">      /></span></span>
<span class="line"><span style="color:#E1E4E8">    );</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>We made a typo. Instead of <code>promotedContentId="value"</code> being passed to the React component, we use <code>promotedContemtId="value"</code>. Did you catch it? Maybe because you were looking for it. But for the code author and reviewers looking at a change with a couple hundred lines of code differing, it would be really easy to miss in a sea of red and green.</p>
<p>This typo caused a few things:</p>
<ol>
<li>The same advertisements were being displayed over and over again to the same viewers. Since they were never attributed as being seen, it was assumed that the advertisement still hadn’t reached the viewer that it was intended to.</li>
<li>We were unable to bill the advertiser for the views on their advertisement. According to our database, the advertisement was never displayed, so there was no count of views, so we had not yet fulfilled our end of the contract.</li>
</ol>
<p>We were using the <a href="https://www.npmjs.com/package/prop-types"><code>prop-types</code></a> package, like everyone else at the time. From the prop-types readme (<em class="text-red-700 dark:text-red-500">emphasis mine</em>):</p>
<blockquote>
<p>You can use prop-types to document the intended types of properties passed to components. React will check props passed to your components against those definitions, and <em class="text-red-700 dark:text-red-500">warn in development</em> if they don’t match.</p>
</blockquote>
<p>Warnings are pointless. They are nothing but noise and invisible unless you’re explicitly looking for <em>potential</em> issues and remember to do anything about it. They do not block and cannot be statically analyzed in CI systems.</p>
<p>Because there was no real error, just a <em>warning</em>, this issue went into production and was there for at least a few days before it was noticed. It potentially cost the company <em>hundreds of thousands of dollars</em> in revenue – probably more.</p>
<p>We had a post-mortem on the issue and came out of it with the following action items:</p>
<ul>
<li>
<p><strong>Add an integration test.</strong></p>
<p>At the very least, adding an integration test will prevent <em>this same issue</em> from happening in <em>this same manner</em>.</p>
</li>
<li>
<p><strong>Add strict type safety.</strong></p>
<p>The integration test will help, but only for this exact path on this exact issue; it won’t prevent <em>similar issues</em> from happening in other places in the future. The only thing that <em>can</em> prevent similar issues was to make all of our attributes and argument keys absolutely strict &#x26; fail during prevent pull-requests from merging when failing the type checking via automated CI processes.</p>
</li>
</ul>
<p>I could see common counter arguments here that there should just be more end-to-end and integration testing. But how strong and reliable are your test environments to ensure that this all works correctly? How much effort, time, and money does it take to maintain full testing solutions? Sure, you could have some and keep them lightweight, but now you’re picking and choosing what’s important to test and what’s not. Strict type safety is the cheapest solution that can apply to everything, everywhere. It’s a contract that cannot be broken.</p>
<p>So we converted our codebase from plain JavaScript over the next year or so to have full strict type safety. We never had the same type of issue again, because our pull requests were always strictly enforced, requiring conformance with strict type safety.</p>
<p>In the end, type safety likely saved us from losing more money. How many countless other issues did type safety solve? It’s impossible to tell, because it prevented us from ever encountering them.</p>
<hr>
<p>One final note: I only ever mention <em>strict</em> type safety. That is TypeScript’s <a href="https://www.typescriptlang.org/tsconfig#strict">strict mode</a> and more: <code>any</code>, <code>Function</code>, <code>object</code>, and other ambiguous types are also explicitly disallowed, as they are a rejection of purpose of type checking.</p>
<p>The <code>any</code> type was a mistake and never should have been included in TypeScript in the first place. It is disappointing to see the number of published libraries that embrace <code>any</code> types, making it difficult to verify with certainty that code is correct.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Import path aliasing is a crutch for poor architecture]]></title>
            <link>https://paularmstrong.dev/blog/2023/08/29/import-path-aliasing-is-a-crutch-for-poor-architecture/</link>
            <guid>https://paularmstrong.dev/blog/2023/08/29/import-path-aliasing-is-a-crutch-for-poor-architecture/</guid>
            <pubDate>Tue, 29 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Path aliasing, sometimes referred to as import or module resolution, in the earliest and most naïve sense, is a method of overloading the <a href="https://nodejs.org/docs/latest-v18.x/api/modules.html#all-together">Node Require Resolution Algorithm</a> so that it first looks in some particular defined folders for modules of a given name before looking for installed modules of the same name in <code>node_modules</code> folders. While this seems handy at first glance, in practice, it’s is an unnecessary maintenance overhead in large distributed teams and a sign of poor code organization and architecture.</p>
<p><a href="https://paularmstrong.dev/blog/2023/08/29/import-path-aliasing-is-a-crutch-for-poor-architecture/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import { Icon } from ‘astro-icon/components’;
import Aside from ’../../components/Aside.astro’;
import Button from ’../../components/Button.astro’;
import SmellyImage from ’../../images/blog/2023-08-import-paths/smelly.png’;
import CleaningImage from ’../../images/blog/2023-08-import-paths/cleaning.png’;</p>
<p>Path aliasing, sometimes referred to as import or module resolution, in the earliest and most naïve sense, is a method of overloading the <a href="https://nodejs.org/docs/latest-v18.x/api/modules.html#all-together">Node Require Resolution Algorithm</a> so that it first looks in some particular defined folders for modules of a given name before looking for installed modules of the same name in <code>node_modules</code> folders. In the early days of Node.js first availability, this was somewhat common practice to overload the <code>require.paths</code> array with some extra stuff:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">require.paths.</span><span style="color:#B392F0">unshift</span><span style="color:#E1E4E8">(path.</span><span style="color:#B392F0">join</span><span style="color:#E1E4E8">(__dirname, </span><span style="color:#9ECBFF">'src'</span><span style="color:#E1E4E8">));</span></span>
<span class="line"></span></code></pre>
<p>With modern tooling, like TypeScript compiler paths, <a href="https://vitejs.dev/config/shared-options.html#resolve-alias">Vite <code>resolve.alias</code></a>, <a href="https://webpack.js.org/configuration/resolve/#resolvealias">WebPack <code>resolve.alias</code></a>, <a href="https://github.com/rollup/plugins/tree/master/packages/alias#entries"><code>@rollup/plugin-alias</code></a>, <a href="https://www.npmjs.com/package/esbuild-plugin-alias">esbuild-plugin-alias</a>, and many others, more specific aliasing or even prefixes for entire paths can be used:</p>
<div class="no-bustout">
```json title="tsconfig.json" {3-5} caption="Typescript compiler options configuration for import path aliases."
{
  "compilerOptions": {
    "paths": {
      "@/*": "./src/*",
    }
  }
}
```
</div>
<p>When configured using the above, any TypeScript project can have files that import from the prefixed alias, short-circuiting the relative lookups that are normally done.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { Button } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/components/Button'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<aside type="tip" title="Skip all of this and remove aliasing now">
  Have some repos that overuse import path aliasing and want to easily get rid of it and not read over a thousand more
  words first? I've packaged up an automated codemod just for you! [Skip to the end](#easily-revert-path-aliasing) to
  get started.
</aside>
<h2 id="the-problems-with-import-path-aliasing">The problems with import path aliasing</h2>
<p>Import path aliasing is a crutch for poorly organized and architected codebases.</p>
<p>The following example probably looks fine to most people:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> Button </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/components/Button'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> TextInput </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/components/TextInput'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> useGetUserQuery </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/shared/api/get-user'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> useLoginMutation </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/shared/api/login'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<p>Expanded out to use the relative paths, it may be more clear why people tend to read for prefix aliasing. It’s messy to read repetitive <code>../</code> and determine if they are correct or not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> Button </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../../../../../../../components/Button'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> TextInput </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../../../../../../../components/TextInput'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> useGetUserQuery </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../../../../../../../shared/api/get-user'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> useLoginMutation </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../../../../../../../shared/api/login'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<p>But this <a href="https://en.wikipedia.org/wiki/Code_smell">code <em>smells</em></a>. This seven-level deep import in the previous example means that the source file is nested at least <em>seven</em> directories within the root of the project. When code is actively and commonly importing from many levels higher in its file tree and reaching deep into separate folders, the organization of the application as a whole is going to be really difficult to follow and maintain – especially for new team members or infrequent contributors.</p>
<p>Hiding the organizational issues behind path aliasing does nothing for the greater organization, but puts it off for another day.</p>
<div class="bustout-sm">
  <img src="{SmellyImage}" width="{1024}" alt="">
</div>
<h3 id="lack-of-standards">Lack of standards</h3>
<p>The most major issue is that there are no standards across frameworks and tooling. Every setup is ad-hoc. While some projects tend to use something like <code>~/</code> or <code>@/</code> to point to the <code>src/</code> directory, many do not. Some even alias unprefixed straight to one or more directories.</p>
<p>From project to project, there’s no way to know what the correct aliasing is without referencing the configuration(s). And furthermore, just because a project <em>has</em> aliasing, doesn’t mean that it is enforced. Imports can always written as relative imports.</p>
<h3 id="enforcement">Enforcement</h3>
<p>There’s no way to enforce and ensure that aliases are always used. Files end up with a mix of aliases and relative imports and it is unclear whether the filepath imports are located next to each other or not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { Button } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/components/form/Button'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { TextField } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../../../components/form/TextField'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<h3 id="confusion">Confusion</h3>
<p>The following two examples are canonical, but one is longer and more complicated than the other – and it’s not the alias version.</p>
<div class="bustout-sm">
<div class="gap-8 no-bustout lg:grid lg:grid-cols-2">
```ts title="src/components/TextField.tsx" caption="Example relative import from the same directory using aliases."
import { Label } from '@/components/Label';
```
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { Label } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './Label'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
</div>
</div>
<h3 id="maintenance-issues">Maintenance issues</h3>
<p>Then there are other static analyzers that don’t understand how to read a <code>tsconfig</code>, <code>vite.config</code>, or whatever is in use to set up aliases. There are many potential places and ways to configure aliasing – and not all of them are compatible with reusing a single configuration, but will have to be done as duplicated configurations slightly differently.</p>
<p>Consider ESLint and using <code>eslint-plugin-import</code> rule <a href="https://github.com/import-js/eslint-plugin-import/blob/HEAD/docs/rules/no-cycle.md"><code>import/no-cycle</code></a> or <a href="https://github.com/import-js/eslint-plugin-import/blob/6b95a021938139726b3f862beb37012d6e2afab2/docs/rules/no-self-import.md"><code>import/no-self-import</code></a>: neither are directly configurable to understand aliasing. Instead, another plugin for ESLint is now required with an array-map configuration, completely different from <code>tsconfig</code> and other setups: <a href="https://www.npmjs.com/package/eslint-import-resolver-alias">eslint-import-resolver-alias</a>.</p>
<div class="no-bustout">
```js title=".eslintrc.cjs" showLineNumbers {5-7}
module.exports = {
  settings: {
    'import/resolver': {
      alias: {
        map: [
          ['@', './src'],
        ],
      },
    },
  },
};
```
</div>
<h4 id="down-the-rabbit-hole">Down the rabbit hole</h4>
<p>There are countless other tools and static analyzers that might need to be configure to use the aliasing as well.</p>
<p>More times than I can remember I have also needed to write large code transforms that read through <code>ImportDeclaration</code>s, looking for particular files and updating references, imported methods, and then rewriting code based on them. When shared across different applications with different setups, I’ve needed to figure out how to take in custom configurations in order to do the mapping from alias to resolved path.</p>
<h3 id="published-modules">Published modules</h3>
<p>If a reusable module is using path aliasing and is going to be published to a registry, it needs ensure that its build tools also rewrite the aliasing before publish. Not all of the plugins and tools enabling aliasing do this automatically, so it’s yet another step to create with caution and care.</p>
<h3 id="pandoras-box">Pandora’s box</h3>
<p>A team might say that they’re going to standardize on just using the <code>@/* → ./src/*</code> aliasing across all of codebases and teams in order to reduce maintenance issues.</p>
<p>But by adding aliasing in the first place, the door is already open for endless possible aliases. <em>Why</em> can’t someone add another alias for a set of common utils that they keep needing? What’s actually stopping them from doing it? If it’s okay, how is it communicated to the team? How is it <a href="#enforcement">enforced</a>?</p>
<hr>
<h2 id="better-organization">Better organization</h2>
<p>Often times, the reason codebases develop problems that make path aliasing attractive comes from the many frameworks with short-sighted recommendations (and sometimes requirements) for how to organize code in an application. Typically for web application, <code>api</code>, <code>pages</code>, <code>components</code>, <code>hooks</code>, and <code>modules</code> are seen as <em>top-level</em> and everything is thrown into them.</p>
<div class="no-bustout">
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="txt"><code><span class="line"><span>└── src</span></span>
<span class="line"><span>    ├── api</span></span>
<span class="line"><span>    ├── components</span></span>
<span class="line"><span>    ├── hooks</span></span>
<span class="line"><span>    ├── modules</span></span>
<span class="line"><span>    └── pages</span></span>
<span class="line"><span>        ├── auth</span></span>
<span class="line"><span>        │   ├── login.tsx</span></span>
<span class="line"><span>        │   └── logout.tsx</span></span>
<span class="line"><span>        ├── dashboard</span></span>
<span class="line"><span>        └── home</span></span>
<span class="line"><span></span></span></code></pre>
</div>
<p>This looks just fine when getting started, but it fails to think forward to the application growing into a feature-rich and complex application. Frustrations start to arise when directories have too many files at a single level, so people come through and try to organize similar things together.</p>
<div class="no-bustout">
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>└── src</span></span>
<span class="line"><span>    └── components</span></span>
<span class="line"><span>        ├── auth</span></span>
<span class="line"><span>        │   └── login</span></span>
<span class="line"><span>        │       └── Form.tsx</span></span>
<span class="line"><span>        ├── cards</span></span>
<span class="line"><span>        │   └── …</span></span>
<span class="line"><span>        ├── form</span></span>
<span class="line"><span>        │   ├── inputs</span></span>
<span class="line"><span>        │   │   ├── PasswordInput.tsx</span></span>
<span class="line"><span>        │   │   ├── NumberInput.tsx</span></span>
<span class="line"><span>        │   │   └── TextInput.tsx</span></span>
<span class="line"><span>        │   └── buttons</span></span>
<span class="line"><span>        │       ├── PrimaryButton.tsx</span></span>
<span class="line"><span>        │       └── SecondaryButton.tsx</span></span>
<span class="line"><span>        └── grids</span></span>
<span class="line"><span>            └── …</span></span>
<span class="line"><span></span></span></code></pre>
</div>
<p>And this may work in the short term. But as teams grow and communication is less clear, these structures become cluttered with internal acronyms, duplication, and much more complex nesting.</p>
<p>Typically, teams become focused as part of a “product pillar”. These pillars allow optimization and specialization in varying areas, which makes colocation all of the application’s code more fragmented, despite best intentions to reuse as much as possible.</p>
<h3 id="use-a-monorepo">Use a monorepo</h3>
<div class="no-bustout">
```txt caption="Example ASCII-tree diagram of an application, its modules, and CODEOWNERS per area"
├── app                               # infra-team
│   └── src
│       └── pages
│           ├── auth                  # auth-team
│           │   ├── login.tsx
│           │   ├── logout.tsx
│           │   └── components
│           ├── dashboard             # home-team
│           │   ├── dashboard.tsx
│           │   └── components
│           └── home                  # home-team
│               ├── home.tsx
│               └── components
└── modules
    ├── components                    # ui-team
    │   ├── package.json
    │   └── src
    ├── api                           # api-team, infra-team
    │   ├── package.json
    │   └── src
    └── hooks                         # infra-team
        ├── package.json
        └── src
```
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plain"><code><span class="line"><span>*                         infra-team</span></span>
<span class="line"><span>app/src/pages/home        home-team</span></span>
<span class="line"><span>app/src/pages/dashboard   dashboard-team-team</span></span>
<span class="line"><span>app/src/pages/auth        auth-team</span></span>
<span class="line"><span>modules/components        ui-team</span></span>
<span class="line"><span>modules/api               api-team, infra-team</span></span>
<span class="line"><span>modules/hooks             infra-team</span></span>
<span class="line"><span></span></span></code></pre>
</div>
<h3 id="bonus-mentions">Bonus mentions</h3>
<p>In no particular order and less important, but worth calling out, here are some other pitfalls to avoid:</p>
<ul>
<li>
<p><strong>Names of modules and directories that are too generic.</strong></p>
<p><code>utils</code> tends to become a dumping ground for everything. Take an extra minute to plan <em>where</em> something should be based on it usage, reusability, and purpose.</p>
</li>
<li>
<p><strong>Reduce the number of imports in components.</strong></p>
<p>Importing from 20 or 30 files for a single component means the component is doing too much. Splitting it into logical bits will make it both easier to maintaing and test. Not only that, but patterns may arise that suggest that portions would be better <a href="#better-organization">organized</a> somewhere else within the codebase.</p>
</li>
</ul>
<hr>
<h2 id="easily-revert-path-aliasing">Easily revert path aliasing</h2>
<p>Hopefully I’ve done a good job and convinced some readers to stop using path aliasing. Unfortunately, the reversion process to move back from aliases to relative imports is manual, difficult, and error-prone.</p>
<p>However, I’ve had to help teams out enough times over the years to finally realize I should make my codemod public and easy to use.</p>
<h3 id="introducing-remove-aliasing">Introducing <code>remove-aliasing</code></h3>
<div class="not-prose flex gap-4">
  <button href="https://github.com/paularmstrong/remove-aliasing" target="_blank">
    <icon name="github" class="inline size-6 fill-current"> Star on GitHub
  </icon></button>
  <button href="https://www.npmjs.com/package/remove-aliasing" target="_blank">
    <icon name="npm" class="inline size-6 fill-current"> View on NPM
  </icon></button>
  <button href="https://github.com/paularmstrong/remove-aliasing/tree/main/README.md" target="_blank">
    <icon name="note" class="inline size-6 fill-current"> Documentation
  </icon></button>
</div>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#B392F0">npx</span><span style="color:#9ECBFF"> remove-aliasing@latest</span><span style="color:#79B8FF"> --root=</span><span style="color:#9ECBFF">"src/"</span><span style="color:#79B8FF"> --prefix=</span><span style="color:#9ECBFF">"@/"</span><span style="color:#9ECBFF"> src/</span></span>
<span class="line"></span></code></pre>
<div class="bustout-sm">
  <img src="{CleaningImage}" width="{1024}" alt="children’s drawing of a robot stacking papers in black and white">
</div>
<hr>
<h2 id="bonus-vscode-settings">Bonus VSCode settings</h2>
<p><em>Updated 2024-03-06</em>.</p>
<p>VSCode comes out of the box with an inconsistent setting: when adding imports for you, it will choose the <em>shortest</em> import path. So if your relative import is even 1-character longer than it would be as an alias, the alias will be used.</p>
<p>You should turn this off.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "javascript.preferences.importModuleSpecifier"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"project-relative"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "typescript.preferences.importModuleSpecifier"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"project-relative"</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>As another helper, make sure that VSCode automatically updates import paths when you move files. This will save you some confusion in those rare instances where things need to be moved around. Be warned, though: moving files across Workspaces in a monorepo may have unexpected results.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "javascript.updateImportsOnFileMove.enabled"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"always"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "javascript.preferences.importModuleSpecifier"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"project-relative"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "typescript.updateImportsOnFileMove.enabled"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"always"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "typescript.preferences.importModuleSpecifier"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"project-relative"</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/robot-line.DGiogqAz.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The line between writing functions yourself and using open source modules]]></title>
            <link>https://paularmstrong.dev/blog/2023/08/14/the-line-between-writing-functions-yourself-and-using-open-source-modules/</link>
            <guid>https://paularmstrong.dev/blog/2023/08/14/the-line-between-writing-functions-yourself-and-using-open-source-modules/</guid>
            <pubDate>Mon, 14 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>When was the last time you stopped to think about the cost of a new third-party package that you’re adding into your process?</p>
<p>Where do you recommend balancing between “not invented here” syndrome and using packages just because they exist? I always come back to the “left-pad incident” as an extreme example to avoid the latter, but where should we be drawing the line between the two?</p>
<p>Let’s take a moment to break down considerations that I try to make when balancing writing code against pulling in third-party modules…</p>
<p><a href="https://paularmstrong.dev/blog/2023/08/14/the-line-between-writing-functions-yourself-and-using-open-source-modules/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import HeroImage from ’../../images/blog/2023-08-packages/packages.jpg’;
import JuiceSqueezeImage from ’../../images/blog/2023-08-packages/juice-squeeze.jpg’;</p>
<p>When was the last time you stopped to think about the cost of a new third-party package that you’re adding into your process?</p>
<p>Where do you recommend balancing between “not invented here” syndrome (<a href="(https://en.wikipedia.org/wiki/Not_invented_here)">NIH</a>) and using packages just because they exist? I always come back to the “left-pad incident” as an extreme example to avoid the latter, but where should we be drawing the line between the two?</p>
<hr>
<p><img src="{HeroImage}" width="{1152}" alt="AI generated image of colorful boxes of varying sizes stacked on top of each other"></p>
<p>While npm doesn’t publish statistics of the number of packages in its registry, in 2019 they passed the <a href="https://blog.npmjs.org/post/615388323067854848/so-long-and-thanks-for-all-the-packages.html">1 million package mark</a>. That means we have <em>at least</em> 1 million packages we could possibly consider using.</p>
<p>That is utterly too many packages. I’m sure we’ve all contributed in some way to the proliferation of <em>a module for everything</em>, but where can we start drawing the line and how do we actually determine whether we should be using one package over another over writing it ourselves?</p>
<hr>
<p>Some considerations that I’ve been thinking about when looking at third-party JavaScript modules:</p>
<ul>
<li><a href="#internal-ability-and-understanding">Can we actually do it all on our own?</a></li>
<li><a href="#maintenance-and-bug-fixing">What’s the cost of maintenance and bug fixes?</a></li>
<li><a href="#bundle-size">How will a third-party module affect bundle size?</a></li>
<li><a href="#is-the-juice-worth-the-squeeze">Will the effort to build an equivalent be too high?</a></li>
<li><a href="#duplicate-concerns">Do we already have a package that satisfies this use case?</a></li>
<li><a href="#standard-apis">Is there an equivalent standard API?</a></li>
<li><a href="#code-review">Does the module actually work as advertised?</a></li>
</ul>
<h2 id="internal-ability-and-understanding">Internal ability and understanding</h2>
<p>I’m currently torn between writing some (what I would consider) simple DOM layout measurements for tooltip positioning versus reaching for <a href="https://floating-ui.com/">floating-ui</a> to do it for me. The library actually has much more than I need, but it’s tree-shakeable, so maybe that’s okay.</p>
<p>I’m confident that <em>I</em> can write and maintain my own measurements and <code>ResizeObserver</code>, but does my team all feel the same? But then again, if they can’t, maybe I should be helping and encouraging them to learn.</p>
<p>There’s no single answer to all of this and it should be carefully taken into consideration. <em>Just because we can, doesn’t mean we should.</em></p>
<h2 id="maintenance-and-bug-fixing">Maintenance and bug fixing</h2>
<p>Extending from the internal ability and understanding, reaching for a library without the knowledge of what it’s actually doing can be worse than copy/pasting from StackOverflow. At least with copying code from somewhere, while you didn’t write it, it’s now yours and your team’s to understand and maintain. If it ends up having a bug in it, you are responsibe and should be able to fix and deploy it at any time.</p>
<p>When pulling in a third-party module, you’re at the mercy of the open source authors &#x26; maintainers – even if you create a pull request, will it get fixed, merged, and deployed for you? Would you even have been able to get ramped up on contributing to the open source repository in a realistic timeframe?</p>
<p>Let’s say you decide to fork a library and maintain it internally. Well now you’ve got an issue of how its repository is set up, standards, lint rules, etc. There are many concerns to consider when forking.</p>
<h2 id="bundle-size">Bundle size</h2>
<p>One of my first stops when evaluating a third-party package is <a href="https://bundlephobia.com/">BundlePhobia</a>. This gives me a general idea of how much a the package will increase my overall bundle size after building and deploying for production, as well as show what <em>other</em> dependencies it has and how their makeup affects the build.</p>
<ul>
<li>Is the relative size of the package realistic for what I’m expecting it to do?</li>
<li>Do any of its dependencies overlap with what I’m already using, and thus may not actually increase my final bundle size as much as this says?</li>
</ul>
<h2 id="is-the-juice-worth-the-squeeze">Is the juice worth the squeeze?</h2>
<img src="{JuiceSqueezeImage}" width="{1152}" alt="AI generated image of oranges being squeezed into orange juice">
<p>I have a team rebuilding our core shared component library at work and we have a strict mandate to ensure WCAG 2.0 accessibility standards. The team knows what they’re doing and can absolutely succeed at meeting those on their own. However, while building the previous iteration of the library, the <code>&#x3C;DatePicker /></code> component took nearly three months to build.</p>
<p>I challenged the team, despite their hesitation about ARIA, to try using <a href="https://react-spectrum.adobe.com/react-aria/index.html">react-aria</a>, a major library of React hooks that aid in providing accessible UI primitives for design systems. One person came back on the same day with a prototype of a fully accessible <code>&#x3C;DatePicker /></code> and stated that it took them about two hours to build what previously took over two months.</p>
<p>Essentially, “is the juice worth the squeeze” means we can determine whether the output is worth the effort. In cases like the above, I can safely answer “no” and move forward with pulling in the dependency. Other times, maybe not.</p>
<h2 id="duplicate-concerns">Duplicate concerns</h2>
<p>Do we already have a package that fulfills this use case? I can’t even count how many applications I’ve seen running a combination of state management libraries like <a href="https://redux.js.org/">Redux</a>, <a href="https://redux-toolkit.js.org/">Redux Toolkit</a>, <a href="https://tanstack.com/query/latest/">React Query</a>, and <a href="https://docs.pmnd.rs/zustand/getting-started/introduction">Zustand</a> (among a scattering of others). Pick one and stick with it; don’t reach for a new library just because you heard it’s new.</p>
<h2 id="standard-apis">Standard APIs</h2>
<p>Is there a standard API that can already do much of the same thing? Check your browser support matrix via MDN or caniuse.com and consider standars in favor of older packages that fulfill the same purposes. Here are just a few of the packages and their standard equivalents that came up for me recently when auditing bundle bloat in applications:</p>

























<table><thead><tr><th>Third-party library</th><th>Equivalent standard</th></tr></thead><tbody><tr><td><a href="https://www.npmjs.com/package/dateformat"><code>dateformat</code></a></td><td><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat"><code>Intl.DateTimeFormat</code></a></td></tr><tr><td><a href="https://www.npmjs.com/package/axios"><code>axios</code></a></td><td><a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch"><code>fetch</code></a></td></tr><tr><td><a href="https://www.npmjs.com/package/uuid"><code>uuid</code></a></td><td><a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID"><code>crypto.randomUUID()</code></a></td></tr><tr><td><a href="https://www.npmjs.com/package/lodash"><code>lodash</code></a></td><td><a href="https://youmightnotneed.com/lodash/"><em>you might not need lodash</em></a></td></tr></tbody></table>
<h2 id="code-review">Code review</h2>
<p>Have we actually looked at the source of the module and verified that it does what it says? Does have tests asserting its behavior works as advertised, or has it been thrown together without concern?</p>
<p>We should actually read the code. Maybe it’s great, but maybe it’s also fundamentally flawed and we might pick that up by reading the code. Or just maybe we realize that it’s simple and we could have written this ourselves…</p>
<hr>
<p>What am I forgetting and what considerations do you make when pulling in new third party packages? Or did you forget to think through any of that today?</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/packages.kKsbzhCF.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[A clean codebase is a happy codebase]]></title>
            <link>https://paularmstrong.dev/blog/2023/03/24/a-clean-codebase-is-a-happy-codebase/</link>
            <guid>https://paularmstrong.dev/blog/2023/03/24/a-clean-codebase-is-a-happy-codebase/</guid>
            <pubDate>Fri, 24 Mar 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>How do so many people just ignore cruft in their projects and keep moving forward? I was fighting with a bunch of <code>package.json</code> files recently that had 10s of dependencies that were completely unused. Why was it left this way?</p>
<p><a href="https://paularmstrong.dev/blog/2023/03/24/a-clean-codebase-is-a-happy-codebase/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>How do so many people just ignore cruft in their projects and keep moving forward? I was fighting with a bunch of <code>package.json</code> files recently that had 10s of dependencies that were completely unused and it got me thinking about keeping codebases clean and tidy.</p>
<ul>
<li>There are files right there that never get used. Why aren’t you deleting them?</li>
<li>Why did you just comment out that big block of code you’re never going to use again instead of deleting it?</li>
<li>Why do you keep including that unused dependency in your package.json?</li>
</ul>
<p>One of the responses I got was that changing something you don’t know carries too much risk that you don’t have time to deal with if it goes awry:</p>
<blockquote>
<p>often the risk of changing unknowns is higher than the benefit. Maybe that dep does something and removing it will cause a build incident that’ll take a week to address.</p>
<p><cite>– <a href="https://macaw.social/@kpk/110067893337633391">@kpk@macaw.social</a></cite></p>
</blockquote>
<hr>
<p>Think of the same situation a little differently:</p>
<p>When you moved into your office, there were a pile of neatly folded towels in the corner of the room. You and all of your coworkers left them there, because you weren’t sure who was responsible and they didn’t seem to be causing a problem.</p>
<p>After a few months, some black mold starts spreading out from under the towels. Now you need to leave immediately, maybe go get checked up with your doctor, and have your office torn apart and the mold dealt with.</p>
<p>Was it really better just letting it go and not dealing with it?</p>
<hr>
<p>Many years ago I was trying to track down a layout error on a major sign in page. There was a random CSS class name being applied to a form element and some stylesheet getting added to the document <code>&#x3C;head /></code> – the class name did not exist in any form within our codebase. How was this possible?</p>
<p>As it turns out, someone had added a Ruby gem a couple of years prior. The mere <em>existence</em> of this gem caused various side effects to form fields that went completely unnoticed for over a year. I’m sure there was a good reason it was added initially, but it no longer served a purpose other than the fact that it broke our layouts.</p>
<p>It took me about 3 days to track this down and less than a minute to remove the dependency. I wish I could get those 3 days back.</p>
<hr>
<p>Let’s be honest, you’re not going to leave a pile of towels sitting around for no reason in an office. They don’t belong there. Just as well, you should not be leaving dependencies sitting around if they don’t serve a real, tested, obvious, or documented purpose.</p>
<p>The lesson is, if you spot a mess in code and dependencies, you’re better off cleaning it up now, lest it fester and cause bigger issues later.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Why the frontend world ignores Web Components]]></title>
            <link>https://paularmstrong.dev/blog/2023/03/11/why-we-do-not-write-web-components/</link>
            <guid>https://paularmstrong.dev/blog/2023/03/11/why-we-do-not-write-web-components/</guid>
            <pubDate>Sat, 11 Mar 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>The problem with Web Components has and always will be that they were designed for the browser and not for the developer. As many features as they get, they’re still ergonomically odd and difficult to maintain.</p>
<p><a href="https://paularmstrong.dev/blog/2023/03/11/why-we-do-not-write-web-components/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>The problem with Web Components has and always will be that they were designed for the browser and not for the developer. As many features as they get, they’re still ergonomically odd and difficult to maintain.</p>
<p>People choose React, Angular, Vue, etc because they are easier for the developer and they make sense in order to accomplish professional and company goals. They are making a tradeoff for on perf (for example) in order to actually work faster. On the company’s dollar, web components are more expensive.</p>
<hr>
<p>In my experience, Web Components really had a few big pieces that made them completely unapproachable:</p>
<ul>
<li>
<p><strong>Testability</strong></p>
<p>If you wanted to test your web components, you needed to have a full browser environment to run the tests. While this is no longer the case, there was a huge burden initially to be able to actually make an application. It took until 2019 for <a href="https://github.com/capricorn86/happy-dom">Happy-DOM</a> to come on the scene and not until <a href="https://github.com/jsdom/jsdom/pull/2548">2020 for JSDOM</a> to have support. That’s at least 4 years too late to even think about competing with other frameworks.</p>
</li>
<li>
<p><strong>Lack of unified frameworks</strong></p>
<p>How do you build Web Components? Well the short answer is you just write them according to spec. The long answer is that you probably use <em>a</em> framework that’s meant for building components. There are a few, so choose wisely. But that’s just the individual component level. Most developers are not just building piecemeal things and sharing them around – they’re paid to build <em>applications</em>.</p>
<p>Say you want to build an application. Let’s ignore the Single page application (<acronym title="Single page application">SPA</acronym>) vs Multi-page application (<acronym title="Multi-page application">MPA</acronym>) argument for a moment. Even just building an MPA, you were on your own to write build tooling, integrations, and everything in-between.</p>
<p>There are <em>some</em> out there, but I honestly can’t figure out where and/or how. I <em>think</em> you could maybe just use WebPack/Parcel/Vite? Maybe web components work in any framework, but apparently it can be <a href="https://css-tricks.com/using-web-components-with-next-or-any-ssr-framework/">a lot of work</a>.</p>
</li>
<li>
<p><strong>Cross-component state management</strong></p>
<p>Honestly, I don’t know – you’re probably on your own here. This just goes straight back to the previous point about lack of unified frameworks.</p>
</li>
<li>
<p><strong>Three languages in one</strong></p>
<p>I thought CSS-in-JS was such a bad thing? That’s what all the purists told me. Well now we’ve got CSS-in-JS and HTML-in-JS and now my editor needs to support three syntax highlighters in every file and I need to hope my string templates all compile correctly.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { html, css, LitElement } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'lit'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { customElement, property } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'lit/decorators.js'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">@</span><span style="color:#B392F0">customElement</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'simple-greeting'</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> SimpleGreeting</span><span style="color:#F97583"> extends</span><span style="color:#B392F0"> LitElement</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  static</span><span style="color:#FFAB70"> styles</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> css</span><span style="color:#9ECBFF">`</span></span>
<span class="line"><span style="color:#9ECBFF">    p {</span></span>
<span class="line"><span style="color:#9ECBFF">      color: blue;</span></span>
<span class="line"><span style="color:#9ECBFF">    }</span></span>
<span class="line"><span style="color:#9ECBFF">  `</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">  @</span><span style="color:#B392F0">property</span><span style="color:#E1E4E8">()</span></span>
<span class="line"><span style="color:#FFAB70">  name</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Somebody'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">  render</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#B392F0"> html</span><span style="color:#9ECBFF">`&#x3C;p>Hello, ${</span><span style="color:#79B8FF">this</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}!&#x3C;/p>`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
</li>
<li>
<p><strong>Purists…</strong></p>
<p>Speaking of those same purists that yell about CSS-in-JS being bad, they are the biggest thing that has put me off of Web Components.</p>
<p>The people who I refer to as “purists” that keep shouting about how we’re all wrong for using something else. I get it, Web Components probably <em>are</em> the better choice for the browser, performance, and end user when it is all said and done. But you’re the ones that created this stuff in a void – have you ever had to build a web application and maintain it for at least 5 years? Probably not since before 2010. Things were <em>different</em> then.</p>
<p>Just go build stuff and be proud of it. Stop shouting at me that I’m terrible for trying to get my job done.</p>
</li>
</ul>
<hr>
<p>Let’s imagine that I want to get start on Web Components today.</p>
<ol>
<li>
<p>Go to <a href="https://webcomponents.org">WebComponents.org</a>. The homepage is basically just a random list of featured components that I could import. Okay neat, but I want to write an application, so let me continue on.</p>
</li>
<li>
<p>There’s no “Documentation” or “Docs” page. Where do I go?</p>
</li>
<li>
<p>Oh, I see an “Introduction” link. This must be the start of the docs</p>
</li>
<li>
<p>Uh-oh, this is just a link to specifications for Custom Elements, Shadow DOM, ES Modules, and HTML template elements.</p>
<p>I’ll be back in a couple weeks after I read all of these.</p>
</li>
<li>
<p>Oh wait, there still isn’t a recommended way to actually <em>build</em> and <em>application</em>, just individual components.</p>
<p>Guess I’ll just go build one myself.</p>
<p><em>Just kidding, I’m just not even going to try.</em></p>
</li>
</ol>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Things I want to see in JavaScript and Frontend development in 2023]]></title>
            <link>https://paularmstrong.dev/blog/2023/01/20/javascript-and-frontend-things-to-see-in-2023/</link>
            <guid>https://paularmstrong.dev/blog/2023/01/20/javascript-and-frontend-things-to-see-in-2023/</guid>
            <pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>We’re now nearly a month into the year 2023. I’ve started a new job and have been doing a lot of research for new projects and auditing old code. This process has given me time to reflect on what is and is not working – and what I’d like to see going forward this year.</p>
<p><a href="https://paularmstrong.dev/blog/2023/01/20/javascript-and-frontend-things-to-see-in-2023/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>We’re now nearly a month into the year 2023. I’ve started a new job and have been doing a lot of research for new projects and auditing old code. This process has given me time to reflect on what is and is not working – and what I’d like to see going forward this year.</p>
<ol>
<li>
<p><strong>Usage of Axios declines or goes EOL</strong></p>
<p>Axios was the original on promise-based XHRs for the web. However, <code>fetch</code> is now standard and well supported. It’s different enough from Axios
Savings: <a href="https://bundlephobia.com/package/axios@1.2.3">7-11kB</a>, depending on if you need the <a href="https://bundlephobia.com/package/whatwg-fetch@3.6.2">fetch polyfill</a>.</p>
<p>I originally posted this quick hot take <a href="https://mstdn.io/@paularmstrong/109716625048218845">about Axios on Mastodon</a> and was surprised that I wasn’t bombarded with hate. Maybe this is the year!</p>
</li>
<li>
<p><strong>Successful use of the <a href="https://nodejs.org/docs/latest-v18.x/api/test.html">Node.js Test Runner</a></strong></p>
<p>This is definitely a new one, but it ticks <em>almost</em> all of the boxes for testing, and as a standard library! The only downside is for those using TypeScript and/or needing any sort of runtime. Although using a node loader/register may be enough, as I tested this out and it seemed to work nicely:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#F97583">></span><span style="color:#E1E4E8"> node --loader esbuild-register/loader -r esbuild-register --test src/foo.test.ts</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">TAP</span><span style="color:#9ECBFF"> version</span><span style="color:#79B8FF"> 13</span></span>
<span class="line"><span style="color:#6A737D"># Subtest: project/src/foo.test.ts</span></span>
<span class="line"><span style="color:#B392F0">ok</span><span style="color:#79B8FF"> 1</span><span style="color:#9ECBFF"> -</span><span style="color:#9ECBFF"> project/src/foo.test.ts</span></span>
<span class="line"><span style="color:#B392F0">  ---</span></span>
<span class="line"><span style="color:#B392F0">  duration_ms:</span><span style="color:#79B8FF"> 274.373517</span></span>
<span class="line"><span style="color:#79B8FF">  ...</span></span>
<span class="line"><span style="color:#B392F0">1..1</span></span>
<span class="line"><span style="color:#6A737D"># tests 1</span></span>
<span class="line"><span style="color:#6A737D"># pass 1</span></span>
<span class="line"><span style="color:#6A737D"># fail 0</span></span>
<span class="line"><span style="color:#6A737D"># cancelled 0</span></span>
<span class="line"><span style="color:#6A737D"># skipped 0</span></span>
<span class="line"><span style="color:#6A737D"># todo 0</span></span>
<span class="line"><span style="color:#6A737D"># duration_ms 276.471144</span></span>
<span class="line"></span></code></pre>
<p>Now I guess we will just need some pretty-printing tools and reporters.</p>
</li>
<li>
<p><strong>Less divisive stances on CSS solutions</strong></p>
<p>I like Tailwindcss. It’s not perfect, but it works very well. I also like some other CSS frameworks and CSS-in-JS solutions. I also don’t like quite a good many of them as well, but you won’t see me throwing hate about them.</p>
</li>
<li>
<p><strong>Less venture capital backed open source</strong></p>
<p>We all love free open source software. But when VCs get involved, it means they expect a huge monetary return. These things just don’t go together.</p>
</li>
<li>
<p><strong>Decline of weak Eslint rules and configs</strong></p>
<p>I’m actually a huge fan of linters – but only those that either 1) rewrite for you or 2) give errors for actual problematic code and patterns.</p>
<p>Stop defaulting to report warnings and remove any rules that are stylistic choices in favor of Prettier<sup><a href="#user-content-fn-eslint" id="user-content-fnref-eslint" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
</li>
<li>
<p><strong>React either gets it’s crap together or gets out of the way</strong></p>
<p>Okay, this is my hot take. Please bear with me as I write this with the best intentions of hoping that React can succeed.</p>
<p>I’ve been on-board with React for the greater part of a decade. I’ve been a part of teams that have helped shaped the ecosystem, the patterns, and the anti-patterns. Now in 2023, React is the defacto tool for building most new websites. I will also still be using React and leading teams using it for the foreseeable future.</p>
<p>And the React Core Team are making a mess of it all.</p>
<p>The React Core Team initially moved <em>fast</em> and gave us amazing results one after another, but now they’re struggling to ship anything at all.</p>
<ul>
<li>
<p>Current documentation is out of date and missing many best practices. The <a href="https://github.com/reactjs/reactjs.org/issues/3308">beta docs were announced</a> more than two years ago, with a promise of fixing this issue, but have not been fully deployed as the main source of truth. As of writing this post, they are listed as ~99% complete.</p>
</li>
<li>
<p>Remember how long it took to get Suspense? Or Fibers?</p>
</li>
<li>
<p>They shipped <code>useCallback</code>, then kept telling us we shouldn’t be using it. Then took over three years to realize their mistake and year to correct their mistake and get the <a href="https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md"><code>useEvent</code></a> hook considered, but <a href="https://github.com/reactjs/rfcs/pull/220#issuecomment-1259938816">four more months</a> to give up and decide on another direction. That was four months ago from writing this post. The future is uncertain.</p>
</li>
<li>
<p>The virtual DOM is slow, and other projects<sup><a href="#user-content-fn-vdom" id="user-content-fnref-vdom" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> know this. React has made no intention to change course.</p>
</li>
<li>
<p>SSR with hooks? They just gave up and took years to try to figure out server components. Is that even ready, or is it only a Next.js thing? This author can’t even keep track anymore.</p>
</li>
</ul>
<p>🔥 So here’s my hot take: React is suffering from slow technical leadership, no clear vision, and inability to ship. But more importantly that it, like every JS framework has shown, will not stay on top forever. The ecosystem is too fragmented. Everyone claims they can do it better and writes their own framework instead of contributing to a single one, and now we’re all eyeing the hot new thing and ignoring the elephant in the room that we are still actually working with.</p>
<p>The latter part of my previous take is not React’s fault, but React could be the solution to it, if they got their act together on the issues they’re creating.</p>
</li>
</ol>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-eslint">
<p>(I’m looking at <em>you</em>, <a href="https://github.com/airbnb/javascript">“Airbnb JavaScript Style Guide”</a>) <a href="#user-content-fnref-eslint" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-vdom">
<p><a href="https://www.solidjs.com/">Solid-JS</a> and <a href="https://preactjs.com/blog/introducing-signals/">Preact</a> both work with Signals, a pattern to avoid running through entire virtual DOMs for HTML manipulation. <a href="#user-content-fnref-vdom" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Tips for healthy and useful meetings in tech]]></title>
            <link>https://paularmstrong.dev/blog/2022/12/19/healthy-meetings-in-tech/</link>
            <guid>https://paularmstrong.dev/blog/2022/12/19/healthy-meetings-in-tech/</guid>
            <pubDate>Mon, 19 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I’m a bit of a stickler about meetings. If I can, I like to instill a healthy meeting culture in teams I work with. Not all meetings are bad, but I have some rules that I like to follow to ensure that they don’t become that way. This is my list of tips for keeping meetings healthy and useful.</p>
<p><a href="https://paularmstrong.dev/blog/2022/12/19/healthy-meetings-in-tech/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>I got an email last week spouting nonsense about how my organization is going to require that all meetings start at 5-minutes after (e.g. 12:05 or 12:35) to help everyone be more on time. Listen, that’s just dumb. A change in what time meetings start does not change peoples’ behaviors and this is a smoke &#x26; mirrors non-solution to bigger issues with your meetings.</p>
<p>What does change meeting behavior is actually changing behavior. I’m a bit of a stickler about meetings. If I can, I like to instill a healthy meeting culture in teams I work with to make them understandable, helpful, and on-time. Not all meetings are bad, but I have some rules that I like to follow to ensure that they don’t become that way.</p>
<p>Keep in mind that I’m in no way an expert, but I’ve been in my fair share of meetings for nearly 20 years. This list of tidbits is from personal experience of what I’ve seen has worked and what hasn’t.</p>
<h2 id="general-meetings">General meetings</h2>
<ul>
<li>
<p><strong>Stay on schedule and end early.</strong></p>
<p>I get the intention of the “5-minute after” starts, but it’s just a silly time to start meetings. Instead of starting late, schedule “short meetings”, eg 25- or 50-minute meetings only.</p>
</li>
<li>
<p><span id="agenda"><strong>Include an agenda with your meeting invitation.</strong></span></p>
<p>Agendas must include a desired outcome of the meeting. If you don’t yet know what that is, you’re not ready to have a meeting. An agenda also helps invitees understand whether it is important to attend the meeting or not – they may also already have a resolution for you and can reach out with it, making the whole meeting unnecessary in the first place.</p>
</li>
<li>
<p><strong>Put a pin in off-topic discussions.</strong></p>
<p>You probably see it often – you’ve got six people in a room and two start going off on a tangentially related problem and start solving that, even though it is not related to your <a href="#agenda">agenda</a>.</p>
<p>Politely interrupt, recognize that importance of the discussion, but remind the group of the purpose of this meeting and ask if it can be revisited either after the primary agenda item has been resolved or moved to another discussion time.</p>
</li>
<li>
<p><span id="recap"><strong>Recap the resolution in an email.</strong></span></p>
<p>Full meeting notes tend to be a waste of effort, and prevent the note-taker from participating in discussion. However, the meeting organizer should recap the resolution and share it with attendees and those that were unable to make the meeting. This helps both for future reference and for fact checking to double check that everyone left the meeting with the same understanding.</p>
<p>Alternatively, any action items could be created as tasks or issues and prioritized on scrum/kanban boards.</p>
<p>A recap can also be an unnecessary step. Use your best judgement here.</p>
</li>
<li>
<p><span id="decline"><strong>It’s okay to decline meetings.</strong></span></p>
<p>Declining meetings is hard and scary – especially for those in a more junior position. I’m not advocating for just constantly declining meetings because you hate them, but rather, knowing when it is okay to decline a meeting and providing context to the organizer.</p>
<p>If you’re invited to a lot of meetings (with many attendees) and find the information is rarely useful to your position and duties, you probably don’t need to be there. Decline the meeting and ask that if your presence is necessary, that the organizer reach back out.</p>
<p>Or if you have something to work on that you know is more important and timely than the meeting, because you’ve seen the <a href="#agenda">agenda</a> ahead. Don’t say “maybe” – just decline, let the organizer know that you have other priorities right now, and suggest a better day and time.</p>
</li>
<li>
<p><strong>Meeting notes are rarely helpful.</strong></p>
<p>For general meetings, notes are not all that helpful. Instead, consider sending a short recap email with any resolutions (see <a href="#recap">previous point on recaps</a>)</p>
<p>On the other hand, meeting types that I have seen notes be helpful afterwards are those that have historical importance, like post-mortems.</p>
<p>If the content of a meeting would otherwise be helpful for those who are not able to participate, a recording is much more helpful, as nothing will be missed, visual aids will be intact, and all participant’s voices will be included.</p>
</li>
</ul>
<h2 id="1-on-1s">1-on-1s</h2>
<h3 id="from-the-senior-lead-or-manager-side">From the senior, lead, or manager side</h3>
<ul>
<li>
<p><strong>Always be on time and avoid rescheduling.</strong></p>
<p>1-on-1s are less importantant to the leader side of the meeting, since typically you will have multiple of these with various people on the team and be able to get a bird’s eye view from there, so it can be tempting to brush off a 1-on-1 with some individuals in favor of other meetings or tasks, but remember that your report likely only has one of these every week or two. This is <em>their</em> time to have your undivided attention; breaking the schedule interrupts their flow and could signal feelings of lacking importance in your eyes.</p>
</li>
<li>
<p><strong>Set expectations up front.</strong></p>
<p>You likely have a standing agenda of what you’re looking to get out of having 1-on-1s with your reports or team. You should be clear with each person what you’re looking for and also make it clear that this comes secondary to <em>their</em> needs and goals. During your first 1-on-1, find out what it is each person is looking to get out of their time with you.</p>
</li>
<li>
<p><strong>Meeting notes are helpful!</strong></p>
<p>For <a href="#group-meetings">other</a> <a href="#recurring-meetings">meetings</a>, I tend to shy away from requiring meeting notes. For 1-on-1s, however, they can be incredibly helpful. These notes should not be a long detailed list, but a bullet point or two about progress, what’s next, and what we’re thinking about long-term. That’s all. These notes can be helpful for both parties to see progression over time.</p>
<p>When I’m leading 1-on-1s, I scribble things into a notebook during the conversation (because typing is distracting) and then move them to a shared doc immediately after the meeting.</p>
</li>
<li>
<p><strong>Feedback is a gift.</strong></p>
<p>Each of these meetings should include an exchange of feedback and it is your responsibility to ensure this happens. Start by offering something that the other person is doing well. Explain how they’re impacting the others, the team, the product, etc and ensure they know it is appreciated and to keep it up. I like to finish the meetings with bringing up something that is not going well, but focusing on what can be done to turn that around into positive feedback next time.</p>
<p>But remember, your 1-on-1s are not a 1-way street! It may be hard to get unfiltered feedback from everyone, but continue to ask. Ensure others know that your most important work is helping them and the best way for you to know how to do that is to know what you’re doing well and what you can do differently or better.</p>
</li>
</ul>
<aside title="“Feedback is a gift”">
  I had a manager that loved to use this phrase (if you’re reading this, you know who you are). I used to hate this
  saying – and I’m sorry for rolling my eyes every time you said it. However, it really has stuck with me and I have a
  lot more appreciation for it now that I have more experience.
</aside>
<h3 id="from-the-more-junior-or-ic-side">From the more junior or <abbr title="Individual Contributor">IC</abbr>-side</h3>
<ul>
<li>
<p><strong>Plan ahead.</strong></p>
<p>The agenda for your 1-on-1 should be set by you. Every day that you have a 1-on-1 with a manager or team lead, spend 15 minutes (maybe when you just sit down at the beginning of the day) to jot down a couple of bullet points for an agenda.</p>
<p>If you’re meeting with a technical leader, this is a great chance to do a short code pairing session, get an in-person review, or have them walk through <em>their</em> work for you.</p>
</li>
<li>
<p><strong>Ask for feedback.</strong></p>
<p>You should never be leaving a 1-on-1 without some actionable feedback, both positive <em>and</em> negative. Praise is wonderfule and it is great to help understand what you’re doing that is going well, but walk out of this meeting by focusing on how to turn the negative feedback around. If it’s unclear what you can do to do better, <em>ask!</em></p>
</li>
<li>
<p><strong>The benefits of these meetings are up to you.</strong></p>
<p>1-on-1s are <em>your time</em> to have your manager/lead’s undivided attention. What do you want to get out of this time with them? Let them know regularly how things are going and what you feel like you’re missing out on or need to help step up your game.</p>
<p>Lastly, don’t <a href="#decline">decline</a> these meetings. Your manager/lead may have important timely feedback that is best given in person, with context.</p>
</li>
</ul>
<h2 id="parting-thoughts">Parting thoughts</h2>
<p>This post actually started out sounding like I was going to rant about how poor meeting culture can be in many organizations, but ended up spurring me to be a little more positive. I hope that something in here is useful for making meetings better and more useful.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Lessons learned: how I’d rebuild a social web app like Twitter today]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/28/lessons-learned-how-i-would-rebuild-twitter-today/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/28/lessons-learned-how-i-would-rebuild-twitter-today/</guid>
            <pubDate>Mon, 28 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I’ve been thinking back to my time as a core member of the original team that built the Twitter web app. I learned a great many lessons there and thinking through them: How would I rebuild something similar to the Twitter web app today, like a Mastodon or Activity Pub web client?</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/28/lessons-learned-how-i-would-rebuild-twitter-today/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import YouTube from ’../../components/YouTube.astro’;</p>
<p>I’ve been thinking back to my time as a core member of the original team that built the Twitter web app. I learned a great many lessons there and thinking through them: How would I rebuild something similar to the Twitter web app today, like a Mastodon or Activity Pub web client?</p>
<h2 id="no-react-native-for-web">No React Native for Web</h2>
<p>We used <a href="https://necolas.github.io/react-native-web/">React Native for Web</a> (<acronym title="React Native for Web">RNW</acronym>) and I have been bullish on its benefits for years. It served me – and us — very well. In fact, its creator was our team’s tech lead for the first few years until he left to join the React core team. While we got a lot out of <acronym title="React Native for Web">RNW</acronym>, we didn’t get any benefit out of the core selling point: cross-platform components. The native apps are highly optimized, do not, and will not use React Native. Without going into too much detail, I’ll just say it is not a battle to even try to fight.</p>
<p>Just as well, <em>web</em> developers tended to have a lot of issues with <acronym title="React Native for Web">RNW</acronym>. Not directly, just that it wasn’t very ergonomic and took too much time to understand. While <acronym title="React Native for Web">RNW</acronym> (and React Native, for that matter) are <a href="https://github.com/facebook/react-native/issues/34425">moving to better direct web compatibility</a> by accepting more web-native styles and props like <code>aria-*=</code> (instead of <code>accessiblity*=</code>), it’s still a bit too much of a shift and frankly the library is falling behind.</p>
<p>For the most part now, all <acronym title="React Native for Web">RNW</acronym> is getting the team now is default styling, automatic <acronym title="left to right">LTR</acronym>/<acronym title="right to left">RTL</acronym> text direction swapping, and some performance help under the hood. None of which aren’t easily handled otherwise.</p>
<p>And while <acronym title="React Native for Web">RNW</acronym> is not a hit on performance directly, pseudo-selectors and interactivity must be handled through React hooks, even just for simple style changes on hover or focus. This <em>does</em> actually translate to an interactivity performance issue.</p>
<youtube code="PYXMMYvNn8k">
<h2 id="solid-js-or-another-non-virtual-dom-framework">Solid-js or another non-virtual DOM framework</h2>
<p>Performance of Twitter web was absolutely critical when I was working on it. In fact, performance was so critical that we called with “Twitter Lite” (before the Android app was released) because its target market was those that needed to load and use Twitter as fast as possible.</p>
<p>While Twitter and similar Fediverse applications are highly interactive, modifying the DOM mostly happens for virtual scrolling and navigation between URLs. There are a lot of little visual fluff additions, like the way favoriting and retweet counts update in semi-real time. One of the difficult problems we had with those was ensuring only the portion of the UI that needed to update was updating.</p>
<p>With <a href="https://www.solidjs.com/">Solid-js</a>, we can remove the virtual DOM and rendering tree to focus solely on small portions of data that update and re-render only what matters without large cascading effects.</p>
<p><a href="https://preactjs.com/blog/introducing-signals/">Preact recently got signals</a>, similar to Solid, which makes it an interesting alternative middle-ground candidate between React and Solid. If it turned out that the team did not feel comfortable enough to push the envelope that far, Preact may be a suitable choice as well!</p>
<p>Just as well, <a href="https://beta.nextjs.org/docs/rendering/server-and-client-components">server components</a> look like a start to changing React into a similar direction, but we’ve been waiting <a href="https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html">two years</a> (or more) for them and they’re still not quite ready. And on top of all of this, I’m still proposing to move away from React (and I’m not going to get into why we are not jumping on the Next.js bandwagon – that may be a conversation for another day).</p>
<h2 id="use-tailwind-css-not-css-in-js">Use Tailwind CSS, not CSS-in-JS</h2>
<p>Without <a href="#no-react-native-for-web">React Native for Web</a>, a CSS/styling solution would be necessary. After trying many of the options available, I feel confident in proposing <a href="https://tailwindcss.com">Tailwind CSS</a> for almost all new projects.</p>
<p>We would be able to redefine and restrict the color, spacing, and sizing tokens down to exactly what the design team has defined. It is also possible to do most of the heavy-lifting on theming from within the Tailwind configuration.</p>
<p>Most CSS-in-JS solutions do not create separate CSS assets that are cached in the CDN. All of the styles are delivered within the JavaScript bundles, causing extra network, evaluation, and runtime contention within the application. For our performance-first approach, it is imperative that we make gains here.</p>
<aside type="info" title="Not convinced?">
<p>If you’re still not convinced on Tailwind, I suggest reading <a href="https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b">Why we’re breaking up with CSS-in-JS</a> by Sam Magura, the second most active maintainer of the Emotion CSS-in-JS library.</p>
</aside>
<p><strong>Bonus:</strong> I would start using <a href="/blog/2022/11/21/client-hint-headers-for-dark-mode-theme/">client hints</a> to determine what theme to serve to users on page load to avoid the current flash of super bright loading screen.</p>
<h2 id="use-vite-or-turbopack">Use Vite or TurboPack</h2>
<p>Webpack is showing its age. <a href="https://vitejs.dev/">Vite</a> has shown it is a fast and solid alternative; <a href="https://turbo.build/pack">TurboPack</a> claims to be the successor of Webpack. Given that TurboPack isn’t widely available or tested for non-Next.js applications, I would likely end up choosing Vite, but still look for a path to swap for TurboPack and test as soon as possible.</p>
<p>Webpack configurations at scale become unruly beasts. There were at most a handful of engineers at Twitter (more likely one or two) that were confident in making changes to the configuration. Vite looks like it could simplify that a bit, but it would still be top of mind for me to ensure that the configurations were easy to follow and well-documented.</p>
<h2 id="split-into-multiple-applications-with-module-federation">Split into multiple applications with module federation</h2>
<p>Twitter is a unique application – in that it could actually be a number of separate applications that have core shared modules. One thing that we struggled with was moving fast on really small parts of the application, eg, “Settings”. Because everything in the application was interconnected and controlled by a single Node.js process, this made reviews, CI, and deploys slow. We could have, however, split things into multiple applications. For example:</p>
<ul>
<li>Post permalinks</li>
<li>Home timeline</li>
<li>Profiles</li>
<li>Direct messages</li>
<li>Settings</li>
<li>New user onboarding</li>
</ul>
<p>These applications then need a number of core shared modules, like the layout, user information, Tweet rendering components, etc.</p>
<p>All of this could be handled with <a href="https://blog.logrocket.com/building-micro-frontends-webpacks-module-federation/">Module Federation</a> in either <a href="https://webpack.js.org/concepts/module-federation/">Webpack</a> or <a href="https://github.com/originjs/vite-plugin-federation">Vite</a>. Module Federation is a newer concept that could handle exactly what I would want: separate applications that reference each others’ shared code to avoid duplication, reduce footprints, allow faster builds and deploys, and much more.</p>
<h3 id="user-impact">User impact</h3>
<p>Module federation presumably would have a net benefit to end users as well. Bundling and separation of code becomes a bit more logical, making shared code more highly available between logged-in vs logged-out users, while also preventing code getting dropped into a shared bundle where it won’t be used. Essentially: we would deliver only the JavaScript necessary – reducing payloads, byte transfer, and core web vitals.</p>
<h2 id="deno-vs-nodejs">Deno vs Node.js</h2>
<p>I would look at using <a href="https://deno.land">Deno</a> instead of <a href="https://nodejs.org">Node.js</a> with a big up-front evaluation. There are a lot of unknowns in running a new service with a new runtime, so it would be really important to feel confident using one vs another.</p>
<p>Node.js is great, tried, and tested and performant enough for even a globally scaled service, but the ergonomics, patterns, and core APIs within Deno make Node.js a bit of a nuisance to maintain. Things have gotten better for sure, but why not re-evaluate if we’ve got the chance?</p>
<h3 id="http-server-framework">HTTP server framework</h3>
<p>This may come as a surprise, but not a lot of performance gains would come through a slightly faster web server – most of the processing time comes from API requests, looking up users, posts, etc. This one would probably come down to personal/team preference to choose between <a href="https://expressjs.com">Express</a> and <a href="https://www.fastify.io/">Fastify</a>.</p>
<p>However, if we end up in Deno land, we’d likely end up using <a href="https://oakserver.github.io/oak/">Oak</a>. It looks a lot like Express and Fastify, but native for Deno.</p>
<h2 id="parting-thoughts">Parting thoughts</h2>
<p>While I strongly believe the solutions outlined here would work well, they’re all theoretical and/or opinion-based. If this were actually a project that I would be working on, I would first ensure proof-of-concepts were made for everything and get buy-in from both direct and supporting teams in my organization. Once all evaluations have been made, then we’d move on to <a href="/blog/2022/10/14/tech-design-template/">writing a technical design</a> before proceeding with building anything more.</p></youtube>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[My biggest technical mistakes (so far)]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/22/my-biggest-technical-mistakes/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/22/my-biggest-technical-mistakes/</guid>
            <pubDate>Tue, 22 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>We all make mistakes in the technical planning process that bubble through to execution. Looking back over the past 10 years or so, these are the two biggest tech &#x26; development mistakes that I think I have made and the lessons that I’ve learned from them.</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/22/my-biggest-technical-mistakes/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>We all make mistakes in the technical planning process that bubble through to execution. Looking back over the past 10 years or so, these are the two biggest tech &#x26; development mistakes that I think I have made and the lessons that I’ve learned from them.</p>
<h2 id="choosing-flow-over-typescript">Choosing Flow over TypeScript</h2>
<p>Back in about 2016, typing JavaScript was still coming into the scene. At Twitter, we were evaluating both <a href="https://www.typescriptlang.org/">TypeScript</a> and <a href="https://flow.org/">Flow</a>. We wanted to add type-safety because it was too easy to accidentally miss required props across components that ensured ads were attributed as such. Without that information, Twitter wouldn’t get paid. The fact is, it happened and we lost out on a lot of money across a few days.</p>
<p>We ended up choosing Flow because:</p>
<ul>
<li>We had many Babel plugins we needed to use and Babel was not TypeScript-compatible. Switching to TypeScript would mean using <code>tsc</code> and <code>ts-loader</code> with Webpack and we would have to rewrite our entire compilation setup and change any Babel plugins into Webpack plugins. This would have been a huge undertaking.</li>
<li>React was written using Flow and thus was poised to be a better support model.</li>
<li>TypeScript either did not have a strict-enough mode in it. There were too many examples at the time that would just pass through, despite best intentions by developers to produce strictness.</li>
<li>TypeScript was slow across large codebases.
<ul>
<li>Flow as <em>fast</em>. We were able to get checking across an entire monorepo with about 10,000 Flow-typed files to run in about 20 seconds.</li>
</ul>
</li>
</ul>
<p>Flow ended up being a mistake because:</p>
<ul>
<li>Adoption lagged. While we jumped on Flow, everyone else started jumping to TypeScript.</li>
<li>The core team at Facebook changed directions multiple times and eventually stated publicly that they didn’t really intend to continue interfacing with the open source community, but would continue publishing releases.</li>
</ul>
<p>Unfortunately, before we realized that TypeScript was taking over and would have been a better choice, we were somewhere like 75-85% Flow coverage. Interop between Flow and TS is not possible and so another slow migration over to TS would not be possible.</p>
<h3 id="lesson-for-type-safety">Lesson for type safety</h3>
<p>I’m not sure that at the time we could have seen it coming that Flow would essentially be dead for anyone external to Facebook/Meta. Next time something like this comes up, I will try to do more due-diligence in checking with the open source teams in understanding their roadmap as well as watch the community’s traction more closely. I would also try to make sure we have an ejection plan if things go sour with the choice we make.</p>
<h2 id="using-a-multi-language-monorepo">Using a multi-language monorepo</h2>
<p>This has actually happened multiple times. The first was at Twitter, which historically had a single “source” repo. <em>Everything</em> was in this repo: from Scala, Python, Ruby, some PHP, and yes, JavaScript. While there was a team dedicated to ensuring that the CI pipelines and tooling were all fast, JavaScript teams were small and spread, without much agreement on stacks. Because of that, the DX team prioritized Scala over anything else. While things “worked” for JavaScript projects, everything was unacceptably slow or poorly linked (eg, 30-60 seconds to switch git branches locally and 30+ minute pull-request CI runs).</p>
<p>After many months of planning, campaigning, and meetings with the DX team, I was able to get my team to build our own monorepo that was for meant for JavaScript only. The results were staggeringly <em>awesome</em>. We were able to cut the amount of time developers in our codebases wasted just waiting around for slow process by about 2 hours per developer per week. Across about 75 contributors weekly, this was a very material gain. On top of that, I received quite a number of kind words of appreciation and happiness for this work 💙.</p>
<p>The second time I made the mistake of using a multi-language repository was at Microsoft for Startups. While our tech stack was small across just Python and Node/JavaScript, it was still really troublesome to feel confident and safe with changes across the different languages. Because of the weak connection between the two stacks, changes needed to be tested carefully and it was always confusing what command you ran to check each individual piece.</p>
<h3 id="lesson-for-monorepos">Lesson for monorepos</h3>
<p>My lesson for Monorepos is that they <em>are</em> actually good, but they only scale as long as you’re using a single language and every workspace internally has some connection to the same graph. As soon as you have a workspace or project that doesn’t use any of the same stack, tools, or language, a new repo is likely the best course forward.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Using client hints to prevent FARTs with website auto dark mode themes]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/21/client-hint-headers-for-dark-mode-theme/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/21/client-hint-headers-for-dark-mode-theme/</guid>
            <pubDate>Mon, 21 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I originally wanted to avoid using any sort of server-side code for my site, getting around the FART completely is not currently possible when you have a statically generated site. In this post, I’ll walk through how I got rid of the flash of inaccurate color themes for Tailwindcss dark mode (with auto media query support!), building off of some others’ examples using Netlify Edge Functions.</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/21/client-hint-headers-for-dark-mode-theme/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import { ThemeSwitcher } from ’../../components/ThemeSwitcher.tsx’;</p>
<p>Flash of inAccurate coloR Themes<sup><a href="#user-content-fn-acronym" id="user-content-fnref-acronym" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>, or <acronym title="Flash of inAccurate coloR Themes">FART</acronym>, as <a href="https://css-tricks.com/flash-of-inaccurate-color-theme-fart/">coined by Chris Coyier</a> is the <em>fancy</em> acronym visit a website and it quickly flashes from one color theme to another. This is most prominent for those that prefer dark mode and see pages render in light mode for a split second, then flipping over to the dark variant. It’s minor, yes, but also super annoying to anyone with a slightly slower connection, as the time at which the theme will switch to dark is dependent on the speed that the JavaScript can download and execute.</p>
<p>I came across <a href="https://www.learnwithjason.dev/blog/css-color-theme-switcher-no-flash">a post by Jason Lengstorf</a> about avoiding the <acronym title="Flash of inAccurate coloR Themes">FART</acronym> by using Netlify’s new <a href="https://docs.netlify.com/edge-functions/overview/">Edge Functions</a>. Lucky me, I’m using Netlify and a <a href="/blog/2022/11/09/the-case-for-astro/">statically-built site</a>, so I figured this would be a great approach for me.</p>
<p><em><strong>Except there was a big issue.</strong></em></p>
<p>Many theme pickers, including the example I was following, allow picking one theme and then sticking to it. While it’s kind to provide a choice, most theme pickers miss out on providing an option for their own system to choose for them. This is handled by the CSS media query <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"><code>prefers-color-scheme</code></a>.</p>
<p>Tailwindcss, which I’m using, allows a one-or-the-other approach as well. By default it uses the <code>prefers-color-scheme</code> approach. Alternatively, you can change it to <code>darkMode: 'class'</code> to control it yourself.</p>
<p>I originally wanted to avoid using any sort of server-side code for my site, but getting around the <acronym title="Flash of inAccurate coloR Themes">FART</acronym> is not possible without <em>some</em> level of server-side processing.</p>
<p>In this post, I’ll walk through how I got rid of the flash of inaccurate color themes for Tailwindcss dark mode, building upon <a href="https://www.learnwithjason.dev/blog/css-color-theme-switcher-no-flash">Jason’s work</a> to add “auto” theme support using <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints">HTTP client hints</a> and media queries.</p>
<h2 id="theme-switcher-component">Theme switcher component</h2>
<p>I’m going to skip over most of my <a href="https://github.com/paularmstrong/paularmstrong.dev/blob/e29bd3d602bcd625f9e29360b422c7e1dc0cf86e/src/components/ThemeSwitcher.tsx">theme switch toggle button code</a>, as mine is in Solid-js and may look a little funky if you’re used to React.</p>
<p>However, here are the important bits of the theme switcher, translated into React:</p>
<p>First, in order to manually control the <code>light</code> and <code>dark</code> mode themes with Tailwindcss, we need toset <code>darkMode: 'class'</code> in our configuration:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#79B8FF">module</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">exports</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  content: [</span><span style="color:#9ECBFF">'src/**/*.{astro,md,mdx,tsx,ts}'</span><span style="color:#E1E4E8">],</span></span>
<span class="line"><span style="color:#E1E4E8">  darkMode: </span><span style="color:#9ECBFF">'class'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<p>To save our theme and whether or not auto-selection is preferred, we can use a basic <code>fetch</code> call with <code>URLSearchParams</code>. Nothing too special is necessary here:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> saveTheme</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">theme</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Theme</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">auto</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> boolean</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URLSearchParams</span><span style="color:#E1E4E8">({ theme, auto: auto </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'false'</span><span style="color:#E1E4E8"> });</span></span>
<span class="line"><span style="color:#B392F0">  fetch</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`/api/theme/?${</span><span style="color:#E1E4E8">params</span><span style="color:#9ECBFF">.</span><span style="color:#B392F0">toString</span><span style="color:#9ECBFF">()</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Then, in our <code>&#x3C;ThemeSwitcher /></code>, we need to create an effect that runs any time our <code>theme</code> or <code>auto</code> modes have changed and call our API. Note that I’m also tracking the source of truth for “auto” mode with a data attribute attached to the <code>&#x3C;html></code> element.</p>
<p>Here’s an example effect in React. Take special note of the highlighted lines regarding the “auto” mode:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> ThemeSwitcher</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">theme</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setTheme</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> React.</span><span style="color:#B392F0">useState</span><span style="color:#E1E4E8">(document.documentElement.classList.</span><span style="color:#B392F0">contains</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'dark'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'dark'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">auto</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setAuto</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> React.</span><span style="color:#B392F0">useState</span><span style="color:#E1E4E8">(document.documentElement.dataset.autoTheme </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">  React.</span><span style="color:#B392F0">useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">    document.documentElement.dataset.autoTheme </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> auto </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'false'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (theme </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'dark'</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8">      document.documentElement.classList.</span><span style="color:#B392F0">add</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'dark'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">    } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">      document.documentElement.classList.</span><span style="color:#B392F0">remove</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'dark'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">    save</span><span style="color:#E1E4E8">(theme, auto);</span></span>
<span class="line"><span style="color:#E1E4E8">  }, [theme, auto]);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // ...</span></span>
<span class="line"><span style="color:#6A737D">  // there’s more needed here, this is just for demonstration purposes</span></span>
<span class="line"><span style="color:#6A737D">  // ...</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>This results in our first two steps:</p>
<ol>
<li>The theme is tracked on the <code>&#x3C;html></code> element, using the <code>className</code> and <code>data-</code> attribute: <code>&#x3C;html class="dark" data-auto-theme="true"></code></li>
<li>Our API (which we’ll cover later) is called every time the user setting changes manually.</li>
</ol>
<h3 id="prefers-color-scheme-media-query">Prefers color scheme media query</h3>
<p>Now that the basic plumbing is set up and we have manual tracking, we need to make sure that when theme switching is set to “auto” mode that we start respecting the <code>prefers-color-scheme</code> media query using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia">matchMedia API</a>.</p>
<p>Follow the comments inline for any needed explanations.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#E1E4E8">React.</span><span style="color:#B392F0">useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">auto) {</span></span>
<span class="line"><span style="color:#6A737D">    // do nothing if not in auto mode.</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // On change to auto-mode, set the theme based on the current media query preference</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> isDark</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> window.matchMedia </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> window.</span><span style="color:#B392F0">matchMedia</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'(prefers-color-scheme: dark)'</span><span style="color:#E1E4E8">).matches;</span></span>
<span class="line"><span style="color:#B392F0">  setTheme</span><span style="color:#E1E4E8">(isDark </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'dark'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Add an event listener any time the device requests a change to the color scheme</span></span>
<span class="line"><span style="color:#F97583">  function</span><span style="color:#B392F0"> listener</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">event</span><span style="color:#F97583">:</span><span style="color:#B392F0"> MediaQueryListEvent</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#B392F0">    setTheme</span><span style="color:#E1E4E8">(event.matches </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'dark'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">  window.</span><span style="color:#B392F0">matchMedia</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'(prefers-color-scheme: dark)'</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'change'</span><span style="color:#E1E4E8">, listener);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Return a cleanup function when unloading so we stop listening to the media change</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">    window.</span><span style="color:#B392F0">matchMedia</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'(prefers-color-scheme: dark)'</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">removeEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'change'</span><span style="color:#E1E4E8">, listener);</span></span>
<span class="line"><span style="color:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#E1E4E8">}, [auto]);</span></span>
<span class="line"></span></code></pre>
<p>That should be it for our theme switcher. Your implementation is really up to you; whether you’re using a dropdown, button, or some other UI is up to you. I’m going to gloss over all of that in favor of getting into the trickier parts of this article. If you’d like an example, you can always check out my <a href="https://github.com/paularmstrong/paularmstrong.dev/blob/8683b83f5fcebaa223b658bc1881d2db0be14524/src/components/ThemeSwitcher.tsx">Solid-js implementation: <code>ThemeSwitcher.tsx</code></a>.</p>
<hr>
<h2 id="edge-functions">Edge functions</h2>
<p>As mentioned previously, we’re going to use <a href="https://docs.netlify.com/edge-functions/overview/">Netlify’s Edge Functions</a> to handle the server-side portion of our requests.</p>
<h3 id="api-endpoint">API endpoint</h3>
<p>First, let’s write our little cookie-setting API as a Netlify Edge Function. To start accepting calls to a URL, edit the Netlify config and add an <code>edge_functions</code> declaration. In my case, I put the pathname as <code>/api/theme/</code> and I’ll place the code in <code>netlify/edge-functions/set-theme.ts</code>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="toml"><code><span class="line"><span style="color:#E1E4E8">[[</span><span style="color:#B392F0">edge_functions</span><span style="color:#E1E4E8">]]</span></span>
<span class="line"><span style="color:#E1E4E8">	path = </span><span style="color:#9ECBFF">"/api/theme/*"</span></span>
<span class="line"><span style="color:#E1E4E8">	function = </span><span style="color:#9ECBFF">"set-theme"</span></span>
<span class="line"></span></code></pre>
<p>Now that we have declared our endpoint, let’s actually write it to save the user’s theme choice as a server-side secure cookie:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { Context } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://edge.netlify.com/'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">context</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Context</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> url</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URL</span><span style="color:#E1E4E8">(req.url);</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> url.searchParams;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Set a server-side secure cookie with the requested URL search params</span></span>
<span class="line"><span style="color:#E1E4E8">  context.cookies.</span><span style="color:#B392F0">set</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">    name: </span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    value: params.</span><span style="color:#B392F0">toString</span><span style="color:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#E1E4E8">    path: </span><span style="color:#9ECBFF">'/'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    secure: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    httpOnly: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    sameSite: </span><span style="color:#9ECBFF">'Strict'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6A737D">    // Set a *REALLY* big expires value</span></span>
<span class="line"><span style="color:#E1E4E8">    expires: </span><span style="color:#F97583">new</span><span style="color:#B392F0"> Date</span><span style="color:#E1E4E8">(Date.</span><span style="color:#B392F0">now</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 2_592_000_000</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Respond with a success JSON payload</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({ error: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8"> }), {</span></span>
<span class="line"><span style="color:#E1E4E8">    status: </span><span style="color:#79B8FF">200</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    headers: {</span></span>
<span class="line"><span style="color:#9ECBFF">      'content-type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<aside title="These use Deno!" type="tip">
<p>One particular big caveat: Netlify edge functions use <a href="https://deno.land/">Deno</a>, not Node.js! So when you see an <code>import</code> statement that pulls from a URL, just know that this is <em>not</em> a mistake.</p>
</aside>
<p>Well, astute reader… did you notice that I didn’t add any error handling here? Technically, someone could try to set any theme they want, even if it were invalid.</p>
<p>Let’s make sure we handle that both if a theme setting is not sent and if the theme is invalid:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { Context } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://edge.netlify.com/'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> themes</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> [</span><span style="color:#9ECBFF">'light'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'dark'</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">as</span><span style="color:#F97583"> const</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">context</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Context</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> url</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URL</span><span style="color:#E1E4E8">(req.url);</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> url.searchParams;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Didn't include a default or fallback theme</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">params.</span><span style="color:#B392F0">has</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({ error: </span><span style="color:#9ECBFF">'Missing theme parameter'</span><span style="color:#E1E4E8"> }), {</span></span>
<span class="line"><span style="color:#E1E4E8">      status: </span><span style="color:#79B8FF">400</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      headers: {</span></span>
<span class="line"><span style="color:#9ECBFF">        'content-type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> theme</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">as</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">typeof</span><span style="color:#E1E4E8"> themes)[</span><span style="color:#79B8FF">number</span><span style="color:#E1E4E8">];</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> auto</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'auto'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Sent a theme that's not valid for our site?</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">themes.</span><span style="color:#B392F0">includes</span><span style="color:#E1E4E8">(theme)) {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({ error: </span><span style="color:#9ECBFF">`Invalid theme: ${</span><span style="color:#E1E4E8">theme</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8"> }), {</span></span>
<span class="line"><span style="color:#E1E4E8">      status: </span><span style="color:#79B8FF">400</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      headers: {</span></span>
<span class="line"><span style="color:#9ECBFF">        'content-type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">  context.cookies.</span><span style="color:#B392F0">set</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">    name: </span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    value: params.</span><span style="color:#B392F0">toString</span><span style="color:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#E1E4E8">    path: </span><span style="color:#9ECBFF">'/'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    secure: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    httpOnly: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    sameSite: </span><span style="color:#9ECBFF">'Strict'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    expires: </span><span style="color:#F97583">new</span><span style="color:#B392F0"> Date</span><span style="color:#E1E4E8">(Date.</span><span style="color:#B392F0">now</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 2_592_000_000</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({ error: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8"> }), {</span></span>
<span class="line"><span style="color:#E1E4E8">    status: </span><span style="color:#79B8FF">200</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    headers: {</span></span>
<span class="line"><span style="color:#9ECBFF">      'content-type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<h3 id="rewriting-static-html-responses">Rewriting static HTML responses</h3>
<p>Next, again, we’ll take a hint from our <a href="https://www.learnwithjason.dev/blog/css-color-theme-switcher-no-flash">inspiration article</a> and rewrite part of our HTML responses for the static-generated multi-page site using Edge Functions. However, this is where we start deviating a lot more:</p>
<h4 id="accepting-client-hint-headers">Accepting Client Hint headers</h4>
<p>Since we want to be able to allow users to select an “auto” select for the “light” or “dark” mode, we need a way for the browser to tell us ahead of time what it actually wants on page load. In comes a new standard, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints">Client Hint Headers</a>.</p>
<p>Client hints are a newer set of HTTP request headers that enable a server to request more information from a browser about the device, network, user, and user-agent specific preferences. In particular, we’re interested in the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints#user_preference_media_features_client_hints"><code>prefers-color-scheme</code> client hint</a>, which will instruct browsers to send back to our server the same data that would normally only be available in CSS media queries.</p>
<p>To get started accepting Client Headers, we need our HTTP responses to add an <code>accept-ch</code> header with each hint that we’re asking for. Since we only need one, this is fairly short to add to the Netlify <code>_headers</code> file:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>/*</span></span>
<span class="line"><span>	accept-ch: sec-ch-prefers-color-scheme</span></span>
<span class="line"><span></span></span></code></pre>
<aside type="danger" title="Client hints caveats">
<p>It is important to note a really huge asterisk here, that I will likely have written <a href="#downsides">multiple times</a> once this is all done<sup><a href="#user-content-fn-clienthints" id="user-content-fnref-clienthints" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>: client hints only work in <a href="https://caniuse.com/mdn-http_headers_sec-ch-prefers-color-scheme">some browsers</a> and require at least one full network round-trip before the browser will start sending them to the server.</p>
</aside>
<p>Now that we’re requesting the client hints, we need to actually do something with them. First, let’s start with a barebones edge function that merely:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { Context } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://edge.netlify.com/'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { HTMLRewriter, Element } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://ghuc.cc/worker-tools/html-rewriter/index.ts'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">context</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Context</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D">  // Get the default response for this URL. This will grab the actual response as if we were not running this function</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> res</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> context.</span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">  // Check the content type. If we're not serving HTML, running the rest of this function will be a waste of compute time and power</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> type</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> res.headers.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'content-type'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">type?.</span><span style="color:#B392F0">startsWith</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'text/html'</span><span style="color:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> res;</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<p>Next, we need to try to read the cookie and fall back on a default value if it’s not available. Using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing">nullish-coalescing</a>, we can make this fairly simple:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> rawCookie</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> context.cookies.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">COOKIE_NAME</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">??</span><span style="color:#9ECBFF"> 'auto=true&#x26;theme=light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URLSearchParams</span><span style="color:#E1E4E8">(rawCookie);</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> isAuto</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'auto'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> theme</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<p>But the whole point of the <code>auto</code> setting on our theme switcher is that we want to read from the <code>prefers-color-scheme</code> client hint. So we’ll modify this block just a smidge to read the header and if the cookie states that auto-mode is preferred, we’ll use the header value if it’s available!</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> prefers</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> req.headers.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'sec-ch-prefers-color-scheme'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> rawCookie</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> context.cookies.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">COOKIE_NAME</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">??</span><span style="color:#9ECBFF"> 'auto=true&#x26;theme=light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URLSearchParams</span><span style="color:#E1E4E8">(rawCookie);</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> isAuto</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'auto'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> theme</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> isAuto </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> prefers </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> prefers </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<p>Lastly, just like in <a href="https://www.learnwithjason.dev/blog/css-color-theme-switcher-no-flash#update-the-currently-selected-theme-in-the-dropdown">the inspiration from “Learn with Jason”</a>, we’ll use the HTMLRewriter and update the <code>className</code> and <code>data-auto-theme</code> attributes on our <code>&#x3C;html></code> element. All-together, our function looks like this:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { Context } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://edge.netlify.com/'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { HTMLRewriter, Element } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'https://ghuc.cc/worker-tools/html-rewriter/index.ts'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">context</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Context</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> res</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> context.</span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> type</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> res.headers.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'content-type'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">type?.</span><span style="color:#B392F0">startsWith</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'text/html'</span><span style="color:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> prefers</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> req.headers.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'sec-ch-prefers-color-scheme'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> rawCookie</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> context.cookies.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">??</span><span style="color:#9ECBFF"> 'auto=true&#x26;theme=light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> params</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> URLSearchParams</span><span style="color:#E1E4E8">(rawCookie);</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> isAuto</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'auto'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> theme</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> isAuto </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> prefers </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> prefers </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> params.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'theme'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> 'light'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> HTMLRewriter</span><span style="color:#E1E4E8">()</span></span>
<span class="line"><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'html'</span><span style="color:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#B392F0">      element</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">element</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Element</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">        const</span><span style="color:#79B8FF"> original</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> element.</span><span style="color:#B392F0">getAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'class'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">||</span><span style="color:#79B8FF"> false</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">        element.</span><span style="color:#B392F0">setAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'class'</span><span style="color:#E1E4E8">, [original, theme].</span><span style="color:#B392F0">filter</span><span style="color:#E1E4E8">(Boolean).</span><span style="color:#B392F0">join</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">));</span></span>
<span class="line"><span style="color:#E1E4E8">        element.</span><span style="color:#B392F0">setAttribute</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data-auto-theme'</span><span style="color:#E1E4E8">, isAuto </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> 'true'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> 'false'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#E1E4E8">    })</span></span>
<span class="line"><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">transform</span><span style="color:#E1E4E8">(res);</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<p>Now we actually need to enable the edge function. In our <code>netlify.toml</code>, like we did before for the <code>set-theme</code> function<sup><a href="#user-content-fn-htmlrewrite" id="user-content-fnref-htmlrewrite" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="toml"><code><span class="line"><span style="color:#E1E4E8">[[</span><span style="color:#B392F0">edge_functions</span><span style="color:#E1E4E8">]]</span></span>
<span class="line"><span style="color:#E1E4E8">  path = </span><span style="color:#9ECBFF">"/*"</span></span>
<span class="line"><span style="color:#E1E4E8">  function = </span><span style="color:#9ECBFF">"theme"</span></span>
<span class="line"></span></code></pre>
<p>And that’s it! You can try it out on the page that you’re currently reading! Just click the theme switcher button in the site header to something different, reload or browse around the site and be amazed as there is no more <acronym title="Flash of inAccurate coloR Themes">FART</acronym> on a statically generated HTML site!</p>
<h2 id="downsides">Downsides</h2>
<p>First and foremost: Client Hints are still considered “experimental” and only available in Chromium-based browsers. For anyone not using one of those, the auto-theme selection won’t work and the <a href="#edge-functions-as-an-api">theme setting edge function</a> will fall back on the last known <code>theme</code> value per the cookie. That fallback should be mostly okay, but still a bummer.</p>
<p>Secondly, Client Hints are not provided by the browser to the server on <em>first</em> load. While there are ongoing discussions about allowing this to happen, there are technical challenges in making browsers first check which hints are being requested in order to send them. All this means that the even if you’re using a browser that supports Client Hints, your first page load to the site will always have a <acronym title="Flash of inAccurate coloR Themes">FART</acronym>.</p>
<p>We <em>could</em> work around that by forcing a redirect/reload on first load, but that would require quite a bit of extra work that is frankly not worth the effort.</p>
<hr>
<aside type="info" title="Alternative post titles">
<p>I had some other stupid post titles in mind, but eventually settled on the safe, SEO-friendly one. But thought I’d share some here anyway:</p>
<ul>
<li>It’s like Gas-X, but for websites!</li>
<li>Warn me with a <em>hint</em> before you <acronym title="Flash of inAccurate coloR Themes">FART</acronym></li>
<li>Client hint headers: like potpourri for your website</li>
</ul>
</aside>
<h2 id="updates">Updates</h2>
<p>I’ve made some changes to how things are done since originally posting this. Hopefully I’ll remember to keep things up to date:</p>
<ul>
<li><time class="font-bold">2022-12-04</time>: Switched the theme tracking from the <code>body</code> element to the
<code>documentElement</code>/<code>html</code> element. This fixes an issue in some mobile browsers where they showed a white background at
the bottom of a full-page scroll by moving the background color classNames up from a wrapping <code>div</code> element and onto
the <code>body</code> element – then moving the <code>dark</code> className up to the <code>html</code> element.</li>
</ul>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-acronym">
<p>I tried to avoid using this silly acronym in the title, but it is kinda fun. Sorry if you’re too mature for 💨 jokes, because I’m not. <a href="#user-content-fnref-acronym" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-clienthints">
<p>Client hints are currently only supported by Chromium-based browsers. This is a huge shame, but hopefully more pressure will be put on other browsers to get traction soon. See the section on <a href="#downsides">downsides</a> for more information. <a href="#user-content-fnref-clienthints" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-htmlrewrite">
<p>Even though we’re ejecting early from our HTML rewriting edge function if the response does not have the <code>text/html</code> content type, there is still a limited number of edge function runs on the Netlify free plan. Yes, the limit is a big number, currently 3 million, but, let’s do our best not to get hit by any surprises.</p>
<p>I’ve split up my edge function into only running on known HTML page paths <a href="https://github.com/paularmstrong/paularmstrong.dev/blob/8683b83f5fcebaa223b658bc1881d2db0be14524/netlify.toml#L11-L35"><code>netlify.toml#L11-L35</code></a>. I would have liked to have seen better path matching or a way to do an exclude-list along with a path glob, but alas, that feature does not exist yet. <a href="#user-content-fnref-htmlrewrite" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[CSS box-sizing: border-box]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/10/css-box-sizing-border-box/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/10/css-box-sizing-border-box/</guid>
            <pubDate>Thu, 10 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I can’t think of any scenario when I do not want <code>box-sizing: border-box;</code> set on every element on my page by default. In fact, I can’t think of the last time that I wanted to use the default <code>content-box</code> method. However, I’ve just come across a major company’s design system that does not use <code>border-box</code> across all elements.</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/10/css-box-sizing-border-box/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>I can’t think of any scenario when I do not want <code>box-sizing: border-box;</code> set on every element on my page by default. In fact, I can’t think of the last time that I <em>wanted</em> to use the default <code>content-box</code> method.</p>
<p>However, I’ve just come across a major company’s design system that <em>does not</em> use <code>border-box</code> across all elements. And worse, if I try to set that manually for myself, the reusable components that they’ve exposed will all break. Why? Why would do this to me?</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#85E89D">html</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF">  box-sizing</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">border-box</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#85E89D">*</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#85E89D">*</span><span style="color:#B392F0">:before</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#85E89D">*</span><span style="color:#B392F0">:after</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#79B8FF">  box-sizing</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">inherit</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Please just add that as your default and never revert back.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[The case for Astro]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/09/the-case-for-astro/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/09/the-case-for-astro/</guid>
            <pubDate>Wed, 09 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I built my website with Astro, without having intended to. I often use my own personal site as a test-bed for learning new frameworks and tech. In testing out Astro, it just sort of stuck. As I continued, it became clear why – but the reason isn’t what I thought it would be.</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/09/the-case-for-astro/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import { Image } from ‘astro:assets’;
import BenchmarkImage from ’../../images/labs/benchmark.png’;</p>
<p>I built <a href="/blog/2022/10/12/new-site-who-dis/">my website with Astro</a>, without having intended to. I often use my own personal site as a test-bed for learning new frameworks and tech. In testing out Astro, it just sort of stuck. As I continued, it became clear why – but the reason may be surprising.</p>
<p>The current headline on the <a href="https://astro.build">Astro.build home page</a> is <strong>“Build faster websites.”</strong> This was a big selling point, initially, in getting me to try Astro. When building a static website, there’s not a lot of point in sending large frameworks (like React) over the wire – even for server-generated content, really. While this is one of my main requirements, it’s not the reason I’ve found to stick with Astro.</p>
<p>Astro also uses <a href="https://vitejs.dev/"><strong>Vite</strong></a> for development and building. It’s a nice bonus how fast the site runs in dev and builds for production. But again, it’s not why Astro is best for me now.</p>
<p>The real icing on the cake comes from two parts of Astro combined:</p>
<ol>
<li>
<p><a href="https://docs.astro.build/en/concepts/islands/">Astro Islands</a></p>
<p>Statically generated content getting hydrated as minimal <em>islands</em> for small interactive pieces of the UI is just what I found I ended up needing over time. While I could have done dark &#x26; light mode for the site automatically chosen by the <code>prefers-color-scheme</code> CSS media query, I wanted to be able to manually toggle modes as well – and who knows, maybe one day I’ll add a third or more other themes. This type of thing <em>requires</em> JavaScript.</p>
</li>
<li>
<p><a href="https://docs.astro.build/en/guides/integrations-guide/">Astro integrations</a></p>
<p>Now here’s the kicker. I wanted to use Astro Islands, but I didn’t want to have to load all of React in order to get it done. I went with <a href="https://www.solidjs.com/">SolidJS</a> for the main interactive bits of the site. At the moment there are 3: the theme switcher, the “scroll to top” button that appears on longer pages when scrolling, and the “share widget” at the bottom of every blog post.</p>
</li>
</ol>
<p>But wait! There’s more! It isn’t just that I could use one framework (an integration) for islands. The fact is that where I want it, I can use <em>any</em> framework. I’ve started a <a href="/labs/">labs section</a> of the site using just that!</p>
<aside>
<p>Check out the React Component Benchmark project example in <a href="/labs/">the Labs</a>.</p>
<a href="/labs/react-component-benchmark/">
  <img src="{BenchmarkImage}" width="{768}" alt="React Component Benchmark">
</a>
</aside>
<p>There’s only one item there so far, but I’m so excited about how it turned out, that I wanted to boast about Astro and the fact that I have a super fast, statically generated, multi-page site that allows me to use almost any tech without worry about bleeding JS libraries across pages.</p>
<p>Open up your network inspector as you browse across my site and you can see for yourself how the page weight is as minimal as possible from one page to another:</p>









































<table><thead><tr><th>Page</th><th>Total JS transfer</th><th>JS cached from previous pages</th><th>Non-cache transfer</th></tr></thead><tbody><tr><td><a href="/">Home</a></td><td>7.5 kB</td><td><em>n/a</em></td><td><em>n/a</em></td></tr><tr><td><a href="/blog/">Blog</a></td><td>7.5 kB</td><td>7.5 kB</td><td>0 kB</td></tr><tr><td><a href="/blog/2022/11/09/the-case-for-astro/">This page!</a></td><td>7.6 kB</td><td>6.5 kB</td><td>1.1 kB</td></tr><tr><td><a href="/labs/">Labs</a></td><td>7.5 kB</td><td>7.5 kB</td><td>0 kB</td></tr><tr><td><a href="/labs/react-component-benchmark/">React Component Benchmark</a></td><td>48 kB</td><td>3.7 kB</td><td>44.3 kB</td></tr></tbody></table>
<p>Want to see how this is all built? My website’s code is available for browsing on <a href="https://github.com/paularmstrong/paularmstrong.dev">Github at paularmstrong/paularmstrong.dev</a>!</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/benchmark.jax5ndMU.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Mourning the loss of Twitter]]></title>
            <link>https://paularmstrong.dev/blog/2022/11/04/mourning-twitter/</link>
            <guid>https://paularmstrong.dev/blog/2022/11/04/mourning-twitter/</guid>
            <pubDate>Fri, 04 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>It is odd to feel this way, I admit. But with Twitter’s new management, I feel like I’m mourning the loss of something that was near and dear to me.</p>
<p><a href="https://paularmstrong.dev/blog/2022/11/04/mourning-twitter/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>While I’m <a href="/blog/2022/11/03/inhumanity-of-twitter-management/">absolutely gutted for the people who are and were employed there</a>, this is something else entirely.</p>
<p>It is odd to feel this way, I admit. But with Twitter’s new management, I feel like I’m mourning the loss of something that was near and dear to me. Not just because I worked there and poured a lot of my care and creativity into every day, but because I’m losing something that has in part gotten me to where I am today.</p>
<p>I have an account so old that it has an numeric, incremental ID number attached to it. I was the 766,247<sup>th</sup> account signed up on Twitter. I used the platform as a way to connect with my peers, to make friends, to learn.</p>
<p>Twitter has been a part of my life for <em>sixteen years</em>. I’ve been letting that thought sink in and wondering if it will continue to be a place that I frequent, that I learn from, and that I socialize on.</p>
<p>And that’s just it: Twitter was an actual <em>socialization</em> and <em>networking</em> tool for me. My timeline was free of trolls, bigots, spam, and all the negative sides that many others saw. I carefully curated what I wanted to see and learn about over just shy of 16 years. It helped me get to know a few people, to share my work and creativity with the world.</p>
<p>I have truly loved it as a platform.</p>
<p>I don’t want to quit it. But I also don’t want to support what it has and is becoming.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[DIY Photo Booth: the nerdy details]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/26/diy-photobooth-nerdy-details/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/26/diy-photobooth-nerdy-details/</guid>
            <pubDate>Wed, 26 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I created my own Photo Booth application with Electron, Node.js, React, and Tailwindcss and used a GoPro as a webcam for my wedding instead of renting a commercial booth. It was fun to build and even more fun to use.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/26/diy-photobooth-nerdy-details/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import { Image } from ‘astro:assets’;
import YouTube from ’../../components/YouTube.astro’;
import XStateHeroLightImage from ’../../images/blog/photobooth/xstate-hero-light.png’;
import XStateHeroDarkImage from ’../../images/blog/photobooth/xstate-hero.png’;
import PreferencesImage from ’../../images/blog/photobooth/preferences.png’;</p>
<p>Previously, I wrote about how <a href="/blog/2022/10/11/diy-photobooth/">I created a DIY Photobooth</a> for my wedding reception and <a href="/blog/2022/10/11/diy-photobooth/#the-result">the results were amazing</a>. Now we can get to the choices, technical details, and hurdles I went through writing the Photo Booth application. You might consider this a <em>post-mortem tech design</em>.</p>
<aside type="info" title="Source code">
<p>If you’re anything like me and you prefer seeing code first, don’t forget that this entire project is open source! Please feel free to skip the whole post and jump right into the <a href="https://github.com/paularmstrong/photobooth">photobooth source code</a>.</p>
</aside>
<h2 id="high-level-plan">High-level plan</h2>
<p>My general plan was to create a full DIY photo booth for my wedding and spend as little money as possible. The photo booth would need the following features:</p>
<ul>
<li>Take a number of photos in succession and stitch them together as a collage.</li>
<li>Upload the final photo collages to a Firebase storage bucket immediately after creation.</li>
<li>Record video messages, up to 30 seconds, as an alternative to a guest book.</li>
<li>Be easy to use: both cognitively and physically.</li>
</ul>
<h2 id="design-choices">Design choices</h2>
<h3 id="electron-application">Electron application</h3>
<p>Most software developers would instantly judge that a native application would be the best choice to build a Photo Booth. And really, they’re probably right. A native application would likely give the speed and stability you want from something incredibly interactive that that needs to work with large images and videos.</p>
<p>That being said, I decided to use Electron for two reasons:</p>
<ol>
<li>I didn’t have the time to spend re-learning native app development.</li>
<li>I really wanted to have a reason to learn and use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API"><code>MediaStream</code> &#x26; <code>MediaRecorder</code> web APIs</a> and <a href="https://xstate.js.org/"><code>x-state</code></a> as a full-blown state-machine.</li>
</ol>
<p>So, while Electron is not the best choice, it’s still a choice that worked and there’s nothing wrong with that.</p>
<h3 id="webcam">Webcam</h3>
<p>In a best-case scenario, I would hook up a DSLR camera through an HDMI capture card and run it as a webcam to get the best quality images out of any photo taken through the <code>MediaStream</code> API. Unfortunately, I didn’t want to jump into buying an HDMI capture card and I also wanted to keep my DSLR handy for other purposes. Otherwise, I’d need to buy both another DSLR and capture card. That would quickly blow up the budget (and then some) on this project.</p>
<p>So we’re stuck using a webcams, but unfortunately, webcams are notoriously crappy. Most are 720p and the ones that say they are 1080p have poor sensors that don’t process light well and make your images come out pretty grainy. Even the “top end” webcams are not without their issues.</p>
<p>However, GoPros can also be used as webcams now, given that you have <a href="https://community.gopro.com/s/article/GoPro-Webcam">the GoPro Webcam app</a> installed. Well guess what, I just happen to have a GoPro Hero 10 that can do this.</p>
<p>While still running at a maximum of 1080p, the images from the GoPro over the <code>MediaStream</code> API come out crisp, clear, bright, and all-around better than good enough!</p>
<aside title="Alternative">
<p>There’s also an Open GoPro API that could be used. However, I found that it <a href="#open-gopro-api">suffered problems that made it a non-starter</a>.</p>
</aside>
<h3 id="user-interaction">User interaction</h3>
<p>Assuming I would not have access to a large touch-screen, I already owned an <a href="https://www.elgato.com/en/stream-deck-mini">Elgato Stream Deck Mini</a>. I could use the 6 buttons on here as an interactive user interface for navigating menus and options.</p>
<aside title="Mistake">
<p>Turns out that a <a href="#no-touch-screen">touch screen would have been a much better choice</a>, had one been available.</p>
</aside>
<h2 id="execution">Execution</h2>
<h3 id="electron">Electron</h3>
<p>Finding the right path to get started with Electron using modern Javascript (or TypeScript) and frameworks that require build tooling took longer than I would have liked. Similar to trying to figure out what is needed to just set up a website built with JS these days, Electron was just as bad or worse.</p>
<p>I stepped in thinking it would just be built-in that webpack would build my application with some useful defaults. Unfortunately, there really isn’t any build tooling to be found out of the box other than compiling to a runnable application.</p>
<p>I first tried <a href="https://github.com/electron-vite/electron-vite-boilerplate">electron-vite</a>, wanting <em>the new hotness</em> of building esmodules, thinking it’d make things faster to iterate. Unfortunately, I quickly hit the limitations of that when pulling in the <a href="https://www.npmjs.com/package/@elgato-stream-deck/node"><code>@elgato-stream-deck/node</code></a> package, as it requires linking to a native dependency, which I couldn’t figure out how to do with Vite, but am well familiar with it in webpack. (If anyone knows, please reach out and point me in the right direction!)</p>
<p>So after attempting to set up a bunch of inter-connected webpack processes myself, I thankfully gave up and replaced everything with <a href="https://www.electronforge.io/">Electron Forge</a>. This simplified the whole setup process, allowed me to hook into the webpack module rules and add the <a href="https://github.com/paularmstrong/photobooth/blob/6a136af7d3202319403c248c03050255b95c2a2e/packages/webpack/src/index.js#L12-L28">node-loader</a>.</p>
<p>After that, most of the Electron setup is pretty run-of-the-mill stuff. I tried to follow best-practices for inter-process communication between node and the client, as well as only allowing the ability to load and save to directories with the correct permissions.</p>
<p>My biggest criticism for Electron is that there is far too much outdate information around the web on how to do the more detailed bits with security, best practices, and even event listeners. The documentation is pretty good, but naming conventions don’t always match what I would expect, coming frmo a Node.js server and JavaScript client background.</p>
<h3 id="state-machine">State machine</h3>
<p>Given the complexity of managing state across two user interfaces, the screen and the StreamDeck Mini, I wanted to ensure that both were in sync at all times. To do this, I chose to build a full <a href="https://github.com/paularmstrong/photobooth/blob/main/packages/main/src/state/machine.ts">state machine (<code>main/src/state/machine.ts</code>)</a> using <a href="https://xstate.js.org/">x-state</a>, run by the Electron Node.js process.</p>
<figure class="lg:bustout">
  <a href="https://stately.ai/viz/96da6066-02dc-448e-a9f9-7a8511767b31" rel="noopener noreferrer" target="_blank">
    <div class="dark:hidden">
      <img src="{XStateHeroLightImage}" alt="Visualizer of the Photo Booth’s state machine on stately.ai" width="{1600}" loading="lazy" itemprop="image">
    </div>
    <div class="hidden dark:block">
      <img src="{XStateHeroDarkImage}" alt="Visualizer of the Photo Booth’s state machine on stately.ai" width="{1600}" loading="lazy" itemprop="image">
    </div>
  </a>
  <figcaption class="pointer-events-auto text-center">
    Check out the interactive version of the Photo Booth state machine on the [Stately
    visualizer](https://stately.ai/viz/96da6066-02dc-448e-a9f9-7a8511767b31#).
  </figcaption>
</figure>
<p>This machine handles the entire state of the application: backend, frontend (GUI), and <em>other</em> frontend (Stream Deck). When entering any state, we assign an array of possible keys and their transition type to the machine’s context, then call the <code>renderKeys</code> action.</p>
<h4 id="communicating-between-the-main-and-renderer-process">Communicating between the <code>main</code> and <code>renderer</code> process</h4>
<p>Sending messages back and forth between the two processes is crucial for most Electron applications, since they run in separate sandboxed processes.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> service</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> interpret</span><span style="color:#E1E4E8">(machine);</span></span>
<span class="line"><span style="color:#E1E4E8">service.</span><span style="color:#B392F0">start</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">service.</span><span style="color:#B392F0">onTransition</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">state</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">streamdeck</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#79B8FF">meta</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> state.context;</span></span>
<span class="line"><span style="color:#E1E4E8">  webContents.</span><span style="color:#B392F0">send</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'transition'</span><span style="color:#E1E4E8">, { value: state.</span><span style="color:#B392F0">toStrings</span><span style="color:#E1E4E8">(), meta } </span><span style="color:#F97583">as</span><span style="color:#B392F0"> TransitionData</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">ipcMain.</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'transition'</span><span style="color:#E1E4E8">, (</span><span style="color:#FFAB70">event</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">action</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  service.</span><span style="color:#B392F0">send</span><span style="color:#E1E4E8">(action);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span>
<span class="line"></span></code></pre>
<p>Let’s look at what happens when taking our four photos has completed and we want to prompt the participants to select a layout for their collage. We have four layouts to choose from, so we render those to the first four buttons, then we want our “Done” action as the last, bottom-right action. So we set up our <code>entry</code> <code>assign</code> handler to assign the keys and actions:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#6A737D">// State snippet: photo.reviewing.selecting</span></span>
<span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#B392F0">	selecting</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#B392F0">		entry</span><span style="color:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#B392F0">			assign</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#B392F0">				keys</span><span style="color:#E1E4E8">: () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#E1E4E8">					{ key: </span><span style="color:#9ECBFF">'quad'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'SELECT'</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">					{ key: </span><span style="color:#9ECBFF">'quadtych'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'SELECT'</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">					{ key: </span><span style="color:#9ECBFF">'collage'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'SELECT'</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">					{ key: </span><span style="color:#9ECBFF">'random'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'SELECT'</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#79B8FF">					null</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">					{ key: </span><span style="color:#9ECBFF">'done'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'DONE'</span><span style="color:#E1E4E8">, description: </span><span style="color:#9ECBFF">'Save your collage'</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">				],</span></span>
<span class="line"><span style="color:#E1E4E8">			}),</span></span>
<span class="line"><span style="color:#9ECBFF">			'renderKeys'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">		],</span></span>
<span class="line"><span style="color:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Then our <code>renderKeys</code> action is immediately fired and renders the defined images to each key, clearing out <code>null</code> (blank) keys along the way.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#B392F0">	actions</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#B392F0">		renderKeys</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">context</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">			const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">keys</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">streamdeck</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> context;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">			for</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">let</span><span style="color:#E1E4E8"> i </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">; i </span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8"> keys.</span><span style="color:#79B8FF">length</span><span style="color:#E1E4E8">; i</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">				const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">key</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> keys[i] </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> {};</span></span>
<span class="line"><span style="color:#F97583">				if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">key) {</span></span>
<span class="line"><span style="color:#E1E4E8">					streamdeck.</span><span style="color:#B392F0">clearKey</span><span style="color:#E1E4E8">(i);</span></span>
<span class="line"><span style="color:#F97583">					continue</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">				}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">				streamdeck.</span><span style="color:#B392F0">fillKeyBuffer</span><span style="color:#E1E4E8">(i, images[key]);</span></span>
<span class="line"><span style="color:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>One small design choice: It was inefficient to send the entire Stream Deck object into the state machine’s context and didn’t mesh well with normal patterns in x-state. However, not passing the entire Stream Deck object to the state machine’s context required a bit of digging into the state’s key context to send the correct action back into the service. Turns out that this really wasn’t too bad.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#E1E4E8">streamdeck.</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'up'</span><span style="color:#E1E4E8">, (</span><span style="color:#FFAB70">keyIndex</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> number</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> action</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> service.state.context.keys[keyIndex];</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">action) {</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">  service.</span><span style="color:#B392F0">send</span><span style="color:#E1E4E8">(action);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span>
<span class="line"></span></code></pre>
<h4 id="syncing-state-between-main-and-renderer-process">Syncing state between main and renderer process</h4>
<p>On each state transition, we also pass the state context to the renderer application and use a custom <a href="#syncing-state-between-main-and-renderer-process"><code>useLocation()</code> hook</a> to merge the state machine’s context into the React Router Location state. This then lets us extract out each key and render it to a <code>&#x3C;HelpCard /></code> on screen.</p>
<p>Since the routing and location needed to be determined by the state machine that’s run in the main node.js process, I added a new faux-context wrapper <code>NavigationProvider</code>, that listens to the <code>transition</code> event passed to the renderer process, and calls React Router’s <code>navigate()</code> function.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { useLocation </span><span style="color:#F97583">as</span><span style="color:#E1E4E8"> useRRLocation, useNavigate } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react-router-dom'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> NavigationProvider</span><span style="color:#E1E4E8">({ </span><span style="color:#FFAB70">children</span><span style="color:#E1E4E8"> }</span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> { </span><span style="color:#FFAB70">children</span><span style="color:#F97583">:</span><span style="color:#B392F0"> React</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">ReactNode</span><span style="color:#E1E4E8"> }) {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> navigate</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useNavigate</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">  React.</span><span style="color:#B392F0">useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> remove</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> window.api.</span><span style="color:#B392F0">addListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'transition'</span><span style="color:#E1E4E8">, ({ </span><span style="color:#FFAB70">value</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">meta</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#B392F0">      navigate</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8">        {</span></span>
<span class="line"><span style="color:#6A737D">          // Important: need to convert x-state value from an array/dot-notation to URL-compatible paths</span></span>
<span class="line"><span style="color:#6A737D">          // eg. ['photo', 'capturing', 'photo.capturing'] -> /photo/capturing</span></span>
<span class="line"><span style="color:#E1E4E8">          pathname: </span><span style="color:#9ECBFF">`/${</span><span style="color:#E1E4E8">value</span><span style="color:#9ECBFF">[</span><span style="color:#E1E4E8">value</span><span style="color:#9ECBFF">.</span><span style="color:#79B8FF">length</span><span style="color:#F97583"> -</span><span style="color:#79B8FF"> 1</span><span style="color:#9ECBFF">].</span><span style="color:#B392F0">replace</span><span style="color:#9ECBFF">(</span><span style="color:#9ECBFF">/</span><span style="color:#85E89D;font-weight:bold">\.</span><span style="color:#9ECBFF">/</span><span style="color:#F97583">g</span><span style="color:#9ECBFF">, </span><span style="color:#9ECBFF">'/'</span><span style="color:#9ECBFF">)</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#E1E4E8">        { state: meta },</span></span>
<span class="line"><span style="color:#E1E4E8">      );</span></span>
<span class="line"><span style="color:#E1E4E8">    });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> remove;</span></span>
<span class="line"><span style="color:#E1E4E8">  }, []);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> &#x3C;>{children}&#x3C;/>;</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Then, in order to ensure that our state object always reported to Typescript with the correct shape, I needed a custom <code>useLocation()</code> hook that forced the type.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#F97583"> type</span><span style="color:#E1E4E8"> { TransitionData } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@pb/main'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> useLocation</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">state</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#79B8FF">location</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useRRLocation</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> { state: state </span><span style="color:#F97583">as</span><span style="color:#B392F0"> TransitionData</span><span style="color:#E1E4E8">[</span><span style="color:#9ECBFF">'meta'</span><span style="color:#E1E4E8">], </span><span style="color:#F97583">...</span><span style="color:#E1E4E8">location };</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<h3 id="user-interface">User interface</h3>
<p>I left this detail out of the plan, because I didn’t care too much about what I ended up going with. I picked React and Tailwindcss, as I figured that’d be the least overhead in terms of familiarity – I wouldn’t need to learn something new here.</p>
<h4 id="animation">Animation</h4>
<p>In the initial working iterations, the application still felt a bit too much like it was meant to be a web application, not eagerly loading the next view and not able to smoothly transition from one state to another. I decided that this was jarring in practice, so I explored how I could make everything feel like it was flowing, fun, and more like a “native” application.</p>
<p>Given that I was using Tailwindcss, <code>CSSTransition</code> from <a href="https://reactcommunity.org/react-transition-group/css-transition">React Transition Group</a> came through as an obvious choice. With it, you can easily add and remove CSS classes for various states of transitioning from one state to another. The hard part came in hooking this into <code>react-router</code> in order to transition from one view to another via animating the different parts of the screen.</p>
<p>After a bunch of trial and error, the key was to forego using react-router’s <code>Routes</code> (v6+) or <code>Switch</code> (v5) in favor of my own implementation that could pass the transition state down through to each view. This required a few parts:</p>
<p>First, we need a route mapping for <em>all</em> views that will be shown. In order for every view to receive transition state, we <em>cannot</em> use nested routing. So we create a <code>routeMap</code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> routeMap</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Record</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">string</span><span style="color:#E1E4E8">, </span><span style="color:#B392F0">React</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">FunctionComponent</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">Props</span><span style="color:#E1E4E8">>> </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#9ECBFF">  '/photo/confirming'</span><span style="color:#E1E4E8">: PhotoConfirm,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/photo/capturing'</span><span style="color:#E1E4E8">: PhotoCapture,</span></span>
<span class="line"><span style="color:#6A737D">  // This state may have multiple sub-states, but they are mostly invisible to the user</span></span>
<span class="line"><span style="color:#9ECBFF">  '/photo/reviewing/*'</span><span style="color:#E1E4E8">: PhotoSave,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/photo/complete'</span><span style="color:#E1E4E8">: PhotoReview,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/video/confirming'</span><span style="color:#E1E4E8">: VideoConfirm,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/video/recording/readying'</span><span style="color:#E1E4E8">: Readying,</span></span>
<span class="line"><span style="color:#6A737D">  // This state may have multiple sub-states, but they are mostly invisible to the user</span></span>
<span class="line"><span style="color:#9ECBFF">  '/video/recording/*'</span><span style="color:#E1E4E8">: VideoRecord,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/video/reviewing'</span><span style="color:#E1E4E8">: VideoReview,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/main/preferences'</span><span style="color:#E1E4E8">: Preferences,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/main/help'</span><span style="color:#E1E4E8">: MainHelp,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/main/*'</span><span style="color:#E1E4E8">: Main,</span></span>
<span class="line"><span style="color:#9ECBFF">  '/setup'</span><span style="color:#E1E4E8">: Startup,</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> routes</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> Object.</span><span style="color:#B392F0">keys</span><span style="color:#E1E4E8">(routeMap).</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">path</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> ({ path }));</span></span>
<span class="line"></span></code></pre>
<p>Once that’s set up, we can use the <code>SwitchTransition</code> and <code>CSSTransition</code> components from React Transition Group to handle moving from one route to another</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { HashRouter, matchRoutes } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react-router-dom'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { CSSTransition, SwitchTransition } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react-transition-group'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { useLocation } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './context'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> Router</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> location</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useLocation</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> match</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> React.</span><span style="color:#B392F0">useMemo</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> matched</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> matchRoutes</span><span style="color:#E1E4E8">(routes, location);</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#F97583"> !</span><span style="color:#E1E4E8">matched </span><span style="color:#F97583">?</span><span style="color:#79B8FF"> null</span><span style="color:#F97583"> :</span><span style="color:#E1E4E8"> matched[</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">].route;</span></span>
<span class="line"><span style="color:#E1E4E8">  }, [location]);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#79B8FF">SwitchTransition</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#79B8FF">CSSTransition</span></span>
<span class="line"><span style="color:#6A737D">        // ensuring match.path here should not be necessary if we had a better guarantee</span></span>
<span class="line"><span style="color:#6A737D">        // that every route would be accounted for</span></span>
<span class="line"><span style="color:#B392F0">        timeout</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{match?.path </span><span style="color:#F97583">?</span><span style="color:#79B8FF"> 250</span><span style="color:#F97583"> :</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#B392F0">        key</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{match?.path </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8">      ></span></span>
<span class="line"><span style="color:#E1E4E8">        {(</span><span style="color:#FFAB70">status</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D">          // Again, just in-case, so we don't crash during development</span></span>
<span class="line"><span style="color:#F97583">          if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">match </span><span style="color:#F97583">||</span><span style="color:#F97583"> !</span><span style="color:#E1E4E8">match.path) {</span></span>
<span class="line"><span style="color:#F97583">            return</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#F97583">          const</span><span style="color:#79B8FF"> Component</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> routeMap[match.path];</span></span>
<span class="line"><span style="color:#6A737D">          // Important! The CSS Transition status gets sent through to be handled by</span></span>
<span class="line"><span style="color:#6A737D">          // each individual View, since all may have their own layout and components</span></span>
<span class="line"><span style="color:#6A737D">          // needing specific animations</span></span>
<span class="line"><span style="color:#F97583">          return</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#79B8FF">Component</span><span style="color:#B392F0"> status</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{status} />;</span></span>
<span class="line"><span style="color:#E1E4E8">        }}</span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;/</span><span style="color:#79B8FF">CSSTransition</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#79B8FF">SwitchTransition</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Okay, that was a bit of setup, but not too bad. Now, for the payoff, each route’s view can accept the <code>status</code> property and add CSS classes for each possible transition status:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> clsx </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'clsx'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> ExampleView</span><span style="color:#E1E4E8">({ </span><span style="color:#FFAB70">status</span><span style="color:#E1E4E8"> }</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Props</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span></span>
<span class="line"><span style="color:#B392F0">      className</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#B392F0">clsx</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#9ECBFF">        'opacity-0'</span><span style="color:#E1E4E8">: status </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'entering'</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> status </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'exited'</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> status </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'unmounted'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">        'transition-all ease-out opacity-1'</span><span style="color:#E1E4E8">: status </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'entered'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#9ECBFF">        'transition-all ease-in opacity-0'</span><span style="color:#E1E4E8">: status </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'exiting'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      })}</span></span>
<span class="line"><span style="color:#E1E4E8">    ></span></span>
<span class="line"><span style="color:#E1E4E8">      {</span><span style="color:#6A737D">/* content to fade in/out */</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>In use, that became cumbersome to remember, so I ended up creating a <a href="https://github.com/paularmstrong/photobooth/blob/6a136af7d3202319403c248c03050255b95c2a2e/packages/renderer/src/modules/transitions.ts"><code>transition()</code> function helper</a>. This made it easy to reuse transitions across different views and components.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { transition } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../modules/transition'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> ExampleView</span><span style="color:#E1E4E8">({ </span><span style="color:#FFAB70">status</span><span style="color:#E1E4E8"> }</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Props</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#B392F0">clsx</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'...'</span><span style="color:#E1E4E8">, status </span><span style="color:#F97583">?</span><span style="color:#B392F0"> transition</span><span style="color:#E1E4E8">(status, </span><span style="color:#9ECBFF">'slideUp'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">:</span><span style="color:#79B8FF"> undefined</span><span style="color:#E1E4E8">)}></span></span>
<span class="line"><span style="color:#E1E4E8">      {</span><span style="color:#6A737D">/* content to slide up when entering and down when exiting */</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<h2 id="last-minute-additions">Last minute additions</h2>
<p>After confirming that the GoPro worked as a webcam, I didn’t really leave it set up to run all of the time during development, as it was just a bit too much to have hanging around. What I didn’t realize though was that if you had multiple cameras available, eg: a laptop with a built-in camera, the GoPro would never be chosen as the default camera.</p>
<p>I quickly needed to add preferences to the application, so I threw together a few settings that would probably make it helpful if anyone else ever wanted to pick up <a href="https://github.com/paularmstrong/photobooth">the source code</a> and repurpose it for their own party.</p>
<p><img src="{PreferencesImage}" alt="Screenshot of the Photo Booth’s preferences view" width="{1200}" loading="lazy" itemprop="image"></p>
<h2 id="result">Result</h2>
<p>I’m super happy with the end result. There isn’t much more to say other than <em>it worked</em> and it worked <em>well</em> – just as well as a $1,500 rental would have.</p>
<p>So here’s me messing around with it at home and trying to get Milo to pose with me.</p>
<youtube code="e6I4G3Bf0Qk" title="The Photo Booth application in action" wide="">
<hr>
<h2 id="what-went-wrong">What went wrong</h2>
<h3 id="open-gopro-api">Open GoPro API</h3>
<p>GoPro has an API called <a href="https://gopro.github.io/OpenGoPro/">Open GoPro</a>. It’s a fully documented API spec to control their cameras over Bluetooth LE, WiFi, and USB<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
<p>Unfortunately, after buying a brand new GoPro Hero 10 ($350) and spending at least 30 hours with the API over USB, I had to throw out the idea of using the API. The fact of it is that GoPro cameras are just <em>very slow</em> at taking photos and being ready to take another photo. Probably fine out in the wild, since the cameras are rarely used for photos – typically just action video, but in a setting where timing and speed are key, controlling the camera with the API was just an absolute failure.</p>
<h3 id="gopro-overheating">GoPro overheating</h3>
<p>Originally, I had bought a USB passthrough door for the battery cover. I took the battery out in hopes that it would both not drain on the battery and not overheat. Well, turns out that the door and lack of battery increased the heat of the GoPro and it ended up overheating. Thankfully, this was during our family dinner the night before the wedding. I set up the “Memory Shack” in our living room so that we could have a full test-run with it. Everything was great until I noticed that the video was really distorted, pointed downward, despite the camera being completely upright.</p>
<p>A very weird experience for sure. I took off the passthrough door and just left the unit completely open. This allowed enough air to fully flow in and out of the device, fixing the overheating problem.</p>
<h3 id="wifi-restrictions">WiFi restrictions</h3>
<p>I wrote a separate script to watch the folder that photos were saved into and automatically resize, compress, and upload to Firebase storage and add into the <a href="/blog/2022/06/04/custom-wedding-website-part-1">wedding website</a>. This worked great during the test run, but turns out our venue’s wifi had some weird network restrictions running and no photos uploaded. It was a bummer that no one was able to download their photos at the event and share, but a quick fix the next morning got all of the photos uploaded to our gallery.</p>
<h3 id="no-touch-screen">No touch screen</h3>
<p>After watching my 10-year-old nephew try to use the Photo Booth with the 6-button Stream Deck Mini, it was instantly obvious that a touch screen would have been the most intuitive choice.</p>
<p>I was thankful that I had covered the borrowed screen with a thin acrylic sheet, lest his finger jabbing go right through the screen.</p>
<p>While the Stream Deck Mini worked well and people figured out the buttons quickly, it still required a learning moment that I would have loved to remove.</p>
<h2 id="cost-breakdown">Cost breakdown</h2>













































<table><thead><tr><th>What</th><th>Cost</th><th>Actual cost</th></tr></thead><tbody><tr><td><a href="https://www.elgato.com/en/stream-deck-mini">Elgato Stream Deck Mini</a></td><td>owned</td><td>$80</td></tr><tr><td><a href="https://www.amazon.com/Lepow-C2S-1920%C3%971080P-Kickstand-Black/dp/B09H2WF8TY?ref_=ast_sto_dp&#x26;th=1&#x26;psc=1">Portable USB-C screen</a></td><td>borrowed</td><td>$190</td></tr><tr><td><a href="https://gopro.com/en/us/shop/cameras/hero10-black/CHDHX-101-master.html">GoPro Hero 10</a></td><td>owned</td><td>$350</td></tr><tr><td><a href="https://www.amazon.com/gp/product/B08DD36M29/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&#x26;psc=1">12” LED ring light</a></td><td>$40</td><td>$40</td></tr><tr><td>Wood &#x26; supplies for structure</td><td>gifted</td><td>?</td></tr><tr><td>Acrylic sheet to cover the screen</td><td>$16</td><td>$16</td></tr><tr><td>Total</td><td>$56</td><td>at least $676</td></tr></tbody></table>
<p>So, given the cost breakdown, I like to tell myself that I only spent about $60 on the entire photo booth – and even if I count every piece of hardware (less the actual computer that has to run the app), I would still be spending about $1,000 less than if I had rented a Photo Booth.</p>
<p>However, if you count how my time is worth money as well, I probably spent at least 100 hours in total across software and physical structure building – maybe more. I don’t even want to try calculating a dollar amount on this.</p>
<p>I like to keep in mind that the time spent was also an investment into my own skills. I can now say that I’ve gone through building at least a decent piece of software using Electron, dug pretty deep into proper state machines, and become intimately familiar with the web Media APIs.</p>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>Some protocols for the Open GoPro API are more comprehensive and stable than others. In my testing, USB was the most stable, but still fell short due to timing issues. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section></youtube>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/diy-hero.DmvakzVs.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Current tool chest]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/18/current-tool-chest/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/18/current-tool-chest/</guid>
            <pubDate>Tue, 18 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>This is a collection of tools and things that I use on a daily basis that I just want to have written down somewhere. Maybe they’re useful for you too.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/18/current-tool-chest/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>This is a collection of tools and things that I use on a daily basis that I just want to have written down somewhere. Maybe they’re useful for you too.</p>
<h2 id="apps">Apps</h2>
<ul>
<li><a href="https://1password.com/">1Password</a> - I tried Bitwarden, hosting an open source version myself, but nothing quite works as seamlessly as 1Password. I’ll keep giving them my money until something better comes along.</li>
<li><a href="https://plexamp.com/">Plexamp</a> - I have <em>a lot</em> of music. Many old and rare albums that just aren’t available on streaming services. And I’m continually finding and purchasing more. Plexamp gives me all the best of what a streaming service can offer, but with <em>my</em> music library.</li>
<li><a href="https://www.sublimetext.com/">Sublime Text</a> - I keep giving VS Code a shot, but it just tends to do <em>too much</em> and get in my way more than it helps. I’ll stick with Sublime Text until it either no longer works or VS Code gets better for my workflow.</li>
</ul>
<h2 id="terminal">Terminal</h2>
<ul>
<li><a href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</a> - Suggests commands from your history without needing to type <kbd aria-label="hold control">⌃</kbd>+<kbd>R</kbd>. Hit your <kbd aria-label="right arrow">→</kbd> key to accept the suggestion, then <kbd>Enter</kbd> to run – or keep typing to do something else.</li>
<li><a href="https://terminalizer.com">Terminalizer</a> - record terminal input/output and create GIFs and videos for sharing how-to and examples.</li>
<li><a href="https://powerline.readthedocs.io/en/latest/">Powerline</a> - Better looking and more informative PS1 and status lines.</li>
<li><a href="https://github.com/tmux/tmux">tmux</a> - terminal multiplexer</li>
</ul>
<h2 id="hardware">Hardware</h2>
<ul>
<li><a href="https://www.logitech.com/en-us/products/mice/mx-master-3s.910-006556.html">Logitech MX Master 3</a> - Get yourself a great mouse.</li>
<li><a href="https://www.keychron.com/products/keychron-k6-wireless-mechanical-keyboard">Keychron K6</a> - Gotta have a keyboard that fits your typing style and feels great.</li>
<li><a href="https://www.caldigit.com/ts3-plus/">CalDigit TS3 Plus</a> - I switch between my person Mac Mini and work laptop multiple times per day. Enabling me to use the same keyboard and dual monitors (along with the Bluetooth switching on the Logitech MX Master 3), it’s almost like having a KVM that supports USB-C/Thunderbolt.</li>
<li><a href="https://www.bluemic.com/en-us/products/yeti-x/">Blue Yeti X microphone</a> on a scissor swing-arm microphone stand - I don’t do much or any streaming anymore, but ensuring that I’m clear in video calls is wonderful.</li>
<li><a href="https://ember.com/products/ember-mug-2?variant=32734187126869">Ember Mug<sup>2</sup></a> - It’s not without flaws, but it sure is nice to have my tea kept hot while I’m working. Nothing’s worse than taking a sip and getting something lukewarm.</li>
</ul>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Technical design starter template]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/14/tech-design-template/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/14/tech-design-template/</guid>
            <pubDate>Fri, 14 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>As teams grow, become distributed, and not everyone is able to be involved in every decision, it becomes more important to record what’s happening and why decisions are and were made. From a technical standpoint, “Tech Designs” are a great way to structure this information and keep around long-term for posterity.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/14/tech-design-template/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import Details from ’../../components/Details.astro’;</p>
<p>As teams grow, become distributed, and not everyone is able to be involved in every decision, it becomes more important to record what’s happening and why decisions are and were made. From a technical standpoint, <em>Tech Designs</em> are a great way to structure this information and keep around long-term for posterity.</p>
<p>The following is a template that I’ve used and introduced to teams I’ve worked with in the past. This template is a great starting point and not intended to be an end-all do-or-die approach, so take this more as inspiration than anything else.</p>
<section class="prose prose-sm mx-4 max-w-none rounded bg-slate-200 p-4 shadow-lg dark:prose-invert lg:prose-base xl:prose-base prose-headings:text-slate-800 prose-em:text-gray-500 dark:bg-slate-800 dark:prose-headings:text-slate-200 dark:prose-em:text-gray-400 md:mx-8 md:p-8">
<h1 id="a-short-but-descriptive-title">A short but descriptive title</h1>





























<table><thead><tr><th align="right"></th><th></th></tr></thead><tbody><tr><td align="right"><strong>Author(s)</strong></td><td>@name</td></tr><tr><td align="right"><strong>Stakeholders</strong></td><td>@stakeholders</td></tr><tr><td align="right"><strong>Status</strong></td><td><em>Draft</em> / <em>In-Review</em> / <em>Rejected</em> / <em>Approved</em> / <em>Completed</em></td></tr><tr><td align="right"><strong>Resourcing</strong></td><td><em>N</em> people</td></tr><tr><td align="right"><strong>Timeline</strong></td><td><em>X</em> weeks</td></tr></tbody></table>
<i>
  A standard table header helps to easily scan who is involved, making decisions, and what kind of high level thinking
  is going into this proposed project.
</i>
<h2 id="problem-statement">Problem statement</h2>
<i>
  Write a short 1-3 sentence problem statement. This section should be succinct to summarize the problem and call out
  its severity quickly.
</i>
<h2 id="goals">Goals</h2>
<i>
  Your design should have at least one goal that it is aiming to solve. Your goals should be **customer-focused** and
  explain how the benefits that your **customer** should expect after the work from this design is completed.
</i>
<i>
  Note that the term *customer* here only relates to the “main benefactor” and is not necessarily a user of a website.
  It could be developers in your organization, a third party system you work with, or someone else entirely.
</i>
<h3 id="non-goals">Non-goals</h3>
<i>
  Sometimes there are specific things that you explicitly **will not** be solving. Include them here so stakeholders
  understand that the scope of this design is limited.
</i>
<h2 id="solution">Solution</h2>
<i>
  Use this section to provide details on how we will solve the problem stated at the beginning of this document. Be sure
  to explain how all of the goals previously listed will be met.
</i>
<i>
  While it is important to be detailed and thorough, avoid going too far and actually writing your implementation here.
  The key is to describe **how** your solution will work just enough to be able to break it out into tasks, without
  doing those tasks yet.
</i>
<i>
  Depending on your team’s needs, you may add some more standard sections here like “Security considerations”, “Data
  privacy”, or “Scalability”.
</i>
<h2 id="alternatives-considered">Alternatives considered</h2>
<i>
  A good technical design also considers alternative approaches and explains why they were not chosen. Ensure to include
  pros and cons of the alternatives versus the chosen solution. Ideally, this section helps to show that an *objective
  decision* is being made.
</i>
<h2 id="open-questions">Open questions</h2>
<i>
  If you’re unsure about anything, list some open questions here. This is a good spot to get extra feedback from
  reviewers before proceeding to build your solution.
</i>
</section>
<hr>
<aside type="tip" title="Goodies">
<p>Grab the markdown formatted source for this template and modify it for your own needs!</p>
<details title="Expand for Markdown">
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="md"><code><span class="line"><span style="color:#79B8FF;font-weight:bold"># A short but descriptive title</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">|                  |                                                               |</span></span>
<span class="line"><span style="color:#E1E4E8">| ---------------: | ------------------------------------------------------------- |</span></span>
<span class="line"><span style="color:#E1E4E8">|    </span><span style="color:#E1E4E8;font-weight:bold">**Author(s)**</span><span style="color:#E1E4E8"> | @name                                                         |</span></span>
<span class="line"><span style="color:#E1E4E8">| </span><span style="color:#E1E4E8;font-weight:bold">**Stakeholders**</span><span style="color:#E1E4E8"> | @stakeholders                                                 |</span></span>
<span class="line"><span style="color:#E1E4E8">|       </span><span style="color:#E1E4E8;font-weight:bold">**Status**</span><span style="color:#E1E4E8"> | </span><span style="color:#E1E4E8;font-style:italic">_Draft_</span><span style="color:#E1E4E8"> / </span><span style="color:#E1E4E8;font-style:italic">_In-Review_</span><span style="color:#E1E4E8"> / </span><span style="color:#E1E4E8;font-style:italic">_Rejected_</span><span style="color:#E1E4E8"> / </span><span style="color:#E1E4E8;font-style:italic">_Approved_</span><span style="color:#E1E4E8"> / </span><span style="color:#E1E4E8;font-style:italic">_Completed_</span><span style="color:#E1E4E8"> |</span></span>
<span class="line"><span style="color:#E1E4E8">|   </span><span style="color:#E1E4E8;font-weight:bold">**Resourcing**</span><span style="color:#E1E4E8"> | </span><span style="color:#E1E4E8;font-style:italic">_N_</span><span style="color:#E1E4E8"> people                                                    |</span></span>
<span class="line"><span style="color:#E1E4E8">|     </span><span style="color:#E1E4E8;font-weight:bold">**Timeline**</span><span style="color:#E1E4E8"> | </span><span style="color:#E1E4E8;font-style:italic">_X_</span><span style="color:#E1E4E8"> weeks                                                     |</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_A standard table header helps to easily scan who is involved, making decisions, and what kind of high level thinking is going into this proposed project._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">## Problem statement</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Write a short 1-3 sentence problem statement. This section should be succinct to summarize the problem and call out its severity quickly._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">## Goals</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Your design should have at least one goal that it is aiming to solve. Your goals should be </span><span style="color:#E1E4E8;font-weight:bold">**customer-focused**</span><span style="color:#E1E4E8;font-style:italic"> and explain how the benefits that your </span><span style="color:#E1E4E8;font-weight:bold">**customer**</span><span style="color:#E1E4E8;font-style:italic"> should expect after the work from this design is completed._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Note that the term *customer* here only relates to the “main benefactor” and is not necessarily a user of a website. It could be developers in your organization, a third party system you work with, or someone else entirely._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">### Non-goals</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Sometimes there are specific things that you explicitly </span><span style="color:#E1E4E8;font-weight:bold">**will not**</span><span style="color:#E1E4E8;font-style:italic"> be solving. Include them here so stakeholders understand that the scope of this design is limited._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">## Solution</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Use this section to provide details on how we will solve the problem stated at the beginning of this document. Be sure to explain how all of the goals previously listed will be met._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_While it is important to be detailed and thorough, avoid going too far and actually writing your implementation here. The key is to describe </span><span style="color:#E1E4E8;font-weight:bold">**how**</span><span style="color:#E1E4E8;font-style:italic"> your solution will work just enough to be able to break it out into tasks, without doing those tasks yet._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_Depending on your team’s needs, you may add some more standard sections here like “Security considerations”, “Data privacy”, or “Scalability”._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">## Alternatives considered</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_A good technical design also considers alternative approaches and explains why they were not chosen. Ensure to include pros and cons of the alternatives versus the chosen solution. Ideally, this section helps to show that an *objective decision* is being made._</span></span>
<span class="line"></span>
<span class="line"><span style="color:#79B8FF;font-weight:bold">## Open questions</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8;font-style:italic">_If you’re unsure about anything, list some open questions here. This is a good spot to get extra feedback from reviewers before proceeding to build your solution._</span></span>
<span class="line"></span></code></pre>
</details>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Creating a modern wedding website in 2022 Part 2: Setting up the repository and frameworks]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/13/custom-wedding-website-part-2/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/13/custom-wedding-website-part-2/</guid>
            <pubDate>Thu, 13 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Don’t go with those junk off-the-shelf websites with all the upsells on them. Make your wedding website yourself.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/13/custom-wedding-website-part-2/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<aside type="info" title="Multi-part series">
<p>Previously in this series, we <a href="/blog/2022/06/04/custom-wedding-website/">introduced the reasoning and requirements</a> and <a href="/blog/2022/06/04/custom-wedding-website-part-1/">chose tools that we will use to build</a> our custom wedding website.</p>
</aside>
<h2 id="setting-up-firebase">Setting up Firebase</h2>
<p>Setting up a Firebase project was <em>actually</em> a delightful experience.</p>
<p>First, create the project in the <a href="https://console.firebase.google.com/u/0/">Firebase console</a>. This step just takes a couple clicks.</p>
<p>After creating the project in the Firebase console, you need to hook something up to actually deploy and work with configurations from the command-line.</p>
<p>If you don’t have it already, install <code>firebase-tools</code> and log into the commandline interface:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#79B8FF"> -g</span><span style="color:#9ECBFF"> firebase-tools</span></span>
<span class="line"><span style="color:#B392F0">firebase</span><span style="color:#9ECBFF"> login</span></span>
<span class="line"></span></code></pre>
<p>Once installed, we can log into firebase and initialize the database. Run the following command, select your project, and customize as you want. I didn’t change any of the defaults to get started.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">firebase</span><span style="color:#9ECBFF"> init</span><span style="color:#9ECBFF"> firestore</span></span>
<span class="line"></span></code></pre>
<p>Next, we are going to want hosting, because it’s free. Again, run the init and walk through the wizard.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">firebase</span><span style="color:#9ECBFF"> init</span><span style="color:#9ECBFF"> hosting</span></span>
<span class="line"></span></code></pre>
<p>From there, the initialization script actually walked through a few questions which didn’t seem patronizing for a <em>wizard</em>. Then it noticed that the origin for my git repo pointed to Github, so it asked if I wanted to set up automated Github actions to deploy on merge to <code>main</code> and stage deploys for PRs. <em>Heck yes!</em></p>
<h2 id="parcel-dev-environment--bundling">Parcel dev environment &#x26; bundling</h2>
<p>Let me get one thing straight: using Parcel was the absolute most straightforward and easy dev/build framework that I’ve ever picked up. I don’t know that I needed to do anything more than point it at a root <code>index.html</code> file – and that was really it!</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">yarn</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> preact</span><span style="color:#9ECBFF"> preact-router</span></span>
<span class="line"><span style="color:#B392F0">yarn</span><span style="color:#9ECBFF"> add</span><span style="color:#79B8FF"> --dev</span><span style="color:#9ECBFF"> autoprefixer</span><span style="color:#9ECBFF"> parcel</span><span style="color:#9ECBFF"> postcss</span><span style="color:#9ECBFF"> tailwindcss</span></span>
<span class="line"></span></code></pre>
<aside type="danger" title="Typescript">
<p>I used Typescript, but you really don’t have to. In fact, I didn’t require that static types were correct at all. It was mostly just a helper to have automcomplete for working with Firebase. From here on out, I’ll actually just strip all the types out and use <code>.js</code> &#x26; <code>.jsx</code> instead of <code>.ts</code> &#x26; <code>.tsx</code>.</p>
</aside>
<p>Next we need some basic setup for Parcel, Preact, and Tailwind. Create the following files following this structure, with copypasta contents to follow:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>wedding-app/</span></span>
<span class="line"><span>├─ src/</span></span>
<span class="line"><span>│  ├─ index.html</span></span>
<span class="line"><span>│  ├─ index.css</span></span>
<span class="line"><span>│  └─ index.jsx</span></span>
<span class="line"><span>├─ package.json</span></span>
<span class="line"><span>└─ tailwind.config.js</span></span>
<span class="line"><span></span></span></code></pre>
<p><code>src/index.html</code> This will be the main entrypoint for Parcel to run its development server and build for
production.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8">&#x3C;!</span><span style="color:#85E89D">doctype</span><span style="color:#B392F0"> html</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">html</span><span style="color:#B392F0"> lang</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"en"</span><span style="color:#B392F0"> dir</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"ltr"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">head</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">meta</span><span style="color:#B392F0"> charset</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"utf-8"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">meta</span><span style="color:#B392F0"> name</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"viewport"</span><span style="color:#B392F0"> content</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"width=device-width, initial-scale=1"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">title</span><span style="color:#E1E4E8">>Kelly &#x26; Paul’s wedding&#x3C;/</span><span style="color:#85E89D">title</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">    // Parcel will pick up references to CSS files and compile them as necessary // This makes Tailwind setup in the</span></span>
<span class="line"><span style="color:#E1E4E8">    next step a breeze</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">link</span><span style="color:#B392F0"> rel</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"stylesheet"</span><span style="color:#B392F0"> href</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"./index.css"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">head</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">body</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    // This will be the root element that we use for the Preact application</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"root"</span><span style="color:#E1E4E8">>&#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    // Parcel will pick up references to JavaScript files (or Typescript) and compile them</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">script</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"module"</span><span style="color:#B392F0"> src</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"./index.jsx"</span><span style="color:#E1E4E8">>&#x3C;/</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">body</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">html</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<p><code>./src/index.jsx</code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="tsx"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { h, render } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'preact'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { App } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './App'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> root</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'root'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#B392F0">render</span><span style="color:#E1E4E8">(&#x3C;</span><span style="color:#79B8FF">App</span><span style="color:#E1E4E8"> />, root</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>
<h2 id="tailwind-css">Tailwind CSS</h2>
<p>If you prefer a more in-depth setup, checkout <a href="https://tailwindcss.com/docs/installation">Tailwindcss‘s installation guide</a>.</p>
<p><code>./src/index.css</code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#F97583">@tailwind</span><span style="color:#E1E4E8"> base;</span></span>
<span class="line"><span style="color:#F97583">@tailwind</span><span style="color:#E1E4E8"> components;</span></span>
<span class="line"><span style="color:#F97583">@tailwind</span><span style="color:#E1E4E8"> utilities;</span></span>
<span class="line"></span></code></pre>
<p><code>./tailwind.config.js</code></p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#79B8FF">module</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">exports</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  content: [</span><span style="color:#9ECBFF">'./src/**/*.{html,ts,tsx}'</span><span style="color:#E1E4E8">],</span></span>
<span class="line"><span style="color:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>
<p><code>./postcssrc.json</code> One little gotcha here: don’t forget to add a Postcss configuration for Parcel to pick up so that it knows it should be using Tailwind!</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "plugins"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "tailwindcss"</span><span style="color:#E1E4E8">: {}</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<h2 id="thats-all">That’s all!</h2>
<p><code>./package.json</code> We just need a couple quick scripts in our root package configuration in order to run the Parcel dev server and build for production</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "name"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"wedding-app"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "private"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "scripts"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "start"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"parcel src/index.html"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">    "build"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"rm -rf dist &#x26;&#x26; NODE_ENV=production parcel build ./src/*.html --public-url /"</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Honestly, this was kind of everything that I needed to get started. I know this is a little nitty gritty into some weird details, but hopefully helpful for someone (or myself) down the line.</p>
<p>From here, we can run <code>yarn start</code> and see our application running in the browser immediately.</p>
<hr>
<aside type="info" title="Multi-part series">
<p>The following other posts are ready now. Check back soon for more!</p>
<ol>
<li><a href="/blog/2022/06/04/custom-wedding-website/">Introduction</a></li>
<li><a href="/blog/2022/06/04/custom-wedding-website-part-1/">Part 1: Choosing the tools</a></li>
<li><a href="/blog/2022/10/13/custom-wedding-website-part-2/"><strong>Part 2: Setting up the repository and frameworks</strong></a></li>
<li><em>More coming soon!</em></li>
</ol>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[New site, who dis?]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/12/new-site-who-dis/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/12/new-site-who-dis/</guid>
            <pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I needed an escape while stressing about my (previously) upcoming wedding. Having some personal annoyances with how difficult it would be to personalize Docusaurus, I sat down and hacked at Astro for a day and a half. I came up with a fully working static website &#x26; blog in no time.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/12/new-site-who-dis/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import Aside from ’../../components/Aside.astro’;
import LighthouseImage from ’../../images/blog/lighthouse.png’;</p>
<p>I needed an escape while stressing about my (previously) upcoming wedding. Having some personal annoyances with how difficult it would be to personalize <a href="https://docusaurus.io" rel="noopener" target="_blank">Docusaurus</a>, I sat down and hacked at <a href="https://astro.build" rel="noopener" target="_blank">Astro</a> for a day and a half. I came up with a fully working static website &#x26; blog in no time.</p>
<h2 id="requirements">Requirements</h2>
<p>I had just a few requirements for my site:</p>
<ul>
<li>The ability to write pages and blog posts with Markdown and MDX.
<ul>
<li>I originally used Docusaurus because I wanted to force myself to spend more time <em>writing</em> than tinkering with the page. This worked, to a point, and I’d like to keep it easy to focus on writing.</li>
</ul>
</li>
<li>The site should be served as statically built HTML pages.
<ul>
<li>There really should be no need for a big backend CMS. Just write pages and push to a source control origin.</li>
</ul>
</li>
<li>The layout should be fully customizable.
<ul>
<li>This is the kicker that is moving me away from Docusaurus. While much of the framework’s built-in components can be <em>swizzled</em> and replaced with custom versions, much of it still cannot, and I do not feel like I have the knowledge depth to contribute.</li>
</ul>
</li>
</ul>
<h2 id="options">Options</h2>
<p>I set out looking at options:</p>
<ul>
<li><strong>Docusaurus</strong> I absolutely love Docusaurus. I will continue to use it for documentation sites as the first tool I reach for. But for my personal site, it just wasn’t cutting it.</li>
<li><strong>Next.js</strong> People seem to love this. I’m still not fully convinced that the weight of the development environment and the prescriptive way of building things is best for me. I <em>have</em> been forcing myself to use it for a couple things, but it just isn’t clicking as <em>the killer</em> framework to me.</li>
<li><strong>Gatsby</strong> No. Just no. Every time I see a Gatsby site, it pulls down at least 10MB of JavaScript. Honestly, I haven’t actually tried Gatsby in a number of years, but last I did, I was turned off by the ergonomics of it. I have no idea if the anecdote about JS size is real or just a red herring.</li>
<li><strong>Eleventy</strong> Looks great, haven’t tried it. (aside: The amount of effort the <a href="https://www.11ty.dev/docs/" target="_blank" rel="noopener noreferrer">eleventy documentation</a> spends trying to convince me to use it makes me extra skeptical)</li>
<li><strong>Astro</strong> I’ve heard a lot of rave reviews. So I started with this one, just thought I was diving in.</li>
</ul>
<h2 id="accidentally-choosing-astro">Accidentally choosing Astro</h2>
<p>I started just toying with Astro for building my site, and before I knew it, within just a few hours, I was mostly done building the basic framework with something that was pleasing enough to look at and work with.</p>
<p>Sitting back for a minute on it, I decided to just run with it. So far, it has been a pleasure to work with and has actually inspired me to really get some content written. Let’s hope it continues.</p>
<h2 id="details">Details</h2>
<h3 id="framework-astro">Framework: Astro</h3>
<p>As stated above, I sort of accidentally ended up going to production with <a href="https://astro.build" rel="noopener" target="_blank">Astro</a>. I really liked how easy the plugin system is and the things the community has build:</p>
<ul>
<li><a href="https://docs.astro.build/en/guides/integrations-guide/mdx/">@astrojs/mdx</a>
<ul>
<li>While I want to write with
Markdown, it’s really hard to make things beautiful without some carefully crafted, reusable components. Being able to
import and use them directly in Markdown is mostly a necessity now.</li>
</ul>
</li>
<li><a href="https://docs.astro.build/en/guides/integrations-guide/solid-js/">@astrojs/solid</a>
<ul>
<li>See <a href="#interaction-solid-js">Interaction: Solid-js</a> for details.</li>
</ul>
</li>
<li><a href="https://docs.astro.build/en/guides/integrations-guide/tailwind/">@astrojs/tailwind</a>
<ul>
<li>See <a href="#css-tailwind-css">CSS: Tailwind
CSS</a> for details.</li>
</ul>
</li>
<li><a href="https://docs.astro.build/en/guides/integrations-guide/image/">@astrojs/image</a>
<ul>
<li>I first started out manually creating multiple variants of images with webp, avif, png, and jpeg and writing the HTML by hand. That was a mistake. This plugin does it for you.</li>
<li>It definitely increases build times, but they’re still tolerable at this time. I’d love to see if this plugin could get a mode to pre-build the image variants and add them to source control.</li>
</ul>
</li>
</ul>
<h3 id="interaction-solid-js">Interaction: Solid-js</h3>
<p>I originally thought that I wouldn’t need any interactive components, so I wasn’t going to add any React/Preact/Solid-js/etc framework.</p>
<p>But then I realized that I’d like a dark-mode theme switcher with the ability to use both automatic selection as well as user-defined preference.</p>
<p>I’ve used Solid-js for some small things before, and the small weight of the library seemed like a good choice for now. I’d like to do a comparison using Svelte, Vue, Lit, and bare web components. That’s going to be for another day, though.</p>
<h3 id="css-tailwind-css">CSS: Tailwind CSS</h3>
<p>I’ve been here since CSS was first introduced. Heck, I was here using nested <code>&#x3C;table></code> and <code>&#x3C;imagemap></code> elements. CSS is great, but if there’s one thing that has really changed the way that I write CSS since CSS-in-JS, it’s <a href="https://tailwindcss.com">Tailwind CSS</a>.</p>
<p>Mostly, it’s easy, it’s predictable, and I don’t really have to think too hard to create something beautiful. <em>Especially</em> paired with the <a href="https://tailwindcss.com/docs/typography-plugin">typography plugin</a> – making a plain markdown post easy to read has never been more simple.</p>
<h3 id="hosting">Hosting</h3>
<p>I was originally hosting my site on Github pages, until I realized that the longest cache time set for any resource is 10 minutes. I quickly changed over to Netlify today without much fuss.</p>
<p>I really can’t complain after measuring using various tools and having the home page come out super fast, performant, accessible, and search optimized.</p>
<p><img src="{LighthouseImage}" width="{1200}" itemprop="image" loading="lazy" alt="Lighthouse score for paularmstrong.dev showing a perfect score of 100 for all metrics"></p>
<h2 id="view-source">View source</h2>
<p>Go and take a look! I have kept the code for this site open source and MIT-licensed (except for blog posts, images, and page content).</p>
<aside type="danger" title="Open source">
<p>All of this site’s source code is on Github at <a href="https://github.com/paularmstrong/paularmstrong.dev">paularmstrong/paularmstrong.dev</a>.</p>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/browser.uxVO1mIp.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[DIY photobooth]]></title>
            <link>https://paularmstrong.dev/blog/2022/10/11/diy-photobooth/</link>
            <guid>https://paularmstrong.dev/blog/2022/10/11/diy-photobooth/</guid>
            <pubDate>Tue, 11 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I created my own photobooth application with Electron, Node.js, React, and Tailwindcss and used a GoPro as a webcam for my wedding instead of renting a commercial booth. It was fun to build and even more fun to use.</p>
<p><a href="https://paularmstrong.dev/blog/2022/10/11/diy-photobooth/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import Aside from ’../../components/Aside.astro’;
import YouTube from ’../../components/YouTube.astro’;
import ShackImage from ’../../images/blog/photobooth/shack.jpg’;
import Sample00Image from ’../../images/blog/photobooth/samples/2022-10-07-224047.jpg’;
import Sample01Image from ’../../images/blog/photobooth/samples/2022-10-08-234507.jpg’;
import Sample02Image from ’../../images/blog/photobooth/samples/2022-10-07-235215.jpg’;
import Sample03Image from ’../../images/blog/photobooth/samples/2022-10-08-233133.jpg’;
import Sample04Image from ’../../images/blog/photobooth/samples/2022-10-07-234849.jpg’;
import Sample05Image from ’../../images/blog/photobooth/samples/2022-10-09-023009.jpg’;</p>
<p>I recently <a href="">got married</a> – and as we know, weddings are <em>expensive</em>. Previously, I documented saving money by <a href="/blog/2022/06/04/custom-wedding-website">creating my own website</a> and foregoing spending money on paper RSVPs. We also decided to skip paying a photographer the exorbitant extra fees to also photograph our reception, but we still wanted to have lots of memories from the event.</p>
<p>I’ve always loved getting in photo booths, having a bunch of laughs, and keeping the printed photos for later. But do you know how much a photobooth rental is? At <em>least</em> $1,500! And even then, they usually print out photos instead of providing a digital version that you can actually save and use long-term.</p>
<p>Well, in keeping with my theme of not spending money, but instead spending <em>way too much time</em> over-engineering a similar solution: I set out to build my own. And I succeeded.</p>
<hr>
<h2 id="application-demo">Application demo</h2>
<p>The application, which is <a href="https://github.com/paularmstrong/photobooth">open-sourced on Github</a>, is built with <a href="https://www.electronjs.org/">Electron</a>, <a href="https://reactjs.org/">React</a>, <a href="https://tailwindcss.com">Tailwindcss</a>, and <a href="https://xstate.js.org/">x-state</a>. It uses browser APIs like <a href="https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API">MediaDevices, MediaStream, and MediaRecorder</a> to record your webcam and pass the data to an HTML canvas to create collage images, as well as video recordings.</p>
<youtube code="e6I4G3Bf0Qk" title="The Photo Booth application in action">
<p>I had a script running to watch the directory where the application saved the photos. That script looked at each new image, resized, and uploaded them directly to my Firebase storage account for immediate display in a gallery on the wedding website.</p>
<h2 id="the-memory-shack">The “Memory Shack”</h2>
<p>From a physical standpoint, the photo “booth” was renamed to the “Memory Shack”. It consisted of a borrowed portable USB-C LCD screen, a 6-button StreamDeck mini (because the screen was not a touch screen), a ring light, and a GoPro running as a webcam.</p>
<p>All of the wood used was donated from a friend and it was all pretty warped. We did the best we could with making it look nice – and it did turn out nice, if you don’t mind that it looks like an old shack in the woods. So, we renamed it to the “Memory Shack”.</p>
<p><img src="{ShackImage}" width="{1200}" itemprop="image" loading="lazy" alt="Photo “Booth” build – aka the “Memory Shack” because of how janky it looked"></p>
<p>Unfortunately, through the chaos of it all, I forgot to get a photo of the actual photobooth at the reception.</p>
<h2 id="the-result">The result</h2>
<p>It may have looked janky and silly, but this thing was a <em>hit</em>! We had props, hats, glasses, and whatever silly things we could find from costumes between ourselves and friends. And the photos turned out absolutely hilarious and fun.</p>
<div class="not-prose mb-12 grid w-full grid-cols-2 gap-4 md:grid-cols-3">
  <img class="!mt-0 w-full max-w-none -rotate-6 shadow-md dark:shadow-lg" src="{Sample00Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
  <img class="!mt-0 w-full max-w-none rotate-12 shadow-md dark:shadow-lg" src="{Sample01Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
  <img class="!mt-0 w-full max-w-none -rotate-6 shadow-md dark:shadow-lg" src="{Sample02Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
  <img class="!mt-0 w-full max-w-none rotate-6 shadow-md dark:shadow-lg" src="{Sample03Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
  <img class="!mt-0 w-full max-w-none -rotate-6 shadow-md dark:shadow-lg" src="{Sample04Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
  <img class="!mt-0 w-full max-w-none rotate-6 shadow-md dark:shadow-lg" src="{Sample05Image}" width="{320}" itemprop="image" loading="lazy" alt="Sample photo from the “Memory Shack” DIY photobooth">
</div>
<p>All-in-all, I spent <em>at least</em> 200 hours between research, prototyping, building, tweaking, and playing around with this. If I tried to cost:benefit ratio it all out with an hourly rate of my time, it probably would have been cheaper to rent a photobooth. However, after seeing the result of what I made, I think that I made the right choice by building this photobooth myself.</p>
<aside title="Stay tuned for more!" type="info">
<p>If you’re interested in the nitty gritty details, stay tuned for a new post soon (currently in progress, I swear!) that dives into some scrapped versions of the application, the choices that I made along the way, and the details of how it all works together.</p>
</aside></youtube>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/main-screen.CLz_pu1a.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Hacking express middleware to automatically capture request timeouts]]></title>
            <link>https://paularmstrong.dev/blog/2022/09/26/hacking-express-nodejs-timeout-middleware/</link>
            <guid>https://paularmstrong.dev/blog/2022/09/26/hacking-express-nodejs-timeout-middleware/</guid>
            <pubDate>Mon, 26 Sep 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>When needing to run a highly available Express server with Node.js and a lot of middleware, it’s important to ensure that you don’t have any runaway processes blocking other requests. In this post, I share an easy wrapper for Express applications to always ensure every middleware is captured in a timeout loop.</p>
<p><a href="https://paularmstrong.dev/blog/2022/09/26/hacking-express-nodejs-timeout-middleware/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>With any server, you need to ensure that you don’t have run-away requests that end up getting stuck in an infinite process, held in memory for long periods of time. Typically, the requester will abandon the request if it is taking too long, perceptually, to respond.</p>
<p>Express in a node.js server doesn’t handle this by itself. Instead you need to add something like the <a href="https://expressjs.com/en/resources/middleware/timeout.html">connect-timeout middleware</a>. However, the default example for this leaves you open to forgetting to add the timeout check within your middleware chain.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> express </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'express'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> timeout </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'connect-timeout'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> haltOnTimedout</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">req</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">next</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">req.timedout) </span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> app</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> express</span><span style="color:#E1E4E8">()</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">timeout</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">5_000</span><span style="color:#E1E4E8">))</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">bodyParser</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(haltOnTimedout)</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">cookieParser</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(haltOnTimedout)</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">myMiddleware</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(haltOnTimedout)</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">handleRequest</span><span style="color:#E1E4E8">());</span></span>
<span class="line"></span></code></pre>
<p>Notice that you need to manually add the <code>haltOnTimedout</code> middleware after <em>each</em> other middleware in the chain. This may be fine and dandy for a small personal project that you, as the sole developer, know all about. However, as you start distributing knowledge to a larger team, it’s imperative that this is easy and automatic. There are few, if any, cases where you would want to opt-out of the behavior of timing out a request if it’s taking too long.</p>
<p>So, here’s a pattern that I’ve used a number of times for various purposes – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">proxying</a> the Express application and wrapping the <code>use()</code> function for some automatic behavior. It’s as simple as wrapping the initial express application (or the application after any setup):</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> app</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> proxyTimeoutMiddleware</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">express</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">timeout</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">5_000</span><span style="color:#E1E4E8">)))</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">bodyParser</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">cookieParser</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">myMiddleware</span><span style="color:#E1E4E8">())</span></span>
<span class="line"><span style="color:#E1E4E8">  .</span><span style="color:#B392F0">use</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">handleRequest</span><span style="color:#E1E4E8">());</span></span>
<span class="line"></span></code></pre>
<h2 id="how-does-it-work">How does it work?</h2>
<p>We create a Proxy that wraps the <code>use()</code> function. This is fairly basic setup to pull out the middleware that is added to express and wrap it with another function:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> proxyTimeoutMiddleware</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">T</span><span style="color:#F97583"> extends</span><span style="color:#B392F0"> Application</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> Router</span><span style="color:#E1E4E8">>(</span><span style="color:#FFAB70">app</span><span style="color:#F97583">:</span><span style="color:#B392F0"> T</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Proxy</span><span style="color:#E1E4E8">(app, {</span></span>
<span class="line"><span style="color:#B392F0">    get</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">this</span><span style="color:#F97583">:</span><span style="color:#B392F0"> T</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">target</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">property</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">receiver</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#6A737D">      // If the target property is not express's `.use()` function, pass through to the default behavior</span></span>
<span class="line"><span style="color:#F97583">      if</span><span style="color:#E1E4E8"> (property </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> 'use'</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> Reflect.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(target, property, receiver);</span></span>
<span class="line"><span style="color:#E1E4E8">      }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">      // Otherwise, return a new function that wraps the first argument, given that it is a function</span></span>
<span class="line"><span style="color:#6A737D">      // with our timeout wrapper</span></span>
<span class="line"><span style="color:#F97583">      return</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> useWithTimeout</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">...</span><span style="color:#FFAB70">args</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">        const</span><span style="color:#79B8FF"> wrappedArgs</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> args.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">arg</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">          if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">typeof</span><span style="color:#E1E4E8"> arg </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'function'</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">            return</span><span style="color:#B392F0"> wrapMiddlewareWithTimeout</span><span style="color:#E1E4E8">(arg);</span></span>
<span class="line"><span style="color:#E1E4E8">          }</span></span>
<span class="line"><span style="color:#F97583">          return</span><span style="color:#E1E4E8"> arg;</span></span>
<span class="line"><span style="color:#E1E4E8">        });</span></span>
<span class="line"><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> Reflect.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(target, property, receiver).</span><span style="color:#B392F0">apply</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">this</span><span style="color:#E1E4E8">, wrappedArgs);</span></span>
<span class="line"><span style="color:#E1E4E8">      };</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Then, our wrapping function returns a new middleware function that checks first if the request is timed out (<code>req.timedout</code> is added via the <code>connect-timeout</code> middleware that we previously added) and returns. Upon early return, not calling <code>req.end()</code>, <code>req.send()</code> or <code>next()</code>, express will return the default 500 response.</p>
<p>Then as well, we wrap the <code>next()</code> function passed to our actual middleware and check if the request timedout <em>after</em> running the middleware and again, if so, early return. Otherwise we call the <code>next()</code> function and continue on.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> wrapMiddlewareWithTimeout</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">middleware</span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">next</span><span style="color:#F97583">:</span><span style="color:#B392F0"> NextFunction</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#79B8FF"> void</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> timeoutWrappedMiddleware</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">next</span><span style="color:#F97583">:</span><span style="color:#B392F0"> NextFunction</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">name</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> middleware;</span></span>
<span class="line"><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (req.timedout) {</span></span>
<span class="line"><span style="color:#6A737D">      // logger.error(`Request timed out before middleware "${name}".`);</span></span>
<span class="line"><span style="color:#F97583">      return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">    middleware</span><span style="color:#E1E4E8">(req, res, </span><span style="color:#F97583">function</span><span style="color:#B392F0"> wrappedNext</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">...</span><span style="color:#FFAB70">args</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">      if</span><span style="color:#E1E4E8"> (req.timedout) {</span></span>
<span class="line"><span style="color:#6A737D">        // logger.error(`Request timed out after middleware "${name}".`);</span></span>
<span class="line"><span style="color:#F97583">        return</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">      }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">      next.</span><span style="color:#B392F0">apply</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">this</span><span style="color:#E1E4E8">, args);</span></span>
<span class="line"><span style="color:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>That’s it! Once this is set up, no one will ever have to think about adding the timeout halting middleware in the future.</p>
<h2 id="other-uses">Other uses</h2>
<p>This same pattern can be used for similar before/after actions on middleware. You may have noticed a couple commented out logging lines in the <code>timeoutWrappedMiddleware</code> example previously. We could also do something similar to record the amount of time each middleware is taking in order to help us track down problem code that could be optimized:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> NANOSECONDS_PER_MILLISECOND</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 1e6</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> MILLISECONDS_PER_SECOND</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 1e3</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> wrapMiddlewareWithMetrics</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">middleware</span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">next</span><span style="color:#F97583">:</span><span style="color:#B392F0"> NextFunction</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#79B8FF"> void</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> metricsWrappedMiddleware</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">req</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Request</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Response</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">next</span><span style="color:#F97583">:</span><span style="color:#B392F0"> NextFunction</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">name</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> middleware;</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">seconds</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">nseconds</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> hrtime</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#6A737D">    // Get the high resolution time in milliseconds</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> start</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> seconds </span><span style="color:#F97583">*</span><span style="color:#79B8FF"> MILLISECONDS_PER_SECOND</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> nseconds </span><span style="color:#F97583">/</span><span style="color:#79B8FF"> NANOSECONDS_PER_MILLISECOND</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">    middleware</span><span style="color:#E1E4E8">(req, res, </span><span style="color:#F97583">function</span><span style="color:#B392F0"> wrappedNext</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">...</span><span style="color:#FFAB70">args</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">      const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">seconds</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">nseconds</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> hrtime</span><span style="color:#E1E4E8">();</span></span>
<span class="line"><span style="color:#F97583">      const</span><span style="color:#79B8FF"> end</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> seconds </span><span style="color:#F97583">*</span><span style="color:#79B8FF"> MILLISECONDS_PER_SECOND</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> nseconds </span><span style="color:#F97583">/</span><span style="color:#79B8FF"> NANOSECONDS_PER_MILLISECOND</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">      myMetrics.</span><span style="color:#B392F0">float</span><span style="color:#E1E4E8">({ metric: </span><span style="color:#9ECBFF">`middleware/${</span><span style="color:#E1E4E8">name</span><span style="color:#F97583"> ||</span><span style="color:#9ECBFF"> 'unknown'}`</span><span style="color:#E1E4E8">, value: end </span><span style="color:#F97583">-</span><span style="color:#E1E4E8"> start });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">      next.</span><span style="color:#B392F0">apply</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">this</span><span style="color:#E1E4E8">, args);</span></span>
<span class="line"><span style="color:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Mass shootings]]></title>
            <link>https://paularmstrong.dev/blog/2022/08/29/shootings/</link>
            <guid>https://paularmstrong.dev/blog/2022/08/29/shootings/</guid>
            <pubDate>Mon, 29 Aug 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>There was a shooting in my town last night.</p>
<p><a href="https://paularmstrong.dev/blog/2022/08/29/shootings/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>There was a shooting in my town last night.</p>
<p>It hurts to hear about each and every one of these. Many others probably feel the same, but there’s something different about needing to text your family and friends so they know you’re okay.</p>
<p>I can’t imagine how it feels to be on the other side waiting to get a message that your loved one is safe – only to never hear back.</p>
<p>Something must be done. It needs to be drastic. People will scream about hundred year old laws – laws that are not needed today – laws that we need the opposite of today.</p>
<p>Please: no more guns.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Creating a modern wedding website in 2022 Part 1: Choosing the tools]]></title>
            <link>https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website-part-1/</link>
            <guid>https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website-part-1/</guid>
            <pubDate>Sat, 04 Jun 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>This part of the modern wedding website series will cover tools chosen to satisfy requirements of creating a modern wedding website in 2022.</p>
<p><a href="https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website-part-1/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import Aside from ’../../components/Aside.astro’;
import Details from ’../../components/Details.astro’;</p>
<p>import FirebaseImage from ’../../images/blog/wedding/firebase.png’;
import AWSImage from ’../../images/blog/wedding/aws-amplify.png’;
import SupabaseImage from ’../../images/blog/wedding/supabase.png’;
import ParseImage from ’../../images/blog/wedding/parse.png’;
import PreactImage from ’../../images/blog/wedding/preact.webp’;
import TailwindImage from ’../../images/blog/wedding/tailwind.png’;
import ParcelImage from ’../../images/blog/wedding/parcel.png’;</p>
<aside type="info" title="Multi-part series">
<p>In the <a href="/blog/2022/06/04/custom-wedding-website/">introduction</a> to this series, we covered the rationale for building a custom wedding website, as well as the requirements that we laid out for ourselves.</p>
</aside>
<p>Now that we know <em>what</em> we need, it’s time to choose the right tools for the job. From our previously set requirements, the following items stood out as important for what we choose here:</p>
<ul>
<li>Free or <em>very cheap</em> hosting with a custom domain and SSL</li>
<li>Invitees must be able to
<ul>
<li>Sign in with their email address and no password (magic link email)</li>
<li>Receive automated emails for their invitation and RSVP confirmation</li>
</ul>
</li>
<li>All significant information must come from a protected database</li>
</ul>
<h2 id="hosting--backend">Hosting &#x26; backend</h2>
<h3 id="google-firebase">Google Firebase</h3>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{FirebaseImage}" alt="Firebase logo" width="{1024}" loading="lazy" itemprop="image">
</div>
<p>I’ve used <a href="https://firebase.google.com/">Google Firebase</a> before, but never deployed a full web application for it. It has always been a bit more tailored for native mobile apps, but I have seen that they’ve worked really hard in making the web tools top-notch.</p>
<h4>Downsides</h4>
<ul>
<li>Locking in to Google</li>
<li>Worse, Firebase is full lock-in and you can’t just replace the API with a different REST of GraphQL endpoint like you could with Amplify. Maybe you could refactor out to <a href="https://supabase.com/">Supabase</a> or another similar alternative, but your options are still very limited.</li>
<li>Despite efforts to trim bundle sizes, Firebase JavaScript packages still come in pretty hefty.</li>
</ul>
<h4>Blockers</h4>
<p>There are no blockers given the <a href="#requirements">requirements</a> that I initially laid out.</p>
<h3 id="alternatives-considered">Alternatives considered</h3>
<p>Hosting and backend framework evalutation can get long and tiring. I considered a few others otuside of Google Firebase – at least one of them I had initially thought I would end up using before considering Firebase.</p>
<details title="Click to expand alternatives considered">
<h4>AWS Amplify</h4>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{AWSImage}" alt="AWS Amplify logo" width="{1838}" loading="lazy" itemprop="image">
</div>
<p>Initially, I had hoped to be able to try out <a href="https://docs.amplify.aws/">AWS Amplify</a>. It would be an opportunity to learn something new that I hadn’t worked with before. It has a free tier and I thought it should cover everything that I wanted. Unfortunately there were roadblocks from the start.</p>
<h5>Downsides</h5>
<ul>
<li>The documentation pushed me through a web wizard. This wizard, while neat and provided a small overview to me of what might be capable, I tend to only really learn by <em>doing</em> and writing code. I did find the docs I needed eventually by searching for “AWS Amplify getting started JavaScript”.</li>
<li>“To set up the project, we’ll first create a new React app with create-react-app” — I don’t want create-react-app! Where are the instructions to do this differently?</li>
<li>Everything in their documentation kept pushing me at <code>@aws-amplify/ui-react</code>. It seemed basic enough, but wanting to get this together quickly, I didn’t want to <em>also</em> learn another UI component framework.</li>
<li>Locking in to Amazon</li>
<li>It seemed like Amplify is really just a little piece of the Amazon puzzle and I’d mostly need to string a bunch of services together, eg SES for sending emails.</li>
</ul>
<h5>Blockers</h5>
<p>Unfortunately, there were a number of blockers that came up for me during investigation and prototyping:</p>
<ul>
<li>It’s not possible to sign in with just an email address.</li>
<li>I really didn’t want to use GraphQL, even if it’s the “new hotness”. That’s just overkill for what I was looking to do.</li>
<li>REST might have been okay, but also still overkill needing to pick &#x26; choose the right tools to fetch data and manage state.</li>
</ul>
<h4>Supabase</h4>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{SupabaseImage}" alt="Supabase logo" width="{2159}" loading="lazy" itemprop="image">
</div>
<p>I would have liked to have tried <a href="https://supabase.com">Supabase</a> since it’s open-source and free. However, I didn’t dive too far into it after the two blockers that I found:</p>
<ul>
<li>Cloud functions are only experimental and very limited</li>
<li>No automated HTML email sending available</li>
</ul>
<h4>Parse Platform</h4>
<div class="not-prose max-w-[480px] rounded bg-white p-4">
  <img src="{ParseImage}" alt="Parse Platform logo" width="{378}" loading="lazy" itemprop="image">
</div>
<p>Parse is another open source platform that I looked at briefly. Unfortunately, it had too many blockers to consider even prototyping.</p>
<ul>
<li>No email sending available</li>
<li>It’s not possible to sign in with just an email address.</li>
<li>Hosting limitations and potential cost
<ul>
<li>Hosting a Parse Platform application would either require a lot of setup and cost with another provider, or using <a href="https://www.back4app.com/compare-all-plans">Back4App’s free plan</a>, which doesn’t appear to have SSL.</li>
</ul>
</li>
</ul>
</details>
<h2 id="application-frameworks--tools">Application frameworks &#x26; tools</h2>
<h3 id="preact"><a href="https://preactjs.com/">Preact</a></h3>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{PreactImage}" alt="Preact logo" width="{1900}" loading="lazy" itemprop="image">
</div>
<p>I could have used React, but I initially wanted to see how small I could keep the build size and didn’t think I’d need any of the extra weight from React for such a small site.</p>
<h3 id="tailwindcss"><a href="https://tailwindcss.com/">Tailwindcss</a></h3>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{TailwindImage}" alt="Tailwindcss logo" width="{2560}" loading="lazy" itemprop="image">
</div>
<p>I have built a few things with Tailwind before and it really makes things simple while keeping the final build sizes small.</p>
<h3 id="parcel"><a href="https://parceljs.org/">Parcel</a></h3>
<div class="not-prose max-w-[480px] rounded bg-slate-50 p-4 dark:bg-slate-200">
  <img src="{ParcelImage}" alt="Parcel logo" width="{1492}" loading="lazy" itemprop="image">
</div>
<p>I have used Parcel before for some really small projects like this and it’s pretty dead simple as a bundler to get set up. I also didn’t want to spend a week tweaking my compilation process to get things <em>just right</em> like I tend to do with Webpack.</p>
<hr>
<aside type="info" title="Multi-part series">
<p>This is the beginning of a multi-part series. The following other posts are ready now. Check back soon for more!</p>
<ol>
<li><a href="/blog/2022/06/04/custom-wedding-website/">Introduction</a></li>
<li><a href="/blog/2022/06/04/custom-wedding-website-part-1/"><strong>Part 1: Choosing the tools</strong></a></li>
<li><a href="/blog/2022/10/13/custom-wedding-website-part-2/">Part 2: Setting up the repository and frameworks</a></li>
<li><em>More coming soon!</em></li>
</ol>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Creating a modern wedding website in 2022: Introduction]]></title>
            <link>https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website/</link>
            <guid>https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website/</guid>
            <pubDate>Sat, 04 Jun 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Multi-part series documenting how to create a modern realtime RSVP-enabled wedding website using Google Firebase, React, Tailwindcss, and Parcel.</p>
<p><a href="https://paularmstrong.dev/blog/2022/06/04/custom-wedding-website/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import { Image } from ‘astro:assets’;
import Aside from ’../../components/Aside.astro’;
import ProposalImage from ’../../images/blog/wedding/proposal.jpg’;
import TheKnotImage from ’../../images/blog/wedding/the-knot-rsvp.jpg’;</p>
<p>Weddings are <em>expensive</em>. Trying to do one on a budget while following the social norms and expectations of planning and putting on a wedding just don’t really mix. After getting engaged, Kelly and I discussed what we did and <em>definitely did not</em> want when it came to our wedding.</p>
<p>One of the first things to do that we threw out was the idea of sending paper invites. Not only is it incredibly wasteful, but we had just received a wedding invitation in the mail and it just seemed expensive, tacky, and impersonal. Take this RSVP card option that comes from <em>popular wedding registry website redacted</em>:</p>
<img src="{TheKnotImage}" alt="RSVP cards from The Knot" width="{1024}" loading="lazy">
<p>Call me <em>not</em> old-fashioned, but the “M____” line makes no sense to me. After researching, I guess you’re supposed to fill in “Mr…”, “Mrs…”, or “Ms…”, but who uses that anymore? And what if you’re extra fancy and it should be “Dr…”? But really, <em>why isn’t this filled in <strong>for me</strong></em>? You should know who I am!</p>
<p>And you know what else? I’m just going to lose that if you send it to me. I’m not saying it happened… well, actually yeah it happened.</p>
<h2 id="do-something-better">Do something better</h2>
<p>So we decided we could do something better. I figure I’ve been building websites long enough that I should be able to handle it.</p>
<p>The following series will recount building a modern wedding website as a replacement for paper snail mail RSVPs.</p>
<h2 id="requirements">Requirements</h2>
<p>Before starting, we made a list of things that we needed in terms of functionality both for us and for our guests:</p>
<ul>
<li>Free or <em>very cheap</em> hosting (wedding websites like theknot are already free, afterall) with a custom domain and SSL</li>
<li>Invitees must be able to
<ul>
<li>Sign in with their email address and no password (magic link email)</li>
<li>Manage their RSVP, as well as their plus-one and/or family, directly on the site</li>
<li>View a schedule of events with maps, times, details</li>
<li>RSVP separately for optional hosted events</li>
<li>Receive automated emails for their invitation and RSVP confirmation</li>
</ul>
</li>
<li>All significant information must come from a protected database
<ul>
<li>For example, exact dates &#x26; locations should require authentication and not be compiled directly into JavaScript/HTML bundles</li>
<li>Mostly this is minor paranoia on my part to have <em>some</em> security on private information</li>
</ul>
</li>
<li>Ability to import guest/invitee information from CSV
<ul>
<li>Immediately send invitation emails upon import</li>
</ul>
</li>
<li>Ability to export RSVPs to a CSV for use in a spreadsheet (no “admin” site necessary)
<ul>
<li>Spreadsheets are much quicker for iteration and data manipulation than writing, committing, and deploying code</li>
<li>Allows my partner to have freedom and ownership</li>
</ul>
</li>
<li>Minimal maintenance overhead
<ul>
<li>I’m not going to be “on-call” for this. I just want it up and running.</li>
</ul>
</li>
</ul>
<hr>
<aside type="info" title="Multi-part series">
<p>This is the beginning of a multi-part series. The following other posts are ready now. Check back soon for more!</p>
<ol>
<li><a href="/blog/2022/06/04/custom-wedding-website/"><strong>Introduction</strong></a></li>
<li><a href="/blog/2022/06/04/custom-wedding-website-part-1/">Part 1: Choosing the tools</a></li>
<li><a href="/blog/2022/10/13/custom-wedding-website-part-2/">Part 2: Setting up the repository and frameworks</a></li>
<li><em>More coming soon!</em></li>
</ol>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/proposal.BQJ6pJXh.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Nine years sober]]></title>
            <link>https://paularmstrong.dev/blog/2022/05/17/nine-years-sober/</link>
            <guid>https://paularmstrong.dev/blog/2022/05/17/nine-years-sober/</guid>
            <pubDate>Tue, 17 May 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[<p><a href="https://paularmstrong.dev/blog/2022/05/17/nine-years-sober/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>Today marks nine full years of sobriety for me. Just a few weeks ago, my fiance asked me a question that’s been nagging her for a bit.</p>
<blockquote>
<p>What can I do to help you celebrate your milesone?</p>
</blockquote>
<p>My response surprised even me.</p>
<blockquote>
<p>Nothing. Please don’t do anything special.</p>
</blockquote>
<p>After giving it a moment of thought, the answer was clear. I don’t want any single day to be more special than the previous or the next. Every moment since the 17th of May, 2013 has been a challenge and a gift. It doesn’t matter what arbitrary measure of time we use to describe it—be it hours, days, months, or years — because I don’t want today to outshine tomorrow.</p>
<hr>
<p>I avoid talking about the journey that got me to the point of cutting alcohol out of my life. In fact, there are only a handful of people who are in my life now that were there before – even fewer that really understand where I was and why I’ve made the choices that I have.</p>
<p>The ony things that anyone reading this should take away:</p>
<ul>
<li>Every moment staying sober can be a major a milestone.</li>
<li>No one needs a reason to quit drinking.</li>
<li>No one owes you an explanation of why they quit drinking.</li>
<li>Never ask anyone why they don’t drink.</li>
</ul>
<hr>
<aside type="tip" title="Get help">
<p>Are you or someone you know struggling with addiction? The <a href="https://www.samhsa.gov/">Substance Abuse and Mental Health Services Administration</a> may be a great starting point.</p>
</aside>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Abandoning AMP]]></title>
            <link>https://paularmstrong.dev/blog/2021/11/08/abandoning-amp/</link>
            <guid>https://paularmstrong.dev/blog/2021/11/08/abandoning-amp/</guid>
            <pubDate>Mon, 08 Nov 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Resigning from the AMP Technical Steering Committee</p>
<p><a href="https://paularmstrong.dev/blog/2021/11/08/abandoning-amp/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>As of October 26<sup>th</sup>, 2021, I have resigned from the AMP Technical Steering Committee. Though no small part of it was brought on by mounting responsibilities at work and at home, I cannot in good conscience recommend AMP to my peers due to recent disclosures about Google’s advertising conduct and its positioning of AMP.</p>
<blockquote>
<p>Why not just stick to web standards and build meat and potatoes web pages that can work with whatever ad exchange you need?</p>
<p>Because Google can also simultaneously exert pressure onto publishers to use AMP to increase likelihood of search ranking performance.</p>
<p>— Lívia Labate (@livlab) <a href="https://twitter.com/livlab/status/1452445878636224514">October 25, 2021</a></p>
</blockquote>
<h2 id="history">History</h2>
<p>Communication from Google to the public has always been that AMP was not given direct favoritism on their platform in any way. In order to help that perception and ensure that the project continued to mature and be a great option to build on, Google set out to relinquish direct control over the entire project.</p>
<p>First, a Technical Steering Committee (TSC), an Advisory Committee (AC), and a set of working groups were formed.</p>
<aside type="note" title="Groups formed">
<h4 id="tsc"><a href="https://github.com/ampproject/meta-tsc">TSC</a></h4>
<blockquote>
<p>We are the group of people who set AMP’s technical &#x26; product direction.</p>
</blockquote>
<p><em>My take: I liked to think of the TSC as a type of “board of directors”.</em></p>
<h4 id="advisory-committee"><a href="https://github.com/ampproject/meta-ac">Advisory Committee</a></h4>
<blockquote>
<p>The AMP Advisory Committee (AC) provides perspective and advice to the Technical Steering Committee (TSC).</p>
</blockquote>
<p><em>My take: this is middle management.</em></p>
<h4 id="working-groups"><a href="https://github.com/ampproject/meta/tree/a2870e0437370fcf0f807a9dd8b0da102edf8d55/working-groups">Working Groups</a></h4>
<blockquote>
<p>An AMP Working Group is a segment of the community with knowledge/interest in specific area of AMP. Working Groups are created by AMP’s Technical Steering Committee.</p>
</blockquote>
<p><em>My take: Project/product managers and people that actually did work.</em></p>
</aside>
<p>The initial goal of the TSC was to have a group of diverse people with less than half of the members representing Google. But instead, we ended up with 50% of members being Google employees who were directly affiliated with AMP internally. The rest was made up as individual people from Twitter (me), Pantheon, Microsoft, and Pinterest.</p>
<p>In a positive light, I think the best thing that we helped facilitate as the TSC was the moving copyright and IP to the <a href="https://openjsf.org/blog/2019/10/10/openjs-foundation-welcomes-amp-project-to-help-improve-user-experience-on-the-web/">OpenJS Foundation</a>.</p>
<h2 id="failing-core-priorities">Failing core priorities</h2>
<p>Outside of that, I’m honestly not sure what we accomplished, if anything, other than signing off on the hard work that the rest of the Advisory Committe and Working Groups were doing.</p>
<p>At AMP Conf in Tokyo, 2019, we promised we would make the TSC a diverse group. We failed at that. For about 1 year we had a member that was not male. Outside of that, we all looked alike and exactly who you would expect to be on a sort of “board of directors” (white men).</p>
<p>I’m sorry that I didn’t do more to continue to push on the issue. I knew it was a problem from the first time we were all on a video call together, saw it worse when we were all physically on stage at AMP Conf, and was fired up to do something about it. And then it fell off of my, and everyone else’s, priorities. Next time I’m in a similar situation, I promise to do better.</p>
<h2 id="what-else-did-we-fail-on">What else did we fail on?</h2>
<ul>
<li>
<p>Signed Exchanges (SXG)</p>
<ul>
<li><a href="https://twitter.com/DavidStrauss">David Strauss</a>, also since resigned from the TSC, worked very hard to help get this moving forward. He has a lot of great insight into this and I’ll avoid speaking for him. Hopefully he will share his opinions publicly soon.</li>
</ul>
</li>
<li>
<p>Keeping AMP relevant</p>
<ul>
<li>
<p>Google/Chrome’s pushing of <a href="https://web.dev/vitals/">Core Web Vitals</a> has given everyone clearer targets to hit. They’ve also proved that <a href="https://twitter.com/addyosmani/status/1456316562609254406">they’re achievable</a>. Already we are seeing many companies and groups ditching AMP because other tools give them more freedom while still being able to deliver fast experiences.</p>
<blockquote>
<p>Is performance on the web getting better? The answer is yes :) 33% of origins now meet the Core Web Vitals thresholds! Lots of improvements from sites using a framework too. <img src="https://pbs.twimg.com/media/FDVHloyUcAAq3SX?format=jpg&#x26;name=4096x4096" alt="">
– Addy Osmani (@addyosmani) <a href="https://twitter.com/addyosmani/status/1456316562609254406">November 4, 2021</a></p>
</blockquote>
</li>
</ul>
</li>
<li>
<p>Changing the perception that Google isn’t the controller of AMP</p>
<ul>
<li>I don’t think this could have changed without getting a vast majority of contributions to the AMP codebase coming from the community instead of Google employees.</li>
<li>Google employs great developers whose sole priorities seem to be to work on AMP</li>
<li>Only one of the design reviews that I attended for AMP projects did I see a proposal come from a non-Google employee</li>
</ul>
</li>
</ul>
<p>These last two points, along with <a href="/blog/open-source-break">other mounting responsibilities</a>, had been making me wonder if I really cared about AMP anymore. Honestly, I’d never actually used it–despite believing in it and believing that it really was/is a technologically amazing framework.</p>
<blockquote>
<p>The dirtiest trick AMP pulled was to get really smart, earnest, well spoken developers to work on it and advocate for it.</p>
<p>— Chris Coyier (@chriscoyier) <a href="https://twitter.com/chriscoyier/status/1452448918935990272">October 25, 2021</a></p>
</blockquote>
<p>The original goals of the project are sound and something to admire. From a tech standpoint, I believe that AMP accomplished some amazing feats. A lot of incredible, smart, and thoughtful people put some seriously hard work into it and it shows. It’s performant, it’s fast, and it’s robust. That’s what we needed.</p>
<p>But that’s not all we got.</p>
<h2 id="the-worst-parts">The worst parts</h2>
<p>A recent civil complaint (<a href="https://storage.courtlistener.com/recap/gov.uscourts.nysd.564903/gov.uscourts.nysd.564903.152.0.pdf">1:21-md-03010-PKC</a>) gives us some troubling detail into how Google positioned AMP for publishers as a necessity and pulled a fast one on winning heaps of advertising business by hindering header bidding.</p>
<p>Honestly, I can’t do justice summarizing all of it. Please read the relevant sections of the complaint, <a href="https://storage.courtlistener.com/recap/gov.uscourts.nysd.564903/gov.uscourts.nysd.564903.152.0.pdf">Page 89–92, sections 245–252</a> (it’s relaly not that much to read). At the very least, check out this part:</p>
<blockquote>
<ol start="247">
<li>Google ad server employees met with AMP employees to strategize about using AMP
to impede header bidding, addressing in particular how much pressure publishers and advertisers
would tolerate. First, Google restricted the AMP code to prohibit publishers from routing their bids
to, or sharing their user data with, more than a few exchanges a time, thereby severely limiting
AMP’s compatibility with header bidding. However, Google made AMP fully compatible with
routing to exchanges through Google’s ad server. Google also designed AMP to force publishers
to route rival exchange bids through Google’s ad server so that Google could continue to peek at
their bids and trade on inside information. Third, Google designed AMP so that users loading AMP
pages would directly communicate with Google cache servers rather than publishers’ servers. This
enabled Google’s access to publishers’ inside and non-public user data. AMP pages also limit the
number of ads on a page, the types of ads publishers can sell, and the variety of enriched content
that publishers can have on their pages.</li>
</ol>
</blockquote>
<h2 id="resigning">Resigning</h2>
<p>I sent my resignation to the rest of the TSC on 2021-10-26. Within hours, another member replied in announcement of their own resignation.</p>
<p>I do very much appreciate having had the privilege to try to help AMP and its community of developers &#x26; users. But at the same time, I noted in my resignation that “I no longer feel comfortable recommending AMP to colleagues both out of necessity and fears of recently filed complaints against Google.”</p>
<p>While I understand that Google employees likely aren’t allowed to respond to anything regarding the legal complaint, it’s been more than two weeks and I haven’t gotten any sort of formal acknowledgement of my resignation or personal note of any kind.</p>
<p>Though it does appear that <a href="https://github.com/ampproject/meta-tsc/commit/4ec9ca95f01be481ff976e8e78461857c2be3224">I was removed from the list of members</a> the following week.</p>
<p>So, short to say, I’m <em>disappointed</em> all around.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Taking a break from open source]]></title>
            <link>https://paularmstrong.dev/blog/2021/10/24/2021-10-24-open-source-break/</link>
            <guid>https://paularmstrong.dev/blog/2021/10/24/2021-10-24-open-source-break/</guid>
            <pubDate>Sun, 24 Oct 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I’ve been sitting on this decision for a long time. It’s not easy to abandon something I have poured a lot of thought, care, time, and effort into. It’s almost like quitting a job… except I never got paid for the job. Open source was, is, and will continue to be the gateway for my entire skillset. Without it, I don’t think I would have ever learned how write software, let alone ever have become a software developer.</p>
<p><a href="https://paularmstrong.dev/blog/2021/10/24/2021-10-24-open-source-break/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>I’ve been sitting on this decision for a long time. It’s not easy to abandon something I have poured a lot of thought, care, time, and effort into. It’s almost like quitting a job… except I never got paid for the job.</p>
<aside type="info" title="tl;dr">
<p>Want to avoid the blah blah sob story? Just go here: <a href="https://github.com/paularmstrong/normalizr/discussions/493">Maintainer help wanted</a></p>
</aside>
<p>Open source was, is, and will continue to be the gateway for my entire skillset. Without it, I don’t think I would have ever learned how write software, let alone ever have become a software developer.</p>
<h2 id="some-history">Some history</h2>
<p>Years ago, before React, Angular, Vue, all of the great frameworks that we’re <em>currently</em> using, we (maybe just me?) obsessed over templating engines. Some were good, some were bad, and some were <a href="https://jinja.palletsprojects.com/en/3.0.x/">Jinja</a>. I absolutely loved Jinja. It’s flexibility, composition, and inheritance made it amazing for building out complex web sites.</p>
<p>Maybe more correct, I fell in love with the PHP version of Jinja, <a href="https://twig.symfony.com/">Twig</a>. At the time, I was working for a small company with a massive PHP website–a massive <em>spaghetti-code</em> PHP website. SQL queries were embedded in the top of PHP pages, security holes, and every bad practice you could imagine existed in its codebase. There wasn’t a single hint that a skilled “frontend developer” had ever worked there. <em>(Fun side note: one did for a hot minute before I joined. Apparently they rage-quit within a month because of how bad everything was.)</em></p>
<p>We went full-gas implementing an MVC for our PHP website. Fixing the security holes, getting those SQL queries shored up, and most importantly (to me) setting up a reusable templating system with Twig. I loved it. The template engine was easy to learn, extend, reuse, and teach.</p>
<p>Then I got more and more into node.js and I wanted a project to help me learn more. I noticed there wasn’t a good templating engine with the features that Twig and Jinja offered. So in came <a href="https://github.com/paularmstrong/swig">Swig</a>. Initially it was nearly compatible with Twig. It actually took off quite a bit.</p>
<p>It had nearly 100% code coverage, was used by quite a few real companies selling real products. I felt like I was <em>making it</em> as an open source developer.</p>
<p>Except I wasn’t. Other than Swig’s own documentation, I never actually got to use Swig for anything. It was my favorite thing I’d created, and I never got to enjoy it.</p>
<p>But that’s not why I abandoned it.</p>
<p>I abandoned it because I wasn’t able to create a healthy community around it. It felt like every notification I got from Github was someone filing an issue or requesting another feature. But barely single pull request was ever there. I tried for a time to keep up fixing and implementing to keep the issues clear. But it became too much. I tried to help others do it themselves, but I would get silence in return. It was <em>exhausting</em> and <em>unrewarding</em>.</p>
<p>With a heavy heart, I opened an issue looking for maintainers. I updated the README to point to it. I left it be for a few months and no one stepped up.</p>
<p>So I closed the issues, stopped accepting any PRs, and marked the repository as archived and unmaintained.</p>
<h2 id="backlash">Backlash</h2>
<p>People were unhappy, to say the least. Some were mad directly <em>at me</em>. How could I do this?</p>
<p>And I’ve seen a few posts, polls, and discussions lately about open source maintainers and long-term maintenance and viability. A good number of people have rational responses, but there are still some that try to argue that as an open source maintainer, it is <em>your duty</em> to continue maintaining what you’ve written. For free. Your time, your effort, your life.</p>
<p>And therein lies the point of this post:</p>
<p>I’m afraid of the backlash. I shouldn’t be, I know that. And I know that there’s a silent majority that understand and will just go on with their lives. But I also know that it’s disheartening when there’s something that you use and rely on that suddenly looks like it’s going to disappear.</p>
<p>Anyway, that’s my little moment of vulnerability. This little post has taken me almost an entire day to write. I haven’t even scratched the surface of my fears around asking for help and relinquishing control of a project. But know that I have it–and there’s a lot of it.</p>
<h2 id="whats-happening">What’s happening</h2>
<p>So why am I writing all of this? Well, because I just don’t have the time right now–and haven’t for most of this year–and doubt I will magically create more hours in the day any time soon. And I don’t want to screw up, disappoint, and lose trust like I did last time.</p>
<p>I’m going through my open source repositories and deciding which need maintainers. Not just contributors, but people able to admin the repository and publish to NPM as well.</p>
<p>This time around, I really don’t want to just shut anything down, archive it, and tell people to fork it themselves. I’d like to see what I’ve written continue to thrive, become better, and be useful tools.</p>
<p>First up, <a href="https://github.com/paularmstrong/normalizr/discussions/493">Normalizr</a>. I haven’t been active with Normalizr for years. It’s not a tool I’ve needed to reach for and I believe it’s pretty stable. I do fear, however, that I may be a little disconnected from the needs of the actual users.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Safe Express.js HTML responses without a templating engine]]></title>
            <link>https://paularmstrong.dev/blog/2020/12/22/javascript-html-templating/</link>
            <guid>https://paularmstrong.dev/blog/2020/12/22/javascript-html-templating/</guid>
            <pubDate>Tue, 22 Dec 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Safely template HTML responses from Node.js and Express.js servers using JavaScript’s built-in templating functions, not giant libraries.</p>
<p><a href="https://paularmstrong.dev/blog/2020/12/22/javascript-html-templating/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;</p>
<p>There are too many choices for server-side rendering HTML pages. Avoiding the heavy hitter frameworks like React, that require a lot more work and will drain server resources quickly, you may find yourself reaching for a templating engine that can dynamically help you build up HTML responses for your JavaScript server.</p>
<p>Having written <a href="https://github.com/paularmstrong/swig">a templating engine</a><sup><a href="#user-content-fn-swig" id="user-content-fnref-swig" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>, I know that many of them include features that make them really attractive to many users. However, you may really not need one at all.</p>
<p>The biggest feature that templating engines tend to provide is safety from Cross-Site Scripting (XSS). Let’s look at a very simplistic example. Let’s say you want to greet a person by name by using some user-submitted information. To make it easy, let’s grab that value from the query param <code>?name</code> from the URL. Fire this snippet up with Node.js:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> express</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'express'</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> app</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> express</span><span style="color:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/'</span><span style="color:#E1E4E8">, (</span><span style="color:#FFAB70">req</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">res</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">  res.</span><span style="color:#B392F0">send</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`&#x3C;p>Hello, ${</span><span style="color:#E1E4E8">req</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">query</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}!&#x3C;/p>`</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E1E4E8">app.</span><span style="color:#B392F0">listen</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">3000</span><span style="color:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>
<p>When I run the above and visit <code>http://localhost:3000/?name=Paul</code>, I am greeted with <code>Hello, Paul!</code>. Awesome! But there’s just one problem. What if someone shares a link that looks like this: <code>http://localhost:3000/?name=%3Cscript%3Ealert(%27got%20you%27);%3C/script%3E</code>?</p>
<p>You might see the problem without even pasting that in your browser. The server will directly inject a <code>&#x3C;script></code> and allow the “attacker” to execute any JavaScript on the page. While this example doesn’t do anything bad other than alert on the page, more complicated attacks could load scripts from somewhere else<sup><a href="#user-content-fn-csp" id="user-content-fnref-csp" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>. This kind of attack is known as <a href="https://owasp.org/www-community/attacks/xss/">Cross Site Scripting</a> (or XSS for short).</p>
<h2 id="templating-engines">Templating engines</h2>
<p>So, with XSS in mind, many templating engines have, by default, protected against that by making their default variable insertion escape the output unless the developer marks it as safe.</p>
<h3 id="handlebars">Handlebars</h3>
<p><a href="https://handlebarsjs.com/guide/#html-escaping">Handlebars</a> output syntax <code>{{expression}}</code> is html-escaped, while <code>{{{expression}}}</code> (three curly-braces) is not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="handlebars"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>Hello, </span><span style="color:#79B8FF">{{</span><span style="color:#FFAB70">name</span><span style="color:#79B8FF">}}</span><span style="color:#E1E4E8">!&#x3C;/</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<h3 id="ejs">EJS</h3>
<p><a href="https://ejs.co/#features">EJS</a> ouput syntax <code>&#x3C;%=</code> is html-escaped, while <code>&#x3C;%-</code> is not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>Hello, </span><span style="color:#FDAEB7;font-style:italic">&#x3C;</span><span style="color:#E1E4E8">%= name %>!&#x3C;/</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<h3 id="swig">Swig</h3>
<p>Swig ouput syntax <code>{{ variable }}</code> is html-escaped, while <code>{{ variable|raw }}</code> is not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="handlebars"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>Hello, </span><span style="color:#79B8FF">{{</span><span style="color:#FFAB70">variable</span><span style="color:#79B8FF">}}</span><span style="color:#E1E4E8">!&#x3C;/</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<h3 id="marko">Marko</h3>
<p><a href="https://markojs.com/docs/syntax/">Marko</a> ouput syntax <code>${variable}</code> is html-escaped, while <code>$!{variable}</code> is not.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="marko"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span><span style="color:#9ECBFF">Hello, </span><span style="color:#E1E4E8">${escaped}</span><span style="color:#9ECBFF">!</span><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<hr>
<p>Of all of the above examples, we notice that automatic HTML-escaping is the default and recommended syntax for each language. And that’s really awesome! But again, you might be able to do this much quicker and simpler.</p>
<h2 id="moving-away-from-template-engines">Moving away from template engines</h2>
<p>Originally, at Twitter, we had written part of our server-side HTML rendering using Handlebars. At the time (2014/15), it was the best choice. It had all of the general features you’d want: automatic HTML escaping was a big piece of that.</p>
<p>We had <em>also</em> looked into JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates">tagged templates</a>. However, we weren’t quite transitioned to Node 4.0.0 (released 2015-09-08) and tagged templates weren’t available to us.</p>
<p>Fast-forward 5 years and I had been auditing a bunch of our server rendering code. What really stuck out to me was that we were using Handlebars and it was largely unchanged. We didn’t use any of the more complex features and the indirection was causing a few issues with figuring out where code was between the logic of our middleware and the actual rendering of the HTML itself.</p>
<p>Tagged template functions are really cool and they’re pretty simple themselves, just with a funny syntax (that you may recognize from implementations of things like <a href="https://styled-components.com/">Styled Components</a>).</p>
<p>Let’s implement a simple tagged template function first, to get the feel for it.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> html</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">stringParts</span><span style="color:#F97583">:</span><span style="color:#B392F0"> TemplateStringsArray</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#FFAB70">values</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">unknown</span><span style="color:#E1E4E8">>) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#9ECBFF"> `${</span><span style="color:#E1E4E8">stringParts</span><span style="color:#9ECBFF">[</span><span style="color:#79B8FF">0</span><span style="color:#9ECBFF">]</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">stringParts</span><span style="color:#9ECBFF">[</span><span style="color:#79B8FF">1</span><span style="color:#9ECBFF">]</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> name</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Paul'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">html</span><span style="color:#9ECBFF">`Hello, ${</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}!`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Hello, Paul!</span></span>
<span class="line"><span></span></span></code></pre>
<p>In the above, you can see the two parts that make up a tagged template function:</p>
<ul>
<li><code>stringParts</code> is an array of strings. Think of these as each part of the tagged string split up by each variable <code>${...}</code>. If you don’t have any variables, the length will be 1. For 1 variable, the length will be two; 2 variables will be length 3, and so on.</li>
<li><code>name</code> (and any further arguments) are the variables that you are inserting into the <code>stringParts</code></li>
</ul>
<p>This works well, but we’d like it to accept any number of inserted variables. To do that, we can spread the arguments and write a reduce the stringParts, merging them with each value in order:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> html</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">stringParts</span><span style="color:#F97583">:</span><span style="color:#B392F0"> TemplateStringsArray</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#FFAB70">values</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">unknown</span><span style="color:#E1E4E8">>) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> stringParts.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">accumulator</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">part</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">i</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> value</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> values[i] </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#9ECBFF"> `${</span><span style="color:#E1E4E8">accumulator</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">part</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">value</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> personOne</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Jane'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> personTwo</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Jim'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">html</span><span style="color:#9ECBFF">`Hello, ${</span><span style="color:#E1E4E8">personOne</span><span style="color:#9ECBFF">} and ${</span><span style="color:#E1E4E8">personTwo</span><span style="color:#9ECBFF">}!`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Hello, Jane and Jim!</span></span>
<span class="line"><span></span></span></code></pre>
<h2 id="strings-only">Strings only</h2>
<p>We still haven’t fixed our big security hole, though! Starting with the most basic, we can escape each value using the <a href="https://github.com/component/escape-html">escape-html</a> package when we add it to the accumulator. This time, you can see that attempting to write a <code>&#x3C;script></code> tag to the output ends up getting escaped, exactly as we would like:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> escape </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'escape-html'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> html</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">stringParts</span><span style="color:#F97583">:</span><span style="color:#B392F0"> TemplateStringsArray</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#FFAB70">values</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">unknown</span><span style="color:#E1E4E8">>) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> stringParts.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">accumulator</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">part</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">i</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> value</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> values[i] </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#9ECBFF"> `${</span><span style="color:#E1E4E8">accumulator</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">part</span><span style="color:#9ECBFF">}${</span><span style="color:#B392F0">escape</span><span style="color:#9ECBFF">(</span><span style="color:#E1E4E8">value</span><span style="color:#9ECBFF">)</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> name</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> '&#x3C;script>alert("hello");&#x3C;/script>'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">html</span><span style="color:#9ECBFF">`Hello, ${</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}!`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Hello, %3Cscript%3Ealert%28%22hello%22%29%3B%3C/script%3E!;</span></span>
<span class="line"><span></span></span></code></pre>
<h2 id="a-fully-featured-result">A fully-featured result</h2>
<p>This works really well, is simple and fast. For completeness, we can take it a couple of steps further and ensure that any <code>value</code> can be <code>mixed</code> type (JSON, Date, etc):</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> escape </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'escape-html'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> html</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">stringParts</span><span style="color:#F97583">:</span><span style="color:#B392F0"> TemplateStringsArray</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">...</span><span style="color:#FFAB70">values</span><span style="color:#F97583">:</span><span style="color:#B392F0"> Array</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">unknown</span><span style="color:#E1E4E8">>) {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> stringParts.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">accumulator</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">part</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">i</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    const</span><span style="color:#79B8FF"> value</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> values[i] </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">    let</span><span style="color:#E1E4E8"> escapedValue </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6A737D">    // If we have a string, just escape it</span></span>
<span class="line"><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">typeof</span><span style="color:#E1E4E8"> value </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'string'</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#E1E4E8">      escapedValue </span><span style="color:#F97583">=</span><span style="color:#B392F0"> escape</span><span style="color:#E1E4E8">(value);</span></span>
<span class="line"><span style="color:#6A737D">      // Arrays and plain JavaScript objects can be JSON stringified with a custom function to ensure each individual value gets escaped</span></span>
<span class="line"><span style="color:#E1E4E8">    } </span><span style="color:#F97583">else</span><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (value </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> (Array.</span><span style="color:#B392F0">isArray</span><span style="color:#E1E4E8">(value) </span><span style="color:#F97583">||</span><span style="color:#79B8FF"> Object</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">prototype</span><span style="color:#E1E4E8">.toString.</span><span style="color:#B392F0">call</span><span style="color:#E1E4E8">(value) </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> '[object Object]'</span><span style="color:#E1E4E8">)) {</span></span>
<span class="line"><span style="color:#E1E4E8">      escapedValue </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">(value, (</span><span style="color:#FFAB70">key</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">value</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">typeof</span><span style="color:#E1E4E8"> value </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'string'</span><span style="color:#F97583"> ?</span><span style="color:#B392F0"> escape</span><span style="color:#E1E4E8">(value) </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> value));</span></span>
<span class="line"><span style="color:#6A737D">      // If we're dealing with a class of some other type, try to use the `toString` method</span></span>
<span class="line"><span style="color:#E1E4E8">    } </span><span style="color:#F97583">else</span><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#9ECBFF">'toString'</span><span style="color:#F97583"> in</span><span style="color:#E1E4E8"> Reflect.</span><span style="color:#B392F0">ownKeys</span><span style="color:#E1E4E8">(value)) {</span></span>
<span class="line"><span style="color:#E1E4E8">      escapedValue </span><span style="color:#F97583">=</span><span style="color:#B392F0"> escape</span><span style="color:#E1E4E8">(value.</span><span style="color:#B392F0">toString</span><span style="color:#E1E4E8">());</span></span>
<span class="line"><span style="color:#6A737D">      // otherwise, just escape the String value</span></span>
<span class="line"><span style="color:#E1E4E8">    } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">      escapedValue </span><span style="color:#F97583">=</span><span style="color:#B392F0"> escape</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">String</span><span style="color:#E1E4E8">(value));</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#9ECBFF"> `${</span><span style="color:#E1E4E8">accumulator</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">part</span><span style="color:#9ECBFF">}${</span><span style="color:#E1E4E8">escapedValue</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">  }, </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> name</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'Paul'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> description</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> '&#x3C;script>alert("hello");&#x3C;/script>'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> jsonContent</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> { content: </span><span style="color:#9ECBFF">'&#x3C;script>alert("hello");&#x3C;/script>'</span><span style="color:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">html</span><span style="color:#9ECBFF">`&#x3C;h1>Hello, ${</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">}!&#x3C;/h1></span></span>
<span class="line"><span style="color:#9ECBFF">  &#x3C;p>${</span><span style="color:#E1E4E8">description</span><span style="color:#9ECBFF">}&#x3C;/p></span></span>
<span class="line"><span style="color:#9ECBFF">  &#x3C;script></span></span>
<span class="line"><span style="color:#9ECBFF">    window.DATA = ${</span><span style="color:#E1E4E8">jsonContent</span><span style="color:#9ECBFF">};</span></span>
<span class="line"><span style="color:#9ECBFF">  &#x3C;/script>`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>&#x3C;h1>Hello, Paul!&#x3C;/h1></span></span>
<span class="line"><span>&#x3C;p>%3Cscript%3Ealert%28%22hello%22%29%3B%3C/script%3E&#x3C;/p></span></span>
<span class="line"><span>&#x3C;script></span></span>
<span class="line"><span>  window.DATA = {"content":"%3Cscript%3Ealert%28%22hello%22%29%3B%3C/script%3E"};</span></span>
<span class="line"><span>&#x3C;/script></span></span>
<span class="line"><span></span></span></code></pre>
<p>And that’s it! That’s the full implementation<sup><a href="#user-content-fn-bugs" id="user-content-fnref-bugs" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup> of a Tagged Template function for JavaScript-based servers that allows you to output XSS-safe. Any HTML within the <code>stringParts</code> content will be written verbatim, while all variables injected will automatically be escaped.</p>
<aside type="tip">
<p>There are a few ways to take this a step further by implementing a way to mark strings as “not-escapable”. You will likely need that ability, but I’ll leave that up to you to decide how to implement.</p>
</aside>
<section data-footnotes="" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-swig">
<p>Swig has been unmaintained for years. Please don’t use it. <a href="#user-content-fnref-swig" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-csp">
<p>Content Security Policies (CSP) could protect us against this particular case, but it’s just
one example to illustrate the point. <a href="#user-content-fnref-csp" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-bugs">
<p>If you find any bugs or have suggestions, please <a href="https://github.com/paularmstrong/paularmstrong.dev/edit/main/src/content/blog/2020-12-22-javascript-html-templating.mdx">submit a PR on GitHub</a>. <a href="#user-content-fnref-bugs" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Rubber Ducky Twitch Bot]]></title>
            <link>https://paularmstrong.dev/blog/2020/11/16/ducky/</link>
            <guid>https://paularmstrong.dev/blog/2020/11/16/ducky/</guid>
            <pubDate>Mon, 16 Nov 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I was having too much fun playing with Twitch and wanted viewers to be able to help me with “rubber duck debugging”.</p>
<p><a href="https://paularmstrong.dev/blog/2020/11/16/ducky/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import YouTube from ’../../components/YouTube.astro’;</p>
<p>It’s not easy trying to stream code development and sit around talking to yourself for hours on end. I keep finding myself needing a rubber duck to explain things to while no one is active in the chat.</p>
<p>In comes the Rubber Ducky chat bot. Check out the code on <a href="https://github.com/paularmstrong/rubber_duck_bot">GitHub at paularmstrong/rubber_duck_bot</a>.</p>
<youtube code="at3bGmRHu-c" title="Rubber ducky bot for Twitch"></youtube>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Git helpers that I can no longer live without]]></title>
            <link>https://paularmstrong.dev/blog/2020/05/15/git-helpers/</link>
            <guid>https://paularmstrong.dev/blog/2020/05/15/git-helpers/</guid>
            <pubDate>Fri, 15 May 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Working with git can be hard, verbose, and repetitive. I’ve been using this small collection of sh/zsh functions for a few years now and I don’t think I could work without them anymore.</p>
<p><a href="https://paularmstrong.dev/blog/2020/05/15/git-helpers/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>Working with git can be hard, verbose, and repetitive. I’ve been using this small collection of sh/zsh functions for a few years now and I don’t think I could work without them anymore.</p>
<ul>
<li><code>main</code> - quickly jump back to the default/<code>HEAD</code> branch of your repo without needing to know what the name of that branch is.</li>
<li><code>rebase</code> - rebase your current branch against the current state of the default/<code>HEAD</code> branch as it is on the remote server.</li>
<li><code>rmbranch</code> - delete your current working branch and move to the default/<code>HEAD</code> branch. Great for PR cleanup!</li>
</ul>
<h2 id="main"><code>main</code></h2>
<p>The industry seems to be agreeing that <code>main</code> is a much better default than the outdated term <code>master</code>. Unfortunately, there are still some repos that I’ve needed to work with that use other names.</p>
<p>This function can help remove the guessing game if you work with a bunch of repos that are still transitioning and/or use different names for the default/<code>HEAD</code> branch and you want to quickly get there from your current branch:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> main</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#E1E4E8">	DEFAULT_BRANCH</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">`</span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> remote show origin </span><span style="color:#F97583">|</span><span style="color:#B392F0"> grep</span><span style="color:#9ECBFF"> "HEAD branch" </span><span style="color:#F97583">|</span><span style="color:#B392F0"> sed</span><span style="color:#9ECBFF"> 's/.*: //'`</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> checkout</span><span style="color:#E1E4E8"> $DEFAULT_BRANCH</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> pull</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Usage:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#B392F0">main</span></span>
<span class="line"></span></code></pre>
<h2 id="rmbranch"><code>rmbranch</code></h2>
<p>I’m often in a position where the current branch I’m on had its PR was completed, merged, and branch removed from the remote. I dislike keeping old copies of branches around that I don’t need and I want them cleaned up immediately.</p>
<p>This function deletes the current branch and moves to the default tracked branch, avoiding doing the whold dance yourself. Bonus: it does the same thing as the previous <code>main</code> function and figures out which default/<code>HEAD</code> branch to switch to.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> rmbranch</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#E1E4E8">	BRANCH</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">`</span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> rev-parse </span><span style="color:#79B8FF">--abbrev-ref</span><span style="color:#9ECBFF"> HEAD`</span></span>
<span class="line"><span style="color:#E1E4E8">	DEFAULT_BRANCH</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">`</span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> remote show origin </span><span style="color:#F97583">|</span><span style="color:#B392F0"> grep</span><span style="color:#9ECBFF"> "HEAD branch" </span><span style="color:#F97583">|</span><span style="color:#B392F0"> sed</span><span style="color:#9ECBFF"> 's/.*: //'`</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> checkout</span><span style="color:#E1E4E8"> $DEFAULT_BRANCH</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> branch</span><span style="color:#79B8FF"> -D</span><span style="color:#E1E4E8"> $BRANCH</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> pull</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Usage:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#B392F0">rmbranch</span></span>
<span class="line"></span></code></pre>
<h2 id="rebase"><code>rebase</code></h2>
<p>Super helpful for workplaces that prefer flat commits and don’t allow merge commits – or you just find it easier to rebase. Quickly fetches your default branch and rebases on top of the latest changes.</p>
<p>Again, this figures out the default/<code>HEAD</code> branch automatically so you don’t have to think about it.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> rebase</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#E1E4E8">	DEFAULT_BRANCH</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">`</span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> remote show origin </span><span style="color:#F97583">|</span><span style="color:#B392F0"> grep</span><span style="color:#9ECBFF"> "HEAD branch" </span><span style="color:#F97583">|</span><span style="color:#B392F0"> sed</span><span style="color:#9ECBFF"> 's/.*: //'`</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> fetch</span><span style="color:#9ECBFF"> origin</span><span style="color:#E1E4E8"> $DEFAULT_BRANCH</span></span>
<span class="line"><span style="color:#B392F0">	git</span><span style="color:#9ECBFF"> rebase</span><span style="color:#9ECBFF"> origin/</span><span style="color:#E1E4E8">$DEFAULT_BRANCH</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Usage:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="sh"><code><span class="line"><span style="color:#B392F0">rebase</span></span>
<span class="line"></span></code></pre>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
        </item>
        <item>
            <title><![CDATA[Twitter Lite and High Performance React Progressive Web Apps at Scale]]></title>
            <link>https://paularmstrong.dev/blog/2017/04/11/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale/</link>
            <guid>https://paularmstrong.dev/blog/2017/04/11/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale/</guid>
            <pubDate>Tue, 11 Apr 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>A look into removing common and uncommon performance bottlenecks in one of the worlds largest React.js PWAs, Twitter Lite.</p>
<p><a href="https://paularmstrong.dev/blog/2017/04/11/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale/">Continue reading…</a></p>]]></description>
            <content:encoded><![CDATA[<p>import Aside from ’../../components/Aside.astro’;
import { Image } from ‘astro:assets’;
import OptimizeBeforeImage from ’../../images/blog/twitter-lite/optimizing-before.png’;
import OptimizeAfterImage from ’../../images/blog/twitter-lite/optimizing-after.png’;
import LighthouseImage from ’../../images/blog/twitter-lite/lighthouse.png’;
import JankBeforeImage from ’../../images/blog/twitter-lite/jank-before.png’;
import JankAfterImage from ’../../images/blog/twitter-lite/jank-after.png’;
import RasterBeforeImage from ’../../images/blog/twitter-lite/raster-before.png’;
import RasterAfterImage from ’../../images/blog/twitter-lite/raster-after.png’;
import LikingImage from ’../../images/blog/twitter-lite/liking.gif’;
import UpdateBeforeImage from ’../../images/blog/twitter-lite/update-before.png’;
import UpdateAfterImage from ’../../images/blog/twitter-lite/update-after.png’;
import DeferBeforeImage from ’../../images/blog/twitter-lite/defer-before.png’;
import DeferAfterImage from ’../../images/blog/twitter-lite/defer-after.png’;
import DangerousBeforeImage from ’../../images/blog/twitter-lite/dangerous-before.png’;
import DangerousAfterImage from ’../../images/blog/twitter-lite/dangerous-after.png’;
import DeferRenderBeforeImage from ’../../images/blog/twitter-lite/defer-render-before.gif’;
import DeferRenderAfterImage from ’../../images/blog/twitter-lite/defer-render-after.gif’;
import StoreBeforeImage from ’../../images/blog/twitter-lite/store-state-before.png’;
import StoreAfterImage from ’../../images/blog/twitter-lite/store-state-after.png’;
import BatchBeforeImage from ’../../images/blog/twitter-lite/batching-before.png’;
import BatchAfterImage from ’../../images/blog/twitter-lite/batching-after.png’;
import PrecacheBeforeImage from ’../../images/blog/twitter-lite/precache-before.png’;
import PrecacheAfterImage from ’../../images/blog/twitter-lite/precache-after.png’;
import SWBeforeImage from ’../../images/blog/twitter-lite/sw-register-before.png’;
import SWAfterImage from ’../../images/blog/twitter-lite/sw-register-after.png’;</p>
<p>A look into removing common and uncommon performance bottlenecks in one of the worlds largest React.js PWAs, Twitter Lite.</p>
<p>Creating a fast web application involves many cycles of measuring where time is wasted, understanding why it’s happening, and applying potential solutions. Unfortunately, there’s never just one quick fix. Performance is a continuous game of watching and measuring for areas to improve. With Twitter Lite, we made small improvements across many areas: from initial load times, to React component rendering (and prevention re-rendering), to image loading, and much more. Most changes tend to be small, but they add up, and the end result is that we have one of the largest and fastest <a href="https://developers.google.com/web/progressive-web-apps/">progressive web applications</a>.</p>
<h2 id="before-reading-on">Before Reading On:</h2>
<aside type="warning" title="Historic document">
<p>This post was original published to <a href="https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3">Medium</a>, when I wasn’t publishing my old site.</p>
<p>I have preserved it here for historical purposes. Please keep in mind that while React has changed greatly since this was first published in 2017, some of the ideas and techniques are still valuable lessons and worth remembering for future performance deep dives.</p>
</aside>
<p>If you’re just starting to measure and work toward increasing the performance of your web application, I highly recommend <a href="http://www.brendangregg.com/flamegraphs.html">learning how to read flame graphs</a>, if you don’t know how already.</p>
<p>Each section below includes example screenshots of timeline recordings from Chrome’s Developer Tools. To make things more clear, I’ve highlighted each pair of examples with what’s bad versus what’s good.</p>
<aside type="tip" title="Special note regarding timelines and flame graphs">
<p>Since we target a very large range of mobile devices, we typically record these in a simulated environment: 5x slower CPU and 3G network connection. This is not only more realistic, but makes problems much more apparent. These may also be further skewed if we’re using <a href="https://facebook.github.io/react/blog/2016/11/16/react-v15.4.0.html#profiling-components-with-chrome-timeline">React v15.4.0’s component profiling</a>. Actual values on desktop performance timelines will tend to be much faster than what’s illustrated here.</p>
</aside>
<hr>
<h2 id="optimizing-for-the-browser">Optimizing for the Browser</h2>
<h3 id="use-route-based-code-splitting">Use Route-Based Code Splitting</h3>
<p>Webpack is powerful but difficult to learn. For some time, we had issues with the <a href="https://webpack.js.org/plugins/commons-chunk-plugin/">CommonsChunkPlugin</a> and the way it worked with some of our circular code dependencies. Because of that, we ended up with only 3 JavaScript asset files, totaling over 1MB in size (420KB gzip transfer size).</p>
<p>Loading a single, or even just a few very large JavaScript files in order to run a site is a huge bottleneck for mobile users to see and interact with a website. Not only does the amount of time it takes for your scripts to transfer over a network increase with their size, but the time it takes for the browser to parse increases as well.</p>
<p>After much wrangling, we were finally able to break up common areas by routes into separate chunks (example below). The day finally came when this code review dropped into our inboxes:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> plugins</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#6A737D">  // extract vendor and webpack's module manifest</span></span>
<span class="line"><span style="color:#F97583">  new</span><span style="color:#E1E4E8"> webpack.optimize.</span><span style="color:#B392F0">CommonsChunkPlugin</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">    names: [</span><span style="color:#9ECBFF">'vendor'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'manifest'</span><span style="color:#E1E4E8">],</span></span>
<span class="line"><span style="color:#E1E4E8">    minChunks: </span><span style="color:#79B8FF">Infinity</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">  }),</span></span>
<span class="line"><span style="color:#6A737D">  // extract common modules from all the chunks (requires no 'name' property)</span></span>
<span class="line"><span style="color:#F97583">  new</span><span style="color:#E1E4E8"> webpack.optimize.</span><span style="color:#B392F0">CommonsChunkPlugin</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">    async: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    children: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    minChunks: </span><span style="color:#79B8FF">4</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">  }),</span></span>
<span class="line"><span style="color:#E1E4E8">];</span></span>
<span class="line"></span></code></pre>
<blockquote>
<p>Adds granular, route-based code-splitting. Faster initial and HomeTimeline render is traded for greater overall app size, which is spread over 40 on-demand chunks and amortized over the length of the session. — <a href="https://twitter.com/necolas">Nicolas Gallagher</a></p>
</blockquote>
<img src="{OptimizeBeforeImage}" alt="Network timeline before code splitting" width="{1280}">
<p>Our original setup (above) took over 5 seconds to load our main bundle, while after splitting out code by routes and common chunks (below), it takes barely 3 seconds (on a simulated 3G network).</p>
<img src="{OptimizeAfterImage}" alt="Network timeline after code splitting" width="{1280}">
<p>This was done early on in our performance focus sprints, but this single change made a huge difference when running Google’s <a href="https://developers.google.com/web/tools/lighthouse/">Lighthouse</a> web application auditing tool:</p>
<div class="bustout">
  <img src="{LighthouseImage}" alt="Lighthouse report before and after" width="{1280}">
</div>
<h3 id="avoid-functions-that-cause-jank">Avoid Functions that Cause Jank</h3>
<p>Over many iterations of our <a href="http://itsze.ro/blog/2017/04/09/infinite-list-and-react.html">infinite scrolling timelines</a>, we used different ways to calculate your scroll position and direction to determine if we needed to ask the API for more Tweets to display. Up until recently, we were using <a href="https://github.com/brigade/react-waypoint">react-waypoint</a>, which worked well for us. However, in chasing the best possible performance for one of the main underlying components of our application, it just wasn’t fast enough.</p>
<p>Waypoints work by calculating many different heights, widths, and positions of elements in order to determine your current scroll position, how far from each end you are, and which direction you’re going. All of this information is useful, but since it’s done on every scroll event it comes at a cost: making those calculations causes jank–and lots of it.</p>
<p>But first, we have to understand what the developer tools mean when they tell us that there is “jank”.</p>
<aside type="tip" title="Jank">
<blockquote>
<p>Most devices today refresh their screens 60 times a second. If there’s an animation or transition running, or the user is scrolling the pages, the browser needs to match the device’s refresh rate and put up 1 new picture, or frame, for each of those screen refreshes.</p>
<p>Each of those frames has a budget of just over 16ms (1 second / 60 = 16.66ms). In reality, however, the browser has housekeeping work to do, so all of your work needs to be completed inside 10ms. When you fail to meet this budget the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience. — <a href="https://developers.google.com/web/fundamentals/performance/rendering/">Paul Lewis on Rendering Performance</a></p>
</blockquote>
</aside>
<p>Over time, we developed a new infinite scrolling component called <em>VirtualScroller</em>. With this new component, we know exactly what slice of Tweets are being rendered into a timeline at any given time, avoiding the need to make expensive calculations as to where we are visually.</p>
<div class="bustout-sm">
  <img src="{JankBeforeImage}" alt="Jank before virtual scrolling" width="{1280}">
</div>
<p>It may not look like much, but before (above) while scrolling, we would cause render jank by trying to calculate the height of various elements. After (below) we cause no jank and reduce the stuttering while scrolling timelines at fast speeds.</p>
<div class="bustout-sm">
  <img src="{JankAfterImage}" alt="Jank after virtual scrolling" width="{1280}">
</div>
<p>By avoiding function calls that cause extra jank, scrolling a timeline of Tweets looks and feels more seamless, giving us a much more rich, almost native experience. While it can always be better, this change makes a noticeable improvement to the smoothness of scrolling timelines. It was a good reminder that every little bit counts when looking at performance.</p>
<h3 id="use-smaller-images">Use Smaller Images</h3>
<p>We first started pushing to use less bandwidth on Twitter Lite by working with multiple teams to get new and smaller sizes of images available from our <a href="https://en.wikipedia.org/wiki/Content_delivery_network">CDNs</a>. It turns out, that by reducing the size of the images we were rendering to be only what we absolutely needed (both in terms of dimensions and quality), we found that not only did we reduce bandwidth usage, but that we were also able to increase performance in the browser, especially while scrolling through image-heavy timelines of Tweets.</p>
<p>In order to determine how much better smaller images are for performance, we can look at the <em>Raster</em> timeline in Chrome Developer Tools. Before we reduced the size of images, it could take 300ms or more just to decode a single image, as shown in the timeline recording below. This is the processing time after an image has been downloaded, but before it can be displayed on the page.</p>
<p>When you’re scrolling a page and aiming for the 60 frame-per-second rendering standard, we want to keep as much processing as possible under 16.667ms (1 frame). It’s taking us nearly 18 frames just to get a single image rendered into the viewport, which is too many. One other thing to note in the timeline: you can see that the <em>Main</em> timeline is mostly blocked from continuing until this image has finished decoding (as shown by the whitespace). This means we’ve got quite a performance bottleneck here!</p>
<div class="bustout-sm">
  <img src="{RasterBeforeImage}" alt="Large image rasterizing" width="{1280}">
</div>
<p>Large images (above) can block the main thread from continuing for 18 frames. Small images (below) take only about 1 frame.</p>
<div class="bustout-sm">
  <img src="{RasterAfterImage}" alt="Small image rasterizing" width="{1280}">
</div>
<p>Now, after we’ve reduced the size of our images (above), we’re looking at just over a single frame to decode our largest images.</p>
<h2 id="optimizing-react">Optimizing React</h2>
<h3 id="make-use-of-the-shouldcomponentupdate-method">Make use of the <code>shouldComponentUpdate</code> method</h3>
<p>A common tip for optimizing the performance of React applications is to <a href="https://facebook.github.io/react/docs/optimizing-performance.html#shouldcomponentupdate-in-action">use the <code>shouldComponentUpdate</code> method</a>. We try to do this wherever possible, but sometimes things slip through the cracks.</p>
<figure class="flex flex-col items-center">
<img src="{LikingImage}" alt="Updates when liking a tweet" width="{251}">
<figcaption>Liking the first Tweet caused both it and the entire Conversation below it to re-render!</figcaption>
</figure>
<p>Here’s an example of a component that was always updating: When clicking the heart icon to like a Tweet in the home timeline, any <code>Conversation</code> component on screen would also re-render. In the animated example, you should see green boxes highlighting where the browser has to re-paint because we’re making the entire <code>Conversation</code> component below the Tweet we’re acting on update.</p>
<p>Below, you’ll see two flame graphs of this action. Without <code>shouldComponentUpdate</code>, we can see its entire tree updated and re-rendered, just to change the color of a heart somewhere else on the screen. After adding <code>shouldComponentUpdate</code> (second image below), we prevent the entire tree from updating and prevent wasting more than one-tenth of a second running unnecessary processing.</p>
<div class="bustout-sm">
  <img src="{UpdateBeforeImage}" alt="Unnecessary updates in performance timelines" width="{1280}">
  <img src="{UpdateAfterImage}" alt="No more unnecessary updates in performance timelines" width="{1280}">
</div>
<h3 id="defer-unnecessary-work-until-componentdidmount">Defer Unnecessary Work until <code>componentDidMount</code></h3>
<p>This change may seem like a bit of a no-brainer, but it’s easy to forget about the little things when developing a large application like Twitter Lite.</p>
<p>We found that we had a lot of places in our code where we were doing expensive calculations for the sake of analytics during the <a href="https://facebook.github.io/react/docs/react-component.html#componentwillmount"><code>componentWillMount</code> React lifecycle method</a>. Every time we did this, we blocked rendering of components a little more. 20ms here, 90ms there, it all adds up quickly. Originally, we were trying to record which tweets were being rendered to our data analytics service in <code>componentWillMount</code>, before they were actually rendered (below).</p>
<img src="{DeferBeforeImage}" alt="componentWillMount timeline" width="{1280}">
<p>By moving that calculation and network call to the React component’s componentDidMount method, we unblocked the main thread and reduced unwanted jank when rendering our components (below).</p>
<img src="{DeferAfterImage}" alt="componentDigMount timeline" width="{1280}">
<h3 id="avoid-dangerouslysetinnerhtml">Avoid <code>dangerouslySetInnerHTML</code></h3>
<p>In Twitter Lite, we use SVG icons, as they’re the most portable and scalable option available to us. Unfortunately, in older versions of React, most SVG attributes were not supported when creating elements from components. So, when we first started writing the application, we were forced to use <code>dangerouslySetInnerHTML</code> in order to use SVG icons as React components.</p>
<p>For example, our original HeartIcon looked something like this:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#B392F0"> HeartIcon</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">props</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span></span>
<span class="line"><span style="color:#E1E4E8">  React.</span><span style="color:#B392F0">createElement</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'svg'</span><span style="color:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#F97583">    ...</span><span style="color:#E1E4E8">props,</span></span>
<span class="line"><span style="color:#E1E4E8">    dangerouslySetInnerHTML: {</span></span>
<span class="line"><span style="color:#E1E4E8">      __html:</span></span>
<span class="line"><span style="color:#9ECBFF">        '&#x3C;g>&#x3C;path d="M38.723 12c-7.187 0-11.16 7.306-11.723 8.131C26.437 19.306 22.504 12 15.277 12 8.791 12 3.533 18.163 3.533 24.647 3.533 39.964 21.891 55.907 27 56c5.109-.093 23.467-16.036 23.467-31.353C50.467 18.163 45.209 12 38.723 12z">&#x3C;/path>&#x3C;/g>'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">    viewBox: </span><span style="color:#9ECBFF">'0 0 54 72'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">  });</span></span>
<span class="line"></span></code></pre>
<p>Not only is it <a href="http://reactjs.cn/react/tips/dangerously-set-inner-html.html">discouraged to use <code>dangerouslySetInnerHTML</code></a>, but it turns out that it’s actually a source of slowness when mounting and rendering.</p>
<img src="{DangerousBeforeImage}" alt="dangerouslySetInnerHTML timeline" width="{1280}">
<p>Before (above), you’ll see it takes roughly 20ms to mount 4 SVG icons, while after (below) it takes around 8.</p>
<img src="{DangerousAfterImage}" alt="dangerouslySetInnerHTML timeline" width="{1280}">
<p>Analyzing the flame graphs above, our original code (above) shows that it takes about 20ms on a slow device to mount the actions at the bottom of a Tweet containing four SVG icons. While this may not seem like much on its own, knowing that we need to render many of these at once, all while scrolling a timeline of infinite Tweets, we realized that this is a huge waste of time.</p>
<p>Since React v15 added support for most SVG attributes, we went ahead and looked to see what would happen if we avoided <code>dangerouslySetInnerHTML</code>. Checking the patched flame graph (above right), we get about an average of <strong>60% savings</strong> each time we need to mount and render one of these sets of icons!</p>
<p>Now, our SVG icons are simple stateless components, don’t use “dangerous” functions, and mount an average of 60% faster. They look like this:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#B392F0"> HeartIcon</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">props</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">	&#x3C;</span><span style="color:#85E89D">svg</span><span style="color:#E1E4E8"> {</span><span style="color:#F97583">...</span><span style="color:#E1E4E8">props} </span><span style="color:#B392F0">viewBox</span><span style="color:#FDAEB7;font-style:italic">=`0</span><span style="color:#FDAEB7;font-style:italic"> 0</span><span style="color:#FDAEB7;font-style:italic"> ${width}</span><span style="color:#FDAEB7;font-style:italic"> ${height}`></span></span>
<span class="line"><span style="color:#FDAEB7;font-style:italic">		&#x3C;g></span></span>
<span class="line"><span style="color:#FDAEB7;font-style:italic">			&#x3C;path</span><span style="color:#B392F0"> d</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"M38.723 12c-7.187 0-11.16 7.306-11.723 8.131C26.437 19.306 22.504 12 15.277 12 8.791 12 3.533 18.163 3.533 24.647 3.533 39.964 21.891 55.907 27 56c5.109-.093 23.467-16.036 23.467-31.353C50.467 18.163 45.209 12 38.723 12z"</span><span style="color:#E1E4E8">>&#x3C;/</span><span style="color:#85E89D">path</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#F97583">		&#x3C;/</span><span style="color:#E1E4E8">g</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#F97583">	&#x3C;/</span><span style="color:#E1E4E8">svg</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>
<h3 id="defer-rendering-when-mounting--unmounting-many-components">Defer Rendering When Mounting &#x26; Unmounting Many Components</h3>
<p>On slower devices, we noticed that it could take a long time for our main navigation bar to appear to respond to taps, often leading us to tap multiple times, thinking that perhaps the first tap didn’t register.</p>
<p>Notice in the image below how the Home icon takes nearly 2 seconds to update and show that it was tapped:</p>
<figure class="flex flex-col items-center">
  <img src="{DeferRenderBeforeImage}" alt="without deferred rendering" width="{252}">
  <figcaption>Without deferring rendering, the navigation bar takes time to respond.</figcaption>
</figure>
<p>No, that wasn’t just the GIF running a slow frame rate. It actually was that slow. But, all of the data for the Home screen was already loaded, so why is it taking so long to show anything?
It turns out that mounting and unmounting large trees of components (like timelines of Tweets) is very expensive in React.</p>
<p>At the very least, we wanted to remove the perception of the navigation bar not reacting to user input. For that, we created a small higher-order-component:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> hoistStatics </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'hoist-non-react-statics'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> React </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react'</span><span style="color:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D">/**</span></span>
<span class="line"><span style="color:#6A737D"> * Allows two animation frames to complete to allow other components to update</span></span>
<span class="line"><span style="color:#6A737D"> * and re-render before mounting and rendering an expensive `WrappedComponent`.</span></span>
<span class="line"><span style="color:#6A737D"> */</span></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> deferComponentRender</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">WrappedComponent</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">  class</span><span style="color:#B392F0"> DeferredRenderWrapper</span><span style="color:#F97583"> extends</span><span style="color:#B392F0"> React</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">Component</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#F97583">    constructor</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">props</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">context</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#79B8FF">      super</span><span style="color:#E1E4E8">(props, context);</span></span>
<span class="line"><span style="color:#79B8FF">      this</span><span style="color:#E1E4E8">.state </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> { shouldRender: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8"> };</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">    componentDidMount</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#E1E4E8">      window.</span><span style="color:#B392F0">requestAnimationFrame</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E1E4E8">        window.</span><span style="color:#B392F0">requestAnimationFrame</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#79B8FF"> this</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">setState</span><span style="color:#E1E4E8">({ shouldRender: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8"> }));</span></span>
<span class="line"><span style="color:#E1E4E8">      });</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">    render</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">      return</span><span style="color:#79B8FF"> this</span><span style="color:#E1E4E8">.state.shouldRender </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#79B8FF">WrappedComponent</span><span style="color:#E1E4E8"> {</span><span style="color:#F97583">...</span><span style="color:#79B8FF">this</span><span style="color:#E1E4E8">.props} /> </span><span style="color:#F97583">:</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#B392F0"> hoistStatics</span><span style="color:#E1E4E8">(DeferredRenderWrapper, WrappedComponent);</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<p>Once applied to our HomeTimeline, we saw near-instant responses of the navigation bar, leading to a perceived improvement overall.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> DeferredTimeline</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> deferComponentRender</span><span style="color:#E1E4E8">(HomeTimeline);</span></span>
<span class="line"><span style="color:#B392F0">render</span><span style="color:#E1E4E8">(&#x3C;</span><span style="color:#79B8FF">DeferredTimeline</span><span style="color:#E1E4E8"> />);</span></span>
<span class="line"></span></code></pre>
<figure class="flex flex-col items-center">
  <img src="{DeferRenderAfterImage}" alt="with deferred rendering" width="{252}">
  <figcaption>After deferring rendering, the navigation bar responds instantly.</figcaption>
</figure>
<h2 id="optimizing-redux">Optimizing Redux</h2>
<h3 id="avoid-storing-state-too-often">Avoid Storing State Too Often</h3>
<p>While <a href="https://facebook.github.io/react/docs/forms.html#controlled-components">controlled components</a> seem to be the recommended approach, making inputs controlled means that they have to update and re-render for every keypress.</p>
<p>While this is not very taxing on a 3GHz desktop computer, a small mobile device with very limited CPU will notice significant lag while typing–especially when deleting many characters from the input.</p>
<p>In order to persist the value of composing Tweets, as well as calculating the number of characters remaining, we were using a controlled component and <em>also</em> passing the current value of the input to our Redux state at each keypress.</p>
<div class="bustout-sm">
  <img src="{StoreBeforeImage}" alt="store state timeline" width="{1280}">
</div>
<p>Above, on a typical Android 5 device, every keypress leading to a change could cause nearly 200ms of overhead. Compound this by a fast typist, and we ended up in a really bad state, with users often reporting that their character insertion point was moving all over the place, resulting in jumbled sentences.</p>
<div class="bustout-sm">
  <img src="{StoreAfterImage}" alt="store state timeline" width="{1280}">
</div>
<p>By removing the draft Tweet state from updating the main Redux state on every keypress and keeping things local in the React component’s state, we were able to reduce the overhead by over 50% (above).</p>
<h3 id="batch-actions-into-a-single-dispatch">Batch Actions into a Single Dispatch</h3>
<p>In Twitter Lite, we’re using <a href="http://redux.js.org/">redux</a> with (react-redux)[<a href="http://redux.js.org/docs/basics/UsageWithReact.html">http://redux.js.org/docs/basics/UsageWithReact.html</a>] to subscribe our components to data state changes. We’ve optimized our data into separate areas of a larger store with <a href="https://github.com/paularmstrong/normalizr">Normalizr</a> and <a href="http://redux.js.org/docs/api/combineReducers.html">combineReducers</a>. This all works wonderfully to prevent duplication of data and keep our stores small. However, each time we get new data, we have to dispatch multiple actions in order to add it to the appropriate stores.</p>
<p>With the way that react-redux works, this means that every action dispatched will cause our connected components (called Containers) to recalculate changes and possibly re-render.</p>
<p>While we use a custom middleware, there are <a href="https://www.npmjs.com/package/redux-batched-actions">other</a> <a href="https://www.npmjs.com/package/redux-batch-middleware">batch</a> <a href="https://www.npmjs.com/package/redux-batch-enhancer">middleware</a> <a href="https://www.npmjs.com/package/redux-batch-actions">available</a>. Choose the one that’s right for you, or write your own.</p>
<p>The best way to illustrate the benefits of batching actions is by using the <a href="https://github.com/crysislinux/chrome-react-perf">Chrome React Perf Extension</a>. After the initial load, we pre-cache and calculate unread DMs in the background. When that happens we add a lot of various entities (conversations, users, message entries, etc). Without <em>batching</em> (first image below), you can see that we end up with double the number of times we render each component (~16) versus with batching (~8) (second image below).</p>
<img src="{BatchBeforeImage}" alt="without batching performance trace" width="{1280}">
<img src="{BatchBeforeImage}" alt="with batching performance trace" width="{1280}">
<h2 id="service-workers">Service Workers</h2>
<p>While Service Workers aren’t available in all browsers yet, they’re an invaluable part of Twitter Lite. When available, we use ours for push notifications, to pre-cache application assets, and more. Unfortunately, being a fairly new technology, there’s still a lot to learn around performance.</p>
<h3 id="pre-cache-assets">Pre-Cache Assets</h3>
<p>Like most products, Twitter Lite is by no means done. We’re still actively developing it, adding features, fixing bugs, and making it faster. This means we frequently need to deploy new versions of our JavaScript assets.</p>
<p>Unfortunately, this can be a burden when users come back to the application and need to re-download a bunch of script files just to view a single Tweet.</p>
<p>In ServiceWorker-enabled browsers, we get the benefit of being able to have the worker automatically update, download, and cache any changed files in the background, on its own, before you come back.</p>
<p>So what does this mean for the user? Near instant subsequent application loads, even after we’ve deployed a new version!</p>
<img src="{PrecacheBeforeImage}" alt="without pre-caching assets" width="{1280}">
<p>As illustrated (above) without ServiceWorker pre-caching, every asset for the current view is forced to load from the network when returning to the application. It takes about 6 seconds on a good 3G network to finish loading. However, when the assets are pre-cached by the ServiceWorker (below), the same 3G network takes less than 1.5 seconds before the page is finished loading. A 75% improvement!</p>
<img src="{PrecacheAfterImage}" alt="with pre-caching assets" width="{1280}">
<h3 id="delay-serviceworker-registration">Delay ServiceWorker Registration</h3>
<p>In many applications, it’s safe to register a ServiceWorker immediately on page load:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">>window.navigator.serviceWorker.register('/sw.js');&#x3C;/</span><span style="color:#85E89D">script</span><span style="color:#E1E4E8">></span></span>
<span class="line"></span></code></pre>
<p>While we try to send as much data to the browser as possible to render a complete-looking page, in Twitter Lite this isn’t always possible. We may not have sent enough data, or the page you’re landing on may not support data pre-filled from the server. Because of this and various other limitations, we need to make some API requests immediately after the initial page load.</p>
<p>Normally, this isn’t a problem. However, if the browser hasn’t installed the current version of our ServiceWorker yet, we need to tell it to install–and with that comes about 50 requests to pre-cache various JS, CSS, and image assets.</p>
<p>When we were using the simple approach of registering our ServiceWorker immediately, we could see the network contention happening within the browser, maxing out our parallel request limit (below).</p>
<img src="{SWBeforeImage}" alt="immediately registering the service worker" width="{1280}">
<p>Notice how when registering your service worker immediately, it can block all other network requests (above). Deferring the service worker (below) allows your initial page load to make required network requests without getting blocked by the concurrent connection limit of the browser.</p>
<img src="{SWAfterImage}" alt="delaying registering the service worker" width="{1280}">
<p>By delaying the ServiceWorker registration until we’ve finished loading extra API requests, CSS and image assets, we allow the page to finish rendering and be responsive, as illustrated in the after screenshot (above).</p>
<hr>
<p>Overall, this is a list of just some of the many improvements that we’ve made over time to Twitter Lite. There are certainly more to come and we hope to continue sharing the problems we find and the ways that we work to overcome them. For more about what’s going on in real-time and more React &#x26; PWA insights, follow <a href="https://mobile.twitter.com/paularmstrong">me</a> and the team on Twitter.</p>]]></content:encoded>
            <author>me@paularmstrong.dev (Paul Armstrong)</author>
            <enclosure url="https://paularmstrong.dev/_astro/banner.B1hUu2HO.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>