<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://alxndr.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://alxndr.blog/" rel="alternate" type="text/html" /><updated>2025-12-23T20:03:48+00:00</updated><id>https://alxndr.blog/feed.xml</id><title type="html">Alexander’s blog</title><subtitle>mostly notes to myself</subtitle><author><name>Alexander</name></author><entry><title type="html">mocking in Vitest: how to conditionally modify a mocked imported function’s behavior</title><link href="https://alxndr.blog/2024/11/20/vitest-mocks-conditional-modifications.html" rel="alternate" type="text/html" title="mocking in Vitest: how to conditionally modify a mocked imported function’s behavior" /><published>2024-11-20T00:00:00+00:00</published><updated>2024-11-20T00:00:00+00:00</updated><id>https://alxndr.blog/2024/11/20/vitest-mocks-conditional-modifications</id><content type="html" xml:base="https://alxndr.blog/2024/11/20/vitest-mocks-conditional-modifications.html"><![CDATA[<p>(This was not apparent at all despite an hour or two looking through <a href="https://vitest.dev">Vitest</a>’s docs…)</p>

<h3 id="tldr">tldr</h3>

<p><a href="https://stackoverflow.com/a/76432956/303896">On StackOverflow</a>, Ben Butterworth succintly solved this Vitest predicament in an answer to a question about Jest.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// a-local-file.js -----------------------------------------------</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">dependency</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./deps</span><span class="dl">'</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">aFunction</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">dependency</span><span class="p">()</span>
    <span class="p">},</span> <span class="mi">2000</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// test.js -------------------------------------------------------</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">test</span><span class="p">,</span> <span class="nx">vi</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vitest</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">aFunction</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./a-local-file</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">dependency</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./deps</span><span class="dl">'</span>

<span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="dl">'</span><span class="s1">./deps</span><span class="dl">'</span><span class="p">)</span>

<span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">when it returns true</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">dependency</span><span class="p">).</span><span class="nx">mockResolvedValue</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="nx">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">...</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="cm">/* ... */</span><span class="p">})</span>
<span class="p">})</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">when it returns false</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">dependency</span><span class="p">).</span><span class="nx">mockResolvedValue</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="nx">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">...</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="cm">/* ... */</span><span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>

<h3 id="why">Why??</h3>

<p>I was adding tests to a proof-of-concept TypeScript function running on AWS Lambda.
The main function is in the file <code class="language-plaintext highlighter-rouge">index.ts</code>.</p>

<p>It uses <code class="language-plaintext highlighter-rouge">import</code> to pull in a helper <code class="language-plaintext highlighter-rouge">login</code> function, in the file <code class="language-plaintext highlighter-rouge">login-helper.ts</code>.</p>

<p>I wanted to test the behavior of the <code class="language-plaintext highlighter-rouge">index.ts</code> function when the login fails, as well as when it works.</p>

<p>With this ability to conditionally mock the extracted login function, I can have one <code class="language-plaintext highlighter-rouge">describe</code> block where I test the failing-login behavior…</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">describe</span><span class="p">,</span> <span class="nx">test</span><span class="p">,</span> <span class="nx">vi</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vitest</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">login</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./login-helper</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">handlePayload</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./index</span><span class="dl">'</span>
<span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="dl">'</span><span class="s1">./login-helper</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">handlePayload</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// ...</span>
  <span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">with invalid login</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">login</span><span class="p">).</span><span class="nx">mockRejectedValue</span><span class="p">(</span><span class="dl">'</span><span class="s1">mocked login fails</span><span class="dl">'</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="nx">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">throws</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">expect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">handlePayload</span><span class="p">(</span><span class="nx">payload</span><span class="p">)).</span><span class="nx">rejects</span><span class="p">.</span><span class="nx">toThrow</span><span class="p">(</span><span class="dl">'</span><span class="s1">mocked login fails</span><span class="dl">'</span><span class="p">)</span>
    <span class="p">})</span>
  <span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>

<p>…as well as the “happy path” logic when the login succeeds…</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">describe</span><span class="p">,</span> <span class="nx">test</span><span class="p">,</span> <span class="nx">vi</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vitest</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">login</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./login-helper</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">handlePayload</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./index</span><span class="dl">'</span>
<span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="dl">'</span><span class="s1">./login-helper</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">handlePayload</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// ...</span>
  <span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">with valid login</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">let</span> <span class="nx">mockedPost</span>
    <span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">mockedPost</span> <span class="o">=</span> <span class="nx">vi</span><span class="p">.</span><span class="nx">fn</span><span class="p">()</span>
      <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">login</span><span class="p">).</span><span class="nx">mockResolvedValue</span><span class="p">({</span>
        <span class="na">post</span><span class="p">:</span> <span class="nx">mockedPost</span><span class="p">,</span>
      <span class="p">})</span>
    <span class="p">})</span>
    <span class="nx">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">attempts to post</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">handlePayload</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
      <span class="nx">expect</span><span class="p">(</span><span class="nx">mockedPost</span><span class="p">).</span><span class="nx">toHaveBeenCalledWith</span><span class="p">({</span><span class="cm">/* ... */</span><span class="p">})</span>
    <span class="p">})</span>
  <span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>]]></content><author><name>Alexander</name></author><category term="howto" /><category term="javascript" /><category term="code" /><category term="testing" /><category term="vitest" /><summary type="html"><![CDATA[(This was not apparent at all despite an hour or two looking through Vitest’s docs…)]]></summary></entry><entry><title type="html">Cypress component testing with Svelte v5 (and SvelteKit)</title><link href="https://alxndr.blog/2024/10/09/svelte-v5-cypress-component-testing.html" rel="alternate" type="text/html" title="Cypress component testing with Svelte v5 (and SvelteKit)" /><published>2024-10-09T00:00:00+00:00</published><updated>2024-10-09T00:00:00+00:00</updated><id>https://alxndr.blog/2024/10/09/svelte-v5-cypress-component-testing</id><content type="html" xml:base="https://alxndr.blog/2024/10/09/svelte-v5-cypress-component-testing.html"><![CDATA[<p>Version 5 of <a href="https://svelte.dev">Svelte</a> makes some dramatic changes in its API.
<a href="https://cypress.io">Cypress</a> component testing <a href="https://github.com/cypress-io/cypress/issues/23618">already doesn’t play nicely with SvelteKit</a>, and Svelte v5 doesn’t make things better.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>We detected that you have versions of dependencies that are not officially supported:

 - `svelte`. Expected ^3.0.0 || ^4.0.0, found 5.0.0-next.262.

If you're experiencing problems, downgrade dependencies and restart Cypress.
</code></pre></div></div>

<p><strong>To set the stage, here’s the problem I saw:</strong> a headless run would throw an error and hang (first it would show some <code class="language-plaintext highlighter-rouge">&lt;svelte:component&gt;</code> deprecation warnings and then spit out <code class="language-plaintext highlighter-rouge">SvelteKitError: Not found: /__cypress/src/index.html</code>), and a headed run would show “Your tests are loading…” and then nothing else would happen.</p>

<p>First, <a href="https://github.com/cypress-io/cypress/issues/26064#issuecomment-1475437226">as recommended by GitHub user @lmiller1990</a>, I added this <code class="language-plaintext highlighter-rouge">viteConfig</code> function to my <code class="language-plaintext highlighter-rouge">cypress.config.ts</code>’s <code class="language-plaintext highlighter-rouge">component.devServer</code> so that Cypress’s Vite server will know that we’re bootstrapping a Svelte project:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">svelte</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@sveltejs/vite-plugin-svelte</span><span class="dl">'</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
    <span class="na">component</span><span class="p">:</span> <span class="p">{</span>
        <span class="c1">// ...</span>
        <span class="na">devServer</span><span class="p">:</span> <span class="p">{</span>
            <span class="c1">// ...</span>
            <span class="na">viteConfig</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">return</span> <span class="p">{</span>
                    <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span><span class="nx">svelte</span><span class="p">()]</span>
                <span class="p">}</span>
            <span class="p">},</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>With this change, a headless run (<code class="language-plaintext highlighter-rouge">npx cypress run --component</code>) starts up the Cypress reporter properly!
However it throws a new error when trying to mount the component:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Svelte error: component_api_invalid_new
Attempted to instantiate src/components/TopNav.svelte with `new TopNav`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
</code></pre></div></div>

<p>As the message notes, Svelte 5 has changed how components are mounted, so we’ll need to convince Cypress to mount them the new way.</p>

<p>Looking in the <code class="language-plaintext highlighter-rouge">cypress/support/component.ts</code> file, I notice that the <code class="language-plaintext highlighter-rouge">mount</code> command comes from the ‘cypress/svelte’ dependency:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">mount</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">cypress/svelte</span><span class="dl">'</span>
</code></pre></div></div>

<p>…then I poked around <a href="https://github.com/cypress-io/cypress">the Cypress repo on GitHub</a> until I found <a href="https://github.com/cypress-io/cypress/tree/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte">the <code class="language-plaintext highlighter-rouge">svelte</code> package</a>, and within its <code class="language-plaintext highlighter-rouge">mount.ts</code> file I see where <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L74">it instantiates the component-under-test using <code class="language-plaintext highlighter-rouge">new</code></a>.</p>

<p>So let’s try to change what it does!</p>

<p>First I opened up the <code class="language-plaintext highlighter-rouge">cypress/support/component.ts</code> file and removed the <code class="language-plaintext highlighter-rouge">import {mount}</code> that was there. Then I copied over the <code class="language-plaintext highlighter-rouge">function mount</code> implementation from <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L60-L93">Cypress’s <code class="language-plaintext highlighter-rouge">mount.ts</code></a>, renamed it to <code class="language-plaintext highlighter-rouge">mountV5</code>, and used it as the implementation function where <code class="language-plaintext highlighter-rouge">Cypress.Command.add</code> is called with <code class="language-plaintext highlighter-rouge">'mount'</code>. Then I also brought over the functions and values that it depends on: <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L25"><code class="language-plaintext highlighter-rouge">componentInstance</code></a>, <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L27-L29"><code class="language-plaintext highlighter-rouge">cleanup</code></a>, <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L32-L40"><code class="language-plaintext highlighter-rouge">getComponentDisplayName</code></a> (which needs <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/svelte/src/mount.ts#L8"><code class="language-plaintext highlighter-rouge">DEFAULT_COMP_NAME</code></a>), and finally <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/mount-utils/src/index.ts#L18-L24"><code class="language-plaintext highlighter-rouge">checkForRemovedStyleOptions</code></a>, <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/mount-utils/src/index.ts#L8-L16"><code class="language-plaintext highlighter-rouge">getContainerEl</code></a>, and <a href="https://github.com/cypress-io/cypress/blob/8a8015b774dc9ce54b30bce82b6a85172d71a895/npm/mount-utils/src/index.ts#L1"><code class="language-plaintext highlighter-rouge">ROOT_SELECTOR</code></a> from the <code class="language-plaintext highlighter-rouge">mount-utils</code> package. (I also removed all the types, just to have fewer in-editor errors to wade through right now.)</p>

<p>Finally I modified the <code class="language-plaintext highlighter-rouge">function mount</code> from Cypress’s <code class="language-plaintext highlighter-rouge">mount.ts</code> so that instead of constructing a <code class="language-plaintext highlighter-rouge">new ComponentConstructor({target, ...options})</code>, it calls <code class="language-plaintext highlighter-rouge">mount(Component, {target, ...options})</code> as <a href="https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes">the Breaking Changes doc</a> suggests — including bringing over the <code class="language-plaintext highlighter-rouge">mount</code> function from the <code class="language-plaintext highlighter-rouge">svelte</code> package, with <code class="language-plaintext highlighter-rouge">import { mount } from 'svelte'</code>.</p>

<p>Oh but it turns out that Svelte v4 swapped out <code class="language-plaintext highlighter-rouge">.$destroy()</code> for calling <code class="language-plaintext highlighter-rouge">unmount</code>, so let’s change the import to also pull in <code class="language-plaintext highlighter-rouge">unmount</code> and use that in the <code class="language-plaintext highlighter-rouge">cleanup</code> function.</p>

<p>Here’s the juicy bit of my <code class="language-plaintext highlighter-rouge">component.ts</code> now:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">mount</span><span class="p">,</span> <span class="nx">unmount</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">svelte</span><span class="dl">'</span>

<span class="kd">function</span> <span class="nx">checkForRemovedStyleOptions</span><span class="p">(</span><span class="nx">mountingOptions</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">of</span> <span class="p">[</span><span class="dl">'</span><span class="s1">cssFile</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">cssFiles</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">style</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">styles</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">stylesheet</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">stylesheets</span><span class="dl">'</span><span class="p">])</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">mountingOptions</span><span class="p">[</span><span class="nx">key</span><span class="p">])</span> <span class="p">{</span>
      <span class="nx">Cypress</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nx">throwErrByPath</span><span class="p">(</span><span class="dl">'</span><span class="s1">mount.removed_style_mounting_options</span><span class="dl">'</span><span class="p">,</span> <span class="nx">key</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">ROOT_SELECTOR</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">[data-cy-root]</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">getContainerEl</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">ROOT_SELECTOR</span><span class="p">)</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">el</span>
  <span class="p">}</span>
  <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No element found that matches selector </span><span class="p">${</span><span class="nx">ROOT_SELECTOR</span><span class="p">}</span><span class="s2">. Please add a root element with data-cy-root attribute to your "component-index.html" file so that Cypress can attach your component to the DOM.`</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">componentInstance</span>
<span class="kd">const</span> <span class="nx">cleanup</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">componentInstance</span><span class="p">)</span>
    <span class="nx">unmount</span><span class="p">(</span><span class="nx">componentInstance</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">DEFAULT_COMP_NAME</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">unknown</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">getComponentDisplayName</span> <span class="o">=</span> <span class="p">(</span><span class="nx">Component</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">Component</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[,</span> <span class="nx">match</span><span class="p">]</span> <span class="o">=</span> <span class="sr">/Proxy</span><span class="se">\&lt;(\w</span><span class="sr">+</span><span class="se">)\&gt;</span><span class="sr">/</span><span class="p">.</span><span class="nx">exec</span><span class="p">(</span><span class="nx">Component</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span> <span class="o">||</span> <span class="p">[]</span>
    <span class="k">return</span> <span class="nx">match</span> <span class="o">||</span> <span class="nx">Component</span><span class="p">.</span><span class="nx">name</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">DEFAULT_COMP_NAME</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">mountV5</span><span class="p">(</span><span class="nx">Component</span><span class="p">,</span> <span class="nx">options</span><span class="o">=</span><span class="p">{})</span> <span class="p">{</span>
  <span class="nx">checkForRemovedStyleOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span>
  <span class="k">return</span> <span class="nx">cy</span><span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Remove last mounted component if cy.mount is called more than once in a test</span>
    <span class="nx">cleanup</span><span class="p">()</span>
    <span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">getContainerEl</span><span class="p">()</span>
    <span class="kd">const</span> <span class="nx">ComponentConstructor</span> <span class="o">=</span> <span class="p">(</span><span class="nx">Component</span><span class="p">.</span><span class="k">default</span> <span class="o">||</span> <span class="nx">Component</span><span class="p">)</span>
    <span class="nx">componentInstance</span> <span class="o">=</span> <span class="nx">mount</span><span class="p">(</span><span class="nx">ComponentConstructor</span><span class="p">,</span> <span class="p">{</span>
      <span class="nx">target</span><span class="p">,</span>
      <span class="p">...</span><span class="nx">options</span><span class="p">,</span>
    <span class="p">})</span>
    <span class="c1">// by waiting, we are delaying test execution for the next tick of event loop</span>
    <span class="c1">// and letting hooks and component lifecycle methods to execute mount</span>
    <span class="k">return</span> <span class="nx">cy</span><span class="p">.</span><span class="nx">wait</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">{</span><span class="na">log</span><span class="p">:</span> <span class="kc">false</span><span class="p">}).</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">log</span> <span class="o">!==</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">mountMessage</span> <span class="o">=</span> <span class="s2">`&lt;</span><span class="p">${</span><span class="nx">getComponentDisplayName</span><span class="p">(</span><span class="nx">Component</span><span class="p">)}</span><span class="s2"> ... /&gt;`</span>
        <span class="nx">Cypress</span><span class="p">.</span><span class="nx">log</span><span class="p">({</span>
          <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mount</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">message</span><span class="p">:</span> <span class="p">[</span><span class="nx">mountMessage</span><span class="p">],</span>
        <span class="p">})</span>
      <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nx">wrap</span><span class="p">({</span><span class="na">component</span><span class="p">:</span> <span class="nx">componentInstance</span><span class="p">},</span> <span class="p">{</span><span class="na">log</span><span class="p">:</span> <span class="kc">false</span><span class="p">})</span>
  <span class="p">})</span>
<span class="p">}</span>

<span class="c1">// Augment the Cypress namespace to include type definitions for your custom command.</span>
<span class="c1">// Alternatively, can be defined in cypress/support/component.d.ts with a &lt;reference path="./component" /&gt; at the top of your spec.</span>
<span class="kr">declare</span> <span class="nb">global</span> <span class="p">{</span>
  <span class="k">namespace</span> <span class="nx">Cypress</span> <span class="p">{</span>
    <span class="kr">interface</span> <span class="nx">Chainable</span> <span class="p">{</span>
      <span class="nl">mount</span><span class="p">:</span> <span class="k">typeof</span> <span class="nx">mountV5</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">Cypress</span><span class="p">.</span><span class="nx">Commands</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">mount</span><span class="dl">'</span><span class="p">,</span> <span class="nx">mountV5</span><span class="p">)</span>
</code></pre></div></div>

<p>And with that, my Cypress component test is passing! 🙌</p>

<p>Here’s <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/d1af2a3d567e68a3a93289b3848ad5023a4a48d7/cypress/support/component.ts">a link to my full <code class="language-plaintext highlighter-rouge">cypress/support/component.ts</code> file</a>, and here’s <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/commit/f91dada0c82d04c3e73b6620cd0fd0931c297f56">the first commit with the other changes to get a bare-bones component test working</a>.</p>]]></content><author><name>Alexander</name></author><category term="howto" /><category term="javascript" /><category term="code" /><category term="testing" /><category term="cypress" /><category term="svelte" /><summary type="html"><![CDATA[Version 5 of Svelte makes some dramatic changes in its API. Cypress component testing already doesn’t play nicely with SvelteKit, and Svelte v5 doesn’t make things better.]]></summary></entry><entry><title type="html">Setting up SvelteKit to use SQLite and prerender a static site to be hosted on GitLab Pages</title><link href="https://alxndr.blog/2024/07/18/sveltekit-and-sqlite-on-gitlab-pages.html" rel="alternate" type="text/html" title="Setting up SvelteKit to use SQLite and prerender a static site to be hosted on GitLab Pages" /><published>2024-07-18T00:00:00+00:00</published><updated>2024-07-18T00:00:00+00:00</updated><id>https://alxndr.blog/2024/07/18/sveltekit-and-sqlite-on-gitlab-pages</id><content type="html" xml:base="https://alxndr.blog/2024/07/18/sveltekit-and-sqlite-on-gitlab-pages.html"><![CDATA[<p>I started with <code class="language-plaintext highlighter-rouge">npm init vite</code> and picked a <a href="https://kit.svelte.dev">SvelteKit</a> project (using <a href="https://www.typescriptlang.org">TypeScript</a>).</p>

<p>First change-up you’ve gotta make is to use <a href="https://kit.svelte.dev/docs/adapter-static">the <code class="language-plaintext highlighter-rouge">@sveltejs/adapter-static</code> adapter</a>. For Svelte(Kit?) reasons it needs to be one of the <code class="language-plaintext highlighter-rouge">devDependencies</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">install</span> <span class="nt">-D</span> @sveltejs/adapter-static
</code></pre></div></div>

<p>…then <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/932ef981b7e689ea5e70057390b0e1ed6e42e1af/svelte.config.js">use it in the <code class="language-plaintext highlighter-rouge">svelte.config.js</code></a> to set <code class="language-plaintext highlighter-rouge">strict: false</code> and specify the <code class="language-plaintext highlighter-rouge">public</code> directory as the source for <code class="language-plaintext highlighter-rouge">pages</code> and <code class="language-plaintext highlighter-rouge">assets</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">adapter</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@sveltejs/adapter-static</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">vitePreprocess</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@sveltejs/vite-plugin-svelte</span><span class="dl">'</span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
  <span class="na">preprocess</span><span class="p">:</span> <span class="p">[</span><span class="nx">vitePreprocess</span><span class="p">()],</span>
  <span class="na">kit</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">adapter</span><span class="p">:</span> <span class="nx">adapter</span><span class="p">({</span>
      <span class="na">pages</span><span class="p">:</span> <span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">assets</span><span class="p">:</span> <span class="dl">'</span><span class="s1">public</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">fallback</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
      <span class="na">precompress</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">strict</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// https://kit.svelte.dev/docs/adapter-static#options-strict</span>
    <span class="p">}),</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<p><a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/932ef981b7e689ea5e70057390b0e1ed6e42e1af/.gitlab-ci.yml">Here’s</a> how I initially got the static site prerendering on GitLab pipeline…</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .gitlab-ci.yml</span>

<span class="na">image</span><span class="pi">:</span> <span class="s">node:latest</span>
<span class="na">pages</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">npm ci</span>
    <span class="pi">-</span> <span class="s">npm run build</span> <span class="c1"># even though this appears to create things in `.svelte-kit/output/{client,server}/*`, @sveltejs/adapter-static puts things into the `public/` dir...</span>
  <span class="na">artifacts</span><span class="pi">:</span>
    <span class="na">paths</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">public</span>
  <span class="na">publish</span><span class="pi">:</span> <span class="s">public</span> <span class="c1"># this might be the default</span>
  <span class="na">only</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">main</span>
</code></pre></div></div>

<p>Okay so at this point GitLab is building the static site and hosting it on GitLab Pages; now for adding SQLite…</p>

<p>First install <code class="language-plaintext highlighter-rouge">better-sqlite3</code> as one of the <code class="language-plaintext highlighter-rouge">devDependencies</code> (and if you’re using TypeScript, <code class="language-plaintext highlighter-rouge">@types/better-sqlite3</code> too)…</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">install</span> <span class="nt">-D</span> better-sqlite3 @types/better-sqlite3
</code></pre></div></div>

<p>Then pick <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/9f8ab5b21fe9329b63892a7e4d0458c3e2a33c8c/src/routes/show/%5Bslug%5D/+page.svelte">a <code class="language-plaintext highlighter-rouge">+page.svelte</code> file</a> which you want to get some data from the database, and open (or create) <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/9f8ab5b21fe9329b63892a7e4d0458c3e2a33c8c/src/routes/show/%5Bslug%5D/+page.server.ts">its corresponding <code class="language-plaintext highlighter-rouge">+page.server.ts</code> file</a>.
In this file you’re gonna import <code class="language-plaintext highlighter-rouge">better-sqlite3</code>, use it to read the <code class="language-plaintext highlighter-rouge">.db</code> SQLite file, and run a query to extract some data…</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">error</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@sveltejs/kit</span><span class="dl">'</span>
<span class="k">import</span> <span class="kd">type</span> <span class="p">{</span><span class="nx">ReqestHandler</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./$types</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">Database</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">better-sqlite3</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:fs</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Database</span><span class="p">(</span><span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">src/path-to-sqlite-data.db</span><span class="dl">'</span><span class="p">))</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">load</span><span class="p">:</span> <span class="nx">RequestHandler</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">params</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">showNum</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">slug</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">stmt</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">prepare</span><span class="p">(</span><span class="dl">'</span><span class="s1">select * from shows where id like ? limit 1</span><span class="dl">'</span><span class="p">)</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">stmt</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="s2">`%</span><span class="p">${</span><span class="nx">showNum</span><span class="p">}</span><span class="s2">%`</span><span class="p">)</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">?.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">showData</span><span class="p">]</span> <span class="o">=</span> <span class="nx">result</span>
    <span class="k">return</span> <span class="nx">showData</span>
  <span class="p">}</span>

  <span class="nx">error</span><span class="p">(</span><span class="mi">404</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Not found</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/9f8ab5b21fe9329b63892a7e4d0458c3e2a33c8c/src/routes/show/%5Bslug%5D/+page.svelte">the <code class="language-plaintext highlighter-rouge">+page.svelte</code></a> will be passed that <code class="language-plaintext highlighter-rouge">showData</code> object as the <code class="language-plaintext highlighter-rouge">PageLoad</code> <code class="language-plaintext highlighter-rouge">data</code>!</p>

<pre><code class="language-svelte">&lt;script lang="ts"&gt;
  import type { PageLoad } from './$types'
  export let data: PageLoad
&lt;/script&gt;

&lt;h1 class="show__title"&gt;
  Show #{data.id}: {data.tagline}
&lt;/h1&gt;

{#if data.notes}
  &lt;p class="show__notes"&gt;{data.notes}&lt;/p&gt;
{/if}
</code></pre>

<p><strong>Ta-daa! It works! 🙌 🥂</strong></p>

<h2 id="testing-with-playwright">Testing with <a href="https://playwright.dev">Playwright</a></h2>

<p>I have lots of experience with <a href="https://cypress.io">Cypress</a>, so to try something (else) new I decided to check out <a href="https://playwright.dev">Playwright</a>.</p>

<p>To add Playwright to my existing app, I used the <a href="https://www.npmjs.com/package/create-playwright"><code class="language-plaintext highlighter-rouge">create-playwright</code></a> CLI tool: <code class="language-plaintext highlighter-rouge">pnpm dlx create-playwright</code></p>

<p>It scaffolds a test subdirectory, a (GitHub) CI actions <code class="language-plaintext highlighter-rouge">yaml</code> file, and installs browsers to test with (chromium, firefox, webkit). Once it’s done, it recommends running <code class="language-plaintext highlighter-rouge">pnpm exec playwright test</code> to set up some starter tests, and then <code class="language-plaintext highlighter-rouge">pnpm exec playwright show-report</code> to start a local webserver showing the results of the test.</p>

<p><a href="https://playwright.dev/docs/ci#gitlab-ci">The Playwright site has suggestions on setup for GitLab CI</a>, namely to use their <code class="language-plaintext highlighter-rouge">mcr.microsoft.com/playwright:v1.45.1-jammy</code> Docker image:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">tests</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">test</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">mcr.microsoft.com/playwright:v1.45.1-jammy</span>
  <span class="na">script</span><span class="pi">:</span>
      <span class="c1"># ...</span>
</code></pre></div></div>

<p>I <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/c42658d22d0cf642eae148712005f7899955c1c4/.gitlab-ci.yml">added that to my existing <code class="language-plaintext highlighter-rouge">gitlab-ci.yml</code> file</a> and the default example tests are passing on GitLab CI! (Note that I used the <code class="language-plaintext highlighter-rouge">stages</code> section to specify that the <code class="language-plaintext highlighter-rouge">test</code> stage runs first, and if it succeeds then the <code class="language-plaintext highlighter-rouge">deploy</code> stage runs. I’ll work on a <code class="language-plaintext highlighter-rouge">build</code> stage after I write some tests…)</p>

<p>To start testing my app, I’ve gotta tell Playwright what URL to visit to find the app. In <a href="https://stackoverflow.com/a/68212980/303896">Playwright &gt;=1.13.x</a>, the <code class="language-plaintext highlighter-rouge">playwright.config.ts</code> file has a <code class="language-plaintext highlighter-rouge">use</code> section wherein we can specify the <code class="language-plaintext highlighter-rouge">baseURL</code> to use what <code class="language-plaintext highlighter-rouge">npm run dev</code> defaults to:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="c1">// ...</span>
  <span class="na">use</span><span class="p">:</span> <span class="p">{</span>
    <span class="cm">/* Base URL to use in actions like `await page.goto('/')`. */</span>
    <span class="na">baseURL</span><span class="p">:</span> <span class="dl">'</span><span class="s1">http://localhost:5173</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="c1">// ...</span>
<span class="p">})</span>
</code></pre></div></div>

<p>To get this working on GitLab, traditionally I’d look for a way to override the <code class="language-plaintext highlighter-rouge">baseURL</code> in the CI environment… However the bottom of <code class="language-plaintext highlighter-rouge">playwright.config.ts</code> has a <code class="language-plaintext highlighter-rouge">webServer</code> section which lets us specify a command for starting the server plus the URL to look for it — and we can use the <code class="language-plaintext highlighter-rouge">$CI</code> environment variable to modify this behavior for running on CI vs local/dev:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="c1">// ...</span>
  <span class="na">webServer</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">command</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span>
      <span class="p">?</span> <span class="dl">'</span><span class="s1">npm run build &amp;&amp; npm run preview -- --port 5173 --strictPort</span><span class="dl">'</span>
      <span class="p">:</span> <span class="dl">'</span><span class="s1">npm run dev -- --port 5173 --strictPort</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">http://localhost:5173</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">reuseExistingServer</span><span class="p">:</span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CI</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="c1">// ...</span>
<span class="p">})</span>
</code></pre></div></div>

<p>…and with that, on GitLab it will <code class="language-plaintext highlighter-rouge">build</code> and then <code class="language-plaintext highlighter-rouge">preview</code> the built files, wait for the <code class="language-plaintext highlighter-rouge">url</code> to be responsive, but on local/dev it will hit the <code class="language-plaintext highlighter-rouge">url</code> and only run the <code class="language-plaintext highlighter-rouge">command</code> if there is not something running there!</p>

<h2 id="svelte-gotchas">Svelte gotchas</h2>

<p>Once I got more of my site built, I found that linking from a <code class="language-plaintext highlighter-rouge">+page.svelte</code> template with slug A to the same template with slug B would result in the URL being changed, but some of the content on the page is not changing… I initially assumed this was due to me breaking Svelte’s reactivity in some way, but it turns out to be a common SvelteKit footgun:</p>

<ul>
  <li><a href="https://github.com/sveltejs/kit/issues/1075">bug #1075: Navigating between same slug does not change page content</a></li>
  <li><a href="https://github.com/sveltejs/kit/issues/1497">bug #1497: Component variables aren’t re-initiated when navigating to a different slug</a></li>
  <li><a href="https://github.com/sveltejs/kit/discussions/5007">discussion: Resetting components on navigation</a></li>
</ul>

<p>…so it’s <em>both</em> “a feature not a bug” and <em>also</em> due to me not declaring the template’s input as “this needs to be reactive”. Great.</p>

<p>The Svelte 4 solution is something like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">export</span> <span class="kd">let</span> <span class="nx">data</span><span class="p">:</span> <span class="nx">PageLoad</span>
  <span class="nx">$</span><span class="p">:</span> <span class="nx">tracksData</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">tracksData</span>
  <span class="nx">$</span><span class="p">:</span> <span class="nx">setlistData</span> <span class="o">=</span> <span class="nx">tracksData</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="cm">/*...*/</span><span class="p">)</span>
</code></pre></div></div>

<p>…but I’m trying to stick with <a href="https://frontendmasters.com/blog/introducing-svelte-5/">Svelte 5</a>, which means doing the same thing with <code class="language-plaintext highlighter-rouge">$props()</code> and <code class="language-plaintext highlighter-rouge">$derived()</code> instead:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">let</span> <span class="p">{</span><span class="nx">data</span><span class="p">}</span> <span class="o">=</span> <span class="nx">$props</span><span class="o">&lt;</span><span class="nx">PageLoad</span><span class="o">&gt;</span><span class="p">()</span>
  <span class="kd">let</span> <span class="nx">setlistData</span> <span class="o">=</span> <span class="nx">$derived</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">tracksData</span><span class="p">.</span><span class="nx">reduce</span><span class="p">(</span><span class="cm">/* ... */</span><span class="p">)</span>
</code></pre></div></div>

<p>Note that because this data is coming from the <code class="language-plaintext highlighter-rouge">+page.server.ts</code> file’s <code class="language-plaintext highlighter-rouge">load</code> function, it ends up at <code class="language-plaintext highlighter-rouge">$props().data</code> and not within the top-level props.</p>

<h2 id="redirects-with-sveltekit-static-adapter">Redirects with SvelteKit static adapter</h2>

<p>I’m slightly tweaking the URLs for things in this rebuild, so I want to add some redirects from the old formats to the new ones. I’ve followed <a href="https://supun.io/sveltekit-static-redirects">this guide</a> to support redirects with the SvelteKit static adapter, but had to tweak it slightly (perhaps for Svelte v5??)… but it’s still worth reading because it explains more fully what is going on here:</p>

<ol>
  <li>
    <p>set up a mapping of old-URLs-to-new-URLs in a JS object, and save as <code class="language-plaintext highlighter-rouge">src/redirects.js</code> (can’t use TypeScript for this apparently?)</p>
  </li>
  <li>
    <p>modify/create <code class="language-plaintext highlighter-rouge">src/routes/+layout.server.ts</code> (note: this belongs in <code class="language-plaintext highlighter-rouge">+layout.server.ts</code> and not <code class="language-plaintext highlighter-rouge">+layout.ts</code> as the above blog post says! otherwise you’ll break <abbr title="hot module reloading">HMR</abbr> in dev) to import that new <code class="language-plaintext highlighter-rouge">redirects.js</code> object along with the <code class="language-plaintext highlighter-rouge">redirect</code> function from SvelteKit, and export a function which checks whether the incoming URL matches one of the keys in the redirect object:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> import {redirect} from '@sveltejs/kit'
 import {REDIRECTS} from '../redirects'
 export async function load({url}) {
   const pathname = url.pathname
   if (REDIRECTS.hasOwnProperty(pathname))
     return redirect(301, REDIRECTS[pathname])
 }
</code></pre></div>    </div>
  </li>
  <li>
    <p>Tie the two together by modifying <code class="language-plaintext highlighter-rouge">svelte.config.js</code> to import the <code class="language-plaintext highlighter-rouge">redirects.js</code> object and add its keys to the list of routes to prerender:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> export default {
   // ...
   kit: {
     // ...
     prerender: {
       entries: ['*', ...Object.keys(REDIRECTS)],
     },
   },
 }
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="client-side-search-using-flexsearch">Client-side search using Flexsearch</h2>

<p><a href="https://github.com/nextapps-de/flexsearch">Flexsearch</a> makes a lot of <a href="https://github.com/nextapps-de/flexsearch/blob/961c3ae84a87fb/README.md#performance-benchmark-ranking">claims about how fast it is on the client-side</a>. That sounds pretty nice, so let’s integrate it!</p>

<p>I used <a href="https://joyofcode.xyz/blazing-fast-sveltekit-search">this guide on integrating Flexsearch with SvelteKit</a> which (despite being aimed at Svelte v4) was quite helpful in pointing the way. The guide mentions adding search match markers to the search response, but I skipped that to start with. Here’s what I ended up doing…</p>

<ul>
  <li>create files:
    <ul>
      <li><a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/lib/search.ts"><code class="language-plaintext highlighter-rouge">src/lib/search.ts</code></a> — contains logic for building and searching the index</li>
      <li><a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/search.json/+server.ts"><code class="language-plaintext highlighter-rouge">src/routes/search.json/+server.ts</code></a> — server-side (in dev; what about when building for static site?) endpoint to “serve prerendered content as JSON”</li>
    </ul>
  </li>
  <li>pick/create a <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/+page.svelte"><code class="language-plaintext highlighter-rouge">+page.svelte</code></a> where the UX for the search will be (I’m using my home page, i.e. <code class="language-plaintext highlighter-rouge">src/routes/+page.svelte</code>)
    <ul>
      <li>use <code class="language-plaintext highlighter-rouge">$state( /*...*/ )</code> to wrap <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/+page.svelte#L25-27">the initial values of <code class="language-plaintext highlighter-rouge">searchTerm</code>, <code class="language-plaintext highlighter-rouge">results</code>, and the ready flag</a></li>
      <li>use <code class="language-plaintext highlighter-rouge">onMount(() =&gt; { /*...*/ })</code> to <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/+page.svelte#L35-39">retrieve <code class="language-plaintext highlighter-rouge">search.json</code> and set up the index</a></li>
      <li>use <code class="language-plaintext highlighter-rouge">$effect(() =&gt; { /*...*/ })</code> to <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/+page.svelte#L41-45">wrap the call to <code class="language-plaintext highlighter-rouge">searchSongsIndex(searchTerm)</code></a></li>
      <li>then your component can <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/blob/558709644/src/routes/+page.svelte#L62-85">check the ready flag and render the search UX</a></li>
    </ul>
  </li>
</ul>

<p>…and it works!</p>

<h2 id="debugging-a-deploy">Debugging a deploy…</h2>

<p>At one point I was happily hacking away and running the site locally with <code class="language-plaintext highlighter-rouge">npm run dev</code>, but when I pushed up to GitLab the Pipeline failed with this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm ci
npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error   &lt;https://github.com/npm/cli/issues&gt;
npm error A complete log of this run can be found in: /root/.npm/_logs/2024-07-18T22_10_54_897Z-debug-0.log
$ npm run build
&gt; almost-dead-dot-net@0.0.1 build
&gt; vite build
sh: 1: vite: not found
</code></pre></div></div>

<p>Ruh roh. First I <a href="https://docs.gitlab.com/ee/ci/variables/#enable-debug-logging">enabled debug logging with the <code class="language-plaintext highlighter-rouge">CI_DEBUG_TRACE</code> variable</a>, but that seems to be like the shell <code class="language-plaintext highlighter-rouge">setopt -o VERBOSE</code> and was not helpful with the actual error…</p>

<p>Instead, change up <a href="https://gitlab.com/alxndr/almost-dead-dot-net/-/commit/177565417a893d86f47158adb0051a8c3a774e0a#587d266bb27a4dc3022bbed44dfa19849df3044c">the CI script</a> to <code class="language-plaintext highlighter-rouge">npm install --cache=.npm</code> so that error logs will be saved in the current working directory, and then have GitLab capture them as artifacts:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pages</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">npm install --cache=.npm &amp;&amp; npm run build</span>
  <span class="na">artifacts</span><span class="pi">:</span>
    <span class="na">paths</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">public</span>
    <span class="pi">-</span> <span class="s">.npm/_logs/*</span>
    <span class="na">when</span><span class="pi">:</span> <span class="s">always</span>
  <span class="na">publish</span><span class="pi">:</span> <span class="s">public</span>
  <span class="na">only</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">main</span>
</code></pre></div></div>

<p>Then even if the job fails, any <code class="language-plaintext highlighter-rouge">npm</code> logs will be made available as artifacts and can be downloaded and inspected.</p>

<p>In my case, there was no new info about <em>why</em> the Exit handler is never called…</p>

<p>Noticing that I specified the <code class="language-plaintext highlighter-rouge">node:latest</code> image to build, I tried setting it to use the version of Node I’m using locally: <code class="language-plaintext highlighter-rouge">node:20.9.0</code>. This changed the error message shown in the Pipeline, giving me a proper stack trace! 🎉</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm ERR! code 1
npm ERR! path /builds/alxndr/almost-dead-dot-net/node_modules/@sveltejs/kit
npm ERR! command failed
npm ERR! command sh -c node postinstall.js
npm ERR! Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.
</code></pre></div></div>

<p>Dutifully I locally removed the <code class="language-plaintext highlighter-rouge">package-lock.json</code> and <code class="language-plaintext highlighter-rouge">node_modules/</code> and reinstalled and committed, and happily GitLab CI was able to build the app!</p>

<h2 id="typescript-checking-in-vimneovim">TypeScript checking in Vim/NeoVim</h2>

<p>Yep I’m one of those weirdos who doesn’t use VSCode.</p>

<p>Instead I am a big fan of <a href="https://neovim.io">NeoVim</a>, and lately I’ve been using <a href="https://github.com/williamboman/mason.nvim">Mason</a> to manage language linters/checkers/etc. Helpfully there is a <a href="https://github.com/sveltejs/language-tools"><code class="language-plaintext highlighter-rouge">svelte-language-server</code></a>! (make sure you’re using the one from the official SvelteJS project and not the now-deprecated initial project by James Birtles)</p>

<p>I’d noticed that TypeScript checking was working as expected in <code class="language-plaintext highlighter-rouge">.ts</code> files, however it’s not working in <code class="language-plaintext highlighter-rouge">.svelte</code> files even with <code class="language-plaintext highlighter-rouge">&lt;script type="ts"&gt;</code>…</p>

<p>Luckily there is <a href="https://discord.com/invite/svelte">a Discord community for Svelte</a> where folks are pretty quick to respond to questions, and <a href="https://github.com/TGlide">@TGlide</a> pointed me in the right direction. I needed to configure the TypeScript LSP client to look for <code class="language-plaintext highlighter-rouge">.svelte</code> files (of course 🤦) and also <a href="https://github.com/alxndr/dotfiles/commit/26b2cc837bd761784#diff-333f3b6c66b09fce0cfdf9d83baa5a9ba2b5725a69a1db1c24d132aaf52b7bf7R340-R345">tweak the Svelte LSP client with something about watched files</a>:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">lsp_capabilities</span> <span class="o">=</span> <span class="n">vim</span><span class="p">.</span><span class="n">lsp</span><span class="p">.</span><span class="n">protocol</span><span class="p">.</span><span class="n">make_client_capabilities</span><span class="p">()</span>
<span class="n">lsp_capabilities</span><span class="p">.</span><span class="n">workspace</span><span class="p">.</span><span class="n">didChangeWatchedFiles</span> <span class="o">=</span> <span class="kc">false</span>
<span class="n">lspc</span><span class="p">.</span><span class="n">svelte</span><span class="p">.</span><span class="n">setup</span> <span class="p">{</span>
  <span class="n">capabilities</span> <span class="o">=</span> <span class="n">lsp_capabilities</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now NeoVim shows me TypeScript errors/warnings within <code class="language-plaintext highlighter-rouge">.svelte</code> files, and updates when I edit (not just on save)!</p>

<h2 id="acknowledgements">Acknowledgements</h2>

<p><a href="https://github.com/vitejs/vite/tree/main/packages/create-vite"><code class="language-plaintext highlighter-rouge">npm init vite</code></a> does a lot of the backend-for-frontend setup, without which I’d be totally unable to do any of this. Chapeau, <a href="https://vitejs.dev">Vite</a>!</p>

<p><a href="https://kit.svelte.dev/docs/load">The SvelteKit docs</a> are quite nice, helpful for a brand-new-beginner like me. Thanks Svelters!</p>

<p><a href="https://kisaragi-hiu.com/kemdict-sveltekit-sqlite">Kisaragi Hiu’s blog</a> shepherded me gently through all of the SQLite stuff! Thank you!</p>

<p>https://www.okupter.com/blog/e2e-testing-with-sveltekit-and-playwright</p>

<hr />]]></content><author><name>Alexander</name></author><category term="howto" /><category term="javascript" /><category term="code" /><summary type="html"><![CDATA[I started with npm init vite and picked a SvelteKit project (using TypeScript).]]></summary></entry><entry><title type="html">Oghamic Toki Pona</title><link href="https://alxndr.blog/2024/01/18/sitelen-oken-pi-toki-pona.html" rel="alternate" type="text/html" title="Oghamic Toki Pona" /><published>2024-01-18T00:00:00+00:00</published><updated>2024-01-18T00:00:00+00:00</updated><id>https://alxndr.blog/2024/01/18/sitelen-oken-pi-toki-pona</id><content type="html" xml:base="https://alxndr.blog/2024/01/18/sitelen-oken-pi-toki-pona.html"><![CDATA[<p>It <a href="/2023/05/25/lo-dzexerno-lerfu-the-oghamic-lojban.html">worked for Lojban</a> so of course it’ll work for <a href="https://tokipona.org">Toki Pona</a>, right?!?</p>

<p>Here’s what it might look like to write Toki Pona using <a href="https://en.wikipedia.org/wiki/Ogham">Ogham</a> letters…</p>

<h1 id="sitelen-oken">sitelen Oken</h1>

<div style="font-size:1.2em">
᚛ᚆᚐᚅ ᚐᚂᚓ ᚂᚔ ᚉᚐᚋᚐ ᚂᚑᚅ ᚅᚐᚄᚔᚅ ᚅᚔ᚜
<br />
᚛ᚑᚅᚐ ᚂᚔ ᚉᚓᚅ ᚈᚐᚃᚐ ᚂᚔ ᚉᚓᚅ ᚁᚐᚂᚔ᚜
<br />
᚛ᚆᚐᚅ ᚐᚂᚓ ᚂᚔ ᚉᚐᚋᚐ ᚂᚑᚅ ᚄᚐᚋᚐ᚜
<br />
᚛ᚆᚐᚅ ᚐᚂᚓ ᚂᚔ ᚆᚑ ᚓ ᚉᚓᚅ ᚁᚔ ᚁᚔᚂᚔᚅ ᚄᚒᚂᚔ᚜
<br />
᚛ᚆᚐᚅ ᚐᚂᚓ ᚂᚔ ᚉᚓᚅ ᚁᚐᚂᚔ ᚓ ᚃᚔᚂᚓ ᚁᚑᚅᚐ ᚑᚅᚐ᚜
<br />
᚛ᚆᚐᚅ ᚐᚂᚓ ᚂᚔ ᚃᚔᚂᚓ ᚁᚐᚂᚔ ᚉᚓᚁᚓᚉᚓᚅ ᚅᚐᚄᚔᚅ ᚅᚔ᚜
<br />
᚛ᚑᚅᚐ ᚂᚔ ᚆᚐᚅ ᚁᚑᚅᚐ ᚈᚐᚃᚐ ᚆᚐᚅ ᚐᚅᚈᚓ᚜
</div>

<blockquote>
  <p>jan ale li kama lon nasin ni:<br />
ona li ken tawa li ken pali.<br />
jan ale li kama lon sama.<br />
jan ale li jo e ken pi pilin suli.<br />
jan ale li ken pali e wile pona ona.<br />
jan ale li wile pali kepeken nasin ni:<br />
ona li jan pona tawa jan ante.</p>
</blockquote>

<h2 id="mapping">mapping</h2>

<div style="display:flex;flex-flow:row wrap">
  <table style="flex-basis:14rem;width:auto;margin:0 auto;white-space:nowrap;font-size:1rem">
    <thead>
      <tr>
        <th style="text-align: center"><small>Latin</small></th>
        <th style="text-align: center"><small>Ogham</small></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: center">a</td>
        <td style="text-align: center">ᚐ</td>
      </tr>
      <tr>
        <td style="text-align: center">e</td>
        <td style="text-align: center">ᚓ</td>
      </tr>
      <tr>
        <td style="text-align: center">i</td>
        <td style="text-align: center">ᚔ</td>
      </tr>
      <tr>
        <td style="text-align: center">j</td>
        <td style="text-align: center">ᚆ</td>
      </tr>
      <tr>
        <td style="text-align: center">k</td>
        <td style="text-align: center">ᚉ</td>
      </tr>
      <tr>
        <td style="text-align: center">l</td>
        <td style="text-align: center">ᚂ</td>
      </tr>
      <tr>
        <td style="text-align: center">m</td>
        <td style="text-align: center">ᚋ</td>
      </tr>
    </tbody>
  </table>

  <table style="flex-basis:12rem;width:auto;margin:0 auto;white-space:nowrap;font-size:1rem">
    <thead>
      <tr>
        <th style="text-align: center"><small>Latin</small></th>
        <th style="text-align: center"><small>Ogham</small></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: center">n</td>
        <td style="text-align: center">ᚅ</td>
      </tr>
      <tr>
        <td style="text-align: center">o</td>
        <td style="text-align: center">ᚑ</td>
      </tr>
      <tr>
        <td style="text-align: center">p</td>
        <td style="text-align: center">ᚁ</td>
      </tr>
      <tr>
        <td style="text-align: center">s</td>
        <td style="text-align: center">ᚄ</td>
      </tr>
      <tr>
        <td style="text-align: center">t</td>
        <td style="text-align: center">ᚈ</td>
      </tr>
      <tr>
        <td style="text-align: center">u</td>
        <td style="text-align: center">ᚒ</td>
      </tr>
      <tr>
        <td style="text-align: center">w</td>
        <td style="text-align: center">ᚃ</td>
      </tr>
    </tbody>
  </table>
</div>]]></content><author><name>Alexander</name></author><category term="orthography" /><category term="toki pona" /><category term="Ogham" /><summary type="html"><![CDATA[It worked for Lojban so of course it’ll work for Toki Pona, right?!?]]></summary></entry><entry><title type="html">a Vim syntax file for Toki Pona</title><link href="https://alxndr.blog/2023/11/16/vim-syntax-tokipona.html" rel="alternate" type="text/html" title="a Vim syntax file for Toki Pona" /><published>2023-11-16T00:00:00+00:00</published><updated>2023-11-16T00:00:00+00:00</updated><id>https://alxndr.blog/2023/11/16/vim-syntax-tokipona</id><content type="html" xml:base="https://alxndr.blog/2023/11/16/vim-syntax-tokipona.html"><![CDATA[<p lang="tp" style="font:1.4em/1em tp-LinjaSuwi;margin-bottom:1rem">a a ilo Vim la mi pali e <a href="https://github.com/alxndr/vim-syntax-tokipona">lipu kute pi nimi kule</a></p>

<p>I made <a href="https://github.com/alxndr/vim-syntax-tokipona">a Vim syntax file for Toki Pona text</a>!</p>

<p>Load it up and <code class="language-plaintext highlighter-rouge">:set filetype=tokipona</code>, then it will highlight headnouns-with-capitalized-names, “la”-phrases, “X ala X”, as well as preverbs and particles and pronouns:</p>

<p><a href="https://i.imgur.com/TfkF8zt.png"><img src="https://i.imgur.com/TfkF8ztm.png" alt="screenshot" /></a></p>

<p>(I recommend also using <a href="https://gitlab.com/dev_urandom/toki-pona-vim-spellcheck">/dev/urandom’s Toki Pona vim spellcheck</a>.)</p>]]></content><author><name>Alexander</name></author><category term="vim" /><category term="toki pona" /><summary type="html"><![CDATA[a a ilo Vim la mi pali e lipu kute pi nimi kule]]></summary></entry><entry><title type="html">sourdough pizza dough ratio calculator</title><link href="https://alxndr.blog/2023/07/28/sourdough-pizza-dough-ratio-calculator.html" rel="alternate" type="text/html" title="sourdough pizza dough ratio calculator" /><published>2023-07-28T00:00:00+00:00</published><updated>2023-07-28T00:00:00+00:00</updated><id>https://alxndr.blog/2023/07/28/sourdough-pizza-dough-ratio-calculator</id><content type="html" xml:base="https://alxndr.blog/2023/07/28/sourdough-pizza-dough-ratio-calculator.html"><![CDATA[<p>I’ve been tending to some sourdough starter ever since the Great Pandemic of 2020. I’ll often make pizza dough with the “leftover” starter.</p>

<p>Below I’ve created an interactive tool to let you enter amounts of starter, water, and flour (and optionally yeast and salt too), and then receive a set of numbers describing the ratios you’d entered.</p>

<p>(Trial and error has landed me around 1 part starter, 4 parts room-temp water, 5 parts high-protein flour, 0.1 part salt… I built this tool in order to help me fine-tune that ratio, and determine ratios for using yeast.)</p>

<noscript>The interactivity below requires client-side JavaScript. Not common for me, I know, but that's all I've got for readable content in this blog post!</noscript>

<script type="module">
import { reactive, html } from 'https://esm.sh/@arrow-js/core';

function int(n) {
  return n.toPrecision(2);
}
function pct(n) {
  return (n*100).toPrecision(3)+'%';
}

const root$ = document.getElementById('dough-ratio-calculator');
const d = reactive({ S: 66.6, W: 300, F: 400, Y: 0.5, T: 9, });
html`
  <ul>
    <li><input value="${() => d.S}" @input="${e => { d.S = e.target.value }}" /> starter
    <li><input value="${() => d.W}" @input="${e => { d.W = e.target.value }}" /> water
    <li><input value="${() => d.F}" @input="${e => { d.F = e.target.value }}" /> flour
    <li><input value="${() => d.T}" @input="${e => { d.T = e.target.value }}" /> salt
    <li><input value="${() => d.Y}" @input="${e => { d.Y = e.target.value }}" /> yeast
  </ul>
  <table><caption>Bakers Percentages</caption>
    <tr><td>starter:</td><td>${() => pct(d.S/d.F)}</td>                </tr>
    <tr>  <td>water:</td><td>${() => pct(d.W/d.F)}</td>                </tr>
    <tr>  <td>flour:</td><td>100% always</td>                          </tr>
    <tr>   <td>salt:</td><td>${() => d.T && pct(d.T/d.F) || 'n/a'}</td></tr>
    <tr>  <td>yeast:</td><td>${() => d.Y && pct(d.Y/d.F) || 'n/a'}</td></tr>
  </table>
  <p><caption>ratios by starter</caption>
    <br/> <abbr title="starter">1</abbr>
    : <abbr title="water">${() => int(d.W/d.S)}</abbr>
    : <abbr title="flour">${() => int(d.F/d.S)}</abbr>
    : <abbr title="salt" >${() => pct(d.T/d.S)}</abbr>
    : <abbr title="yeast">${() => pct(d.Y/d.S)}</abbr>
  </p>
`(root$);

</script>

<div id="dough-ratio-calculator"></div>]]></content><author><name>Alexander</name></author><summary type="html"><![CDATA[I’ve been tending to some sourdough starter ever since the Great Pandemic of 2020. I’ll often make pizza dough with the “leftover” starter.]]></summary></entry><entry><title type="html">Oghamic Lojban</title><link href="https://alxndr.blog/2023/05/25/lo-dzexerno-lerfu-the-oghamic-lojban.html" rel="alternate" type="text/html" title="Oghamic Lojban" /><published>2023-05-25T00:00:00+00:00</published><updated>2023-05-25T00:00:00+00:00</updated><id>https://alxndr.blog/2023/05/25/lo-dzexerno-lerfu-the-oghamic-lojban</id><content type="html" xml:base="https://alxndr.blog/2023/05/25/lo-dzexerno-lerfu-the-oghamic-lojban.html"><![CDATA[<p>Haven’t you always wanted to write <a href="https://en.wikipedia.org/wiki/Lojban">Lojban</a> using <a href="https://en.wikipedia.org/wiki/Ogham">Ogham</a> letters? .i’isai — Me too!</p>

<p>(I’d come up with this a few months ago and only posted it <a href="https://mas.to/@rexsa/109956933715872457">on Mastodon</a>, but also it had some errors, so here it is with a more permanent place and with corrections…)</p>

<h1 id="lo-dzexerno-lerfu">lo dzexerno lerfu</h1>

<p>The <a href="https://mw.lojban.org/papri/.o'i_mu_xagji_sofybakni_cu_zvati_le_purdi">classic pangram</a>:</p>

<div style="font-size:1.2em">
᚛ᚖᚑᚕᚔ ᚋᚒ ᚙᚐᚌᚗᚔ ᚄᚑᚃᚆᚁᚐᚉᚅᚔ ᚊᚒ ᚎᚍᚐᚈᚔ ᚂᚓ ᚘᚒᚏᚇᚔ᚜
</div>

<blockquote>
  <p>.o’i mu xagji sofybakni cu zvati le purdi</p>
</blockquote>

<div style="font-size:1.2em">
᚛ᚖᚔ ᚏᚑ ᚏᚓᚋᚅᚐ ᚊᚒ ᚄᚓ ᚗᚔᚅᚎᚔ ᚊᚑ ᚎᚔᚃᚏᚓ ᚗᚓ ᚄᚔᚋᚇᚒᚕᚔ ᚁᚓ ᚂᚓ ᚏᚆᚖ ᚅᚔᚂᚄᚓᚂᚄᚔᚕᚐ ᚖᚓᚂᚓᚔ ᚏᚆᚖ ᚄᚓᚂᚊᚏᚒ᚜  
᚛ᚖᚔ ᚏᚆᚖ ᚄᚓ ᚋᚓᚅᚂᚔ ᚌᚔᚕᚓ ᚄᚓ ᚄᚓᚎᚋᚐᚏᚇᚓ᚜  
᚛ᚖᚔ ᚖᚓᚔ ᚗᚓᚄᚓᚉᚔᚕᚒᚁᚑ ᚏᚆᚖ ᚄᚔᚋᚆᚎᚒᚕᚓ ᚈᚐᚕᚔ ᚂᚓ ᚈᚒᚅᚁᚐ᚜  
</div>

<blockquote>
  <p>.i ro remna cu se jinzi co zifre je simdu’i be le ry. nilselsi’a .elei ry. selcru  .i ry. se menli gi’e se sezmarde  .i .ei jeseki’ubo ry. simyzu’e ta’i le tunba</p>
</blockquote>

<h2 id="mapping">mapping</h2>

<div style="display:flex;flex-flow:row wrap">
  <table style="flex-basis:14rem;width:auto;margin:0 auto;white-space:nowrap;font-size:1rem">
    <thead>
      <tr>
        <th style="text-align: center"><small>Latin</small></th>
        <th style="text-align: center"><small>Ogham</small></th>
        <th style="text-align: center"><small>IPA</small></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: center">delimiters</td>
        <td style="text-align: center">᚛  ᚜</td>
        <td style="text-align: center">(pause)</td>
      </tr>
      <tr>
        <td style="text-align: center">.</td>
        <td style="text-align: center">ᚖ</td>
        <td style="text-align: center">[ʔ]</td>
      </tr>
      <tr>
        <td style="text-align: center">’</td>
        <td style="text-align: center">ᚕ</td>
        <td style="text-align: center">[h] / [θ]</td>
      </tr>
      <tr>
        <td style="text-align: center">a</td>
        <td style="text-align: center">ᚐ</td>
        <td style="text-align: center">[a] / [ɑ]</td>
      </tr>
      <tr>
        <td style="text-align: center">b</td>
        <td style="text-align: center">ᚁ</td>
        <td style="text-align: center">[b]</td>
      </tr>
      <tr>
        <td style="text-align: center">c</td>
        <td style="text-align: center">ᚊ</td>
        <td style="text-align: center">[ʃ] / [ʂ]</td>
      </tr>
      <tr>
        <td style="text-align: center">d</td>
        <td style="text-align: center">ᚇ</td>
        <td style="text-align: center">[d]</td>
      </tr>
      <tr>
        <td style="text-align: center">e</td>
        <td style="text-align: center">ᚓ</td>
        <td style="text-align: center">[ɛ] / [e]</td>
      </tr>
      <tr>
        <td style="text-align: center">f</td>
        <td style="text-align: center">ᚃ</td>
        <td style="text-align: center">[f] / [ɸ]</td>
      </tr>
      <tr>
        <td style="text-align: center">g</td>
        <td style="text-align: center">ᚌ</td>
        <td style="text-align: center">[ɡ]</td>
      </tr>
      <tr>
        <td style="text-align: center">i</td>
        <td style="text-align: center">ᚔ</td>
        <td style="text-align: center">[i]</td>
      </tr>
      <tr>
        <td style="text-align: center">j</td>
        <td style="text-align: center">ᚗ</td>
        <td style="text-align: center">[ʒ] / [ʐ]</td>
      </tr>
      <tr>
        <td style="text-align: center">k</td>
        <td style="text-align: center">ᚉ</td>
        <td style="text-align: center">[k]</td>
      </tr>
    </tbody>
  </table>

  <table style="flex-basis:12rem;width:auto;margin:0 auto;white-space:nowrap;font-size:1rem">
    <thead>
      <tr>
        <th style="text-align: center"><small>Latin</small></th>
        <th style="text-align: center"><small>Ogham</small></th>
        <th style="text-align: center"><small>IPA</small></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: center">l</td>
        <td style="text-align: center">ᚂ</td>
        <td style="text-align: center">[l]</td>
      </tr>
      <tr>
        <td style="text-align: center">m</td>
        <td style="text-align: center">ᚋ</td>
        <td style="text-align: center">[m]</td>
      </tr>
      <tr>
        <td style="text-align: center">n</td>
        <td style="text-align: center">ᚅ</td>
        <td style="text-align: center">[n]</td>
      </tr>
      <tr>
        <td style="text-align: center">o</td>
        <td style="text-align: center">ᚑ</td>
        <td style="text-align: center">[o] / [ɔ]</td>
      </tr>
      <tr>
        <td style="text-align: center">p</td>
        <td style="text-align: center">ᚘ</td>
        <td style="text-align: center">[p]</td>
      </tr>
      <tr>
        <td style="text-align: center">r</td>
        <td style="text-align: center">ᚏ</td>
        <td style="text-align: center">[r]</td>
      </tr>
      <tr>
        <td style="text-align: center">s</td>
        <td style="text-align: center">ᚄ</td>
        <td style="text-align: center">[s]</td>
      </tr>
      <tr>
        <td style="text-align: center">t</td>
        <td style="text-align: center">ᚈ</td>
        <td style="text-align: center">[t]</td>
      </tr>
      <tr>
        <td style="text-align: center">u</td>
        <td style="text-align: center">ᚒ</td>
        <td style="text-align: center">[u]</td>
      </tr>
      <tr>
        <td style="text-align: center">v</td>
        <td style="text-align: center">ᚍ</td>
        <td style="text-align: center">[v] / [β]</td>
      </tr>
      <tr>
        <td style="text-align: center">x</td>
        <td style="text-align: center">ᚙ</td>
        <td style="text-align: center">[x]</td>
      </tr>
      <tr>
        <td style="text-align: center">y</td>
        <td style="text-align: center">ᚆ</td>
        <td style="text-align: center">[ə]</td>
      </tr>
      <tr>
        <td style="text-align: center">z</td>
        <td style="text-align: center">ᚎ</td>
        <td style="text-align: center">[z]</td>
      </tr>
    </tbody>
  </table>
</div>]]></content><author><name>Alexander</name></author><category term="orthography" /><category term="lojban" /><category term="Ogham" /><summary type="html"><![CDATA[Haven’t you always wanted to write Lojban using Ogham letters? .i’isai — Me too!]]></summary></entry><entry><title type="html">nasin pi lipu nimi</title><link href="https://alxndr.blog/2023/05/23/nasin-pi-lipu-nimi.html" rel="alternate" type="text/html" title="nasin pi lipu nimi" /><published>2023-05-23T00:00:00+00:00</published><updated>2023-05-23T00:00:00+00:00</updated><id>https://alxndr.blog/2023/05/23/nasin-pi-lipu-nimi</id><content type="html" xml:base="https://alxndr.blog/2023/05/23/nasin-pi-lipu-nimi.html"><![CDATA[<p>A thought experiment: how might one use a <em><a href="https://tokipona.org">toki pona</a></em> reference document which only uses <em><a href="https://jonathangabel.com/toki-pona/">sitelen sitelen</a></em>?</p>

<p>Here’s an exploration: <em><a href="https://alxndr.github.io/nasin-pi-lipu-nimi/?src=alxndr.blog&amp;campaign=blogpost-nasin">nasin sitelen tawa lipu pi nimi ale</a></em></p>

<h2 id="sona-background-info">sona (background info)</h2>

<p><em><a href="https://jonathangabel.com/toki-pona/">sitelen sitelen</a></em> is one of the many <a href="https://sona.pona.la/wiki/Writing_systems">orthographies</a> of <em><a href="https://tokipona.org">toki pona</a></em>.</p>

<p>It is a beautiful and creative writing system!
But its inherent flexibility can be challenging to navigate, and can make it difficult to recall the definition of a glyph.</p>

<p>As a beginner to <em>toki pona</em> who was fascinated by <em>sitelen sitelen</em>, I was looking for many source documents to see how different people drew the glyphs.
I then found myself wanting a method of defining a glyph based on its visual appearance, not knowing its pronunciation, and with only a rough understanding of <em>sitelen sitelen</em> (or even the language’s vocabulary).</p>

<p>This reminded me of studying Mandarin, and using Chinese dictionaries organized by <a href="https://en.wikipedia.org/wiki/Radical_(Chinese_characters)">radical and stroke count</a> — and I wondered if something similar (but also <em>pona</em>!) were possible for <em>sitelen sitelen</em>…</p>

<h2 id="nasin-method">nasin (method)</h2>

<blockquote>
  <p>There are 26 “roots” (visual components);<br />
The roots are in a particular order.<br />
Each glyph has a unique set of roots associated with it;<br />
The dictionary lists each glyph in order according to the order of the roots.</p>
</blockquote>

<p>This allows a dictionary user to quickly locate an unrecognized glyph within an arbitrarily-sized list of otherwise “difficult-to-order” glyphs, with only a rough understanding of the roots and ordering necessary, and <strong>without depending on any knowledge of pronunciation</strong> (of either the glyph or the patterns/motifs common to <em>sitelen sitelen</em> glyhs).</p>

<ul>
  <li>The most significant aspect of the glyph is designated the <strong>primary root</strong>. All glyphs have a primary root.
    <ul>
      <li>The “most significant” aspect could be the center or the largest portion of the glyph, or a unique component compared to the remainder of the glyph.</li>
      <li>The primary root will determine the broad location of the glyph.</li>
    </ul>
  </li>
  <li>Remaining aspects of the glyph may be designated <strong>additional roots</strong>.
    <ul>
      <li>Additional roots are not necessary (i.e. some glyphs have a primary root and no additional roots);</li>
      <li>These tend to follow a pattern of “next most significant”, with allowances so that glyph sequences are unique.</li>
      <li>The sequence of additional roots will determine the fine-grained location of the glyph.</li>
    </ul>
  </li>
</ul>

<h3 id="ijo-roots">ijo (roots)</h3>

<p>My goal is to have the “roots” be roughly from simple-to-complex, and to use the established <em>sitelen sitelen</em> phoneme glyphs as a starting point.</p>

<p>The five vowel “infixes” are prominent in many of the full glyphs, so I’ve chosen to use them as the very beginning. The order is roughly: mouth; basic eye; complex eye/s; full face.</p>

<p>The nine consonant “bases” are next, in this rough order: dots; wrinkles; bumps; different structures.</p>

<p>The above fourteen phoneme glyphs don’t quite cover all the existing full glyphs, so I’ve identified some more shapes (drawing from existing <em>sitelen pona</em> glyphs) as common roots. These are ordered from simple/geometric to complex/anatomical.</p>

<p>Last but not least, two “meta” roots, notable for how they’re placed in relation to other self-contained glyphs: the “cartouche” container shape; and the “plinth” supportive-base shape.</p>

<p><strong>The specific roots and order are still subject to change.</strong></p>

<table style="font-size:1rem">
  <thead>
    <tr>
      <th style="text-align: center">root</th>
      <th style="text-align: left">visual description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xu.jpg" class="sitelen" /></td>
      <td style="text-align: left">tongue — a long/tall bump with a line; a tongue with septum</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xo.jpg" class="sitelen" /></td>
      <td style="text-align: left">eye — a circle (perhaps with dot in center); a basic eye</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xa.jpg" class="sitelen" /></td>
      <td style="text-align: left">eye &amp; brow — a dotted circle with supporting line; an eye with eyeball and orbital cavity</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xe.jpg" class="sitelen" /></td>
      <td style="text-align: left">closed eye — two perpendicular lines intersecting; a plus, X, crossed lines</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xi.jpg" class="sitelen" /></td>
      <td style="text-align: left">anterior face — a line with dots on either site; a nose and two eyes facing the reader</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xj.jpg" class="sitelen" /></td>
      <td style="text-align: left">crease? — two/three short roughly-parallel lines, adjacent to a larger feature</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xp.jpg" class="sitelen" /></td>
      <td style="text-align: left">dots — two or more dots, perhaps with a nearby line/crease</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xt.jpg" class="sitelen" /></td>
      <td style="text-align: left">lips? — a line intersecting (not crossing) another line</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xk.jpg" class="sitelen" /></td>
      <td style="text-align: left">bucktooth? — multiple bumps with one in front of the rest</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xm.jpg" class="sitelen" /></td>
      <td style="text-align: left">downward buds — multiple bumps at the bottom, with a crease</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xw.jpg" class="sitelen" /></td>
      <td style="text-align: left">upward buds — multiple bumps at the top, with a crease</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xn.jpg" class="sitelen" /></td>
      <td style="text-align: left">tailfan — multiple bumps at the top or side, without a crease</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xl.jpg" class="sitelen" /></td>
      <td style="text-align: left">cob — a tall / narrow shape</td>
    </tr>
    <tr>
      <td style="text-align: center"><img src="https://jonathangabel.com/images/t47_tokipona/kalalili/t47_kalalili_xs.jpg" class="sitelen" /></td>
      <td style="text-align: left">stack — three stacked circles / shapes</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">linja</span></td>
      <td style="text-align: left">a long line, perhaps slightly curved</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="juniko">Ƨ</span></td>
      <td style="text-align: left">a backwards “S” or curvy “Z”; a swirly reversing (but not closed) line</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">sama</span></td>
      <td style="text-align: left">two (or more) parallel lines</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">weka</span></td>
      <td style="text-align: left">three (or more) short <em>non-parallel</em> lines emanating from a shared vanishing point</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">nena</span></td>
      <td style="text-align: left">a bump / curved protrusion</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">leko</span></td>
      <td style="text-align: left">a square / right-angled shape</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">monsuta</span></td>
      <td style="text-align: left">a sharp point / acute angle</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="juniko">◎</span></td>
      <td style="text-align: left">a hole; depth (along the dimension perpendicular to the plane containing text)</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">uta</span></td>
      <td style="text-align: left">a mouth</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="juniko">☞</span></td>
      <td style="text-align: left">a hand with fingers / a foot with toes; an extremity with digits</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="sp">poki</span></td>
      <td style="text-align: left">(meta) containing structure; shape which contains / surrounds full glyphs; cartouche / quotation</td>
    </tr>
    <tr>
      <td style="text-align: center"><span class="juniko">_</span></td>
      <td style="text-align: left">(meta) supporting structure; wide-and-short shape which often appears under full glyphs; plinth</td>
    </tr>
  </tbody>
</table>

<h2 id="o-kepeken-e-ilo-ni-a-try-the-dictionary">o kepeken e ilo ni a (try the dictionary!)</h2>

<p>Try it out at: <a href="https://alxndr.github.io/nasin-pi-lipu-nimi/?src=alxndr.blog&amp;campaign=blogpost-nasin">alxndr.github.io/nasin-pi-lipu-nimi</a></p>

<p>The first row of glyphs are the roots. Pick one or more of them, and the big list of glyphs below will update to filter and show only the glyphs which include the root(s) you’ve selected.</p>

<p>Then you can click on a glyph to get more info about it: pronunciation, definition, <em>sitelen Emosi</em>!</p>

<h2 id="jan-pi-pona-tawa-mi-acknowledgements">jan pi pona tawa mi (acknowledgements)</h2>

<ul>
  <li><em>jan mama mi</em>, who showed me Egyptian hieroglyphs</li>
  <li><em><a href="https://oberlin.edu">jan pi pana sona</a></em>, who showed me Chinese hanzi and tolerated me</li>
  <li><em>jan Sonja</em>, who created <em><a href="https://tokipona.org">toki pona</a></em></li>
  <li><em>jan Josan</em>, who created <em><a href="https://jonathangabel.com/toki-pona/">sitelen sitelen</a></em> (please also read <a href="https://jonathangabel.com/toki-pona/acknowledgements/"><em>jan Josan</em>’s acknowledgements</a>)</li>
  <li><em>sike Sapi</em>, who corrected my broken <em>toki pona</em></li>
  <li><em>toki pona</em> communities
    <ul>
      <li><em>kulupu <a href="https://discord.gg/XCfMszsf54">Kama Sona</a> pi ilo Siko</em></li>
      <li><em>kulupu <a href="https://discord.gg/mapona">Ma Pona Pi Toki Pona</a> pi ilo Siko</em></li>
    </ul>
  </li>
  <li>inspiration
    <ul>
      <li><a href="https://en.wikipedia.org//wiki/Xu_Shen">許慎</a>, who tried to bring order to a chaotic system</li>
      <li>theotherwebsite’s <a href="https://theotherwebsite.com/tokipona/">Toki Pona Dictionary</a>, where <em>sitelen pona</em> glyphs are ordered by complexity</li>
      <li><em>jan Tepu</em>’s reference <a href="https://davidar.github.io/tp/kama-sona" style="font:1.75em/0.75em sp-LinjaLipamanka;text-decoration:none!important">o kama sona e sitelen pona kepeken sitelen</a>, which only uses <em>sitelen pona</em></li>
      <li>Olaf Janssen’s <a href="http://livingtokipona.smoishele.com/examples/liveinput/liveinput.html"><em>sitelen sitelen</em> renderer</a></li>
      <li><em>pipi pi walo pimeja</em>’s <em><a href="https://greybeetle213.github.io/sitelen_Lasina_tawa_sitelen_pona">sitelen Lasina tawa sitelen pona</a></em></li>
    </ul>
  </li>
</ul>

<h3 id="ilo-pi-pali-mi-colophon">ilo pi pali mi (colophon)</h3>

<p>Definitions in the dictionary are from <a href="https://linku.la">linku.la</a>.</p>

<p>Unicode characters were identified using <a href="https://shapecatcher.com">Shapecatcher</a>.</p>

<p>Fonts / glyphs:</p>
<ul>
  <li>The <em>sitelen pona</em> glyphs/roots here are <a href="https://lipamanka.gay">Lipamanka</a>’s <em><a href="https://lipamanka.gay/linjamanka">linja lipamanka</a></em> font. These glyphs are also used in the dictionary.</li>
  <li>The SVG <em>sitelen sitelen</em> glyphs in the dictionary are Sumpygump’s <a href="https://github.com/sumpygump/sitelen-sitelen">ported SVGs</a> of <em>jan Same</em>’s glyph vectors.</li>
  <li>The hand-drawn <em>sitelen sitelen</em> glyphs are from <em>jan Josan</em>’s <a href="https://jonathangabel.com/toki-pona/dictionaries/glyphs/">page of <em>sitelen nimi ale</em></a>.</li>
</ul>

<p>The dictionary (and this blog) are hosted on GitHub Pages.</p>

<hr />

<style>
  @font-face {
    font-family: sp-LinjaLipamanka;
    src: url('https://lipamanka.gay/linjalipamanka-normal.otf');
    font-weight: 400;
  }
  .sp {
    font: 2em tp-LinjaLipamanka;
  }
  .sitelen {
    max-width: 3em;
    filter: invert(83%);
  }
  .juniko {
    font-size: 2em;
  }
</style>]]></content><author><name>Alexander</name></author><category term="toki pona" /><category term="orthography" /><category term="sitelen sitelen" /><summary type="html"><![CDATA[A thought experiment: how might one use a toki pona reference document which only uses sitelen sitelen?]]></summary></entry><entry><title type="html">how to consistently pay back dependency tech-debt in an Agile environment</title><link href="https://alxndr.blog/2023/05/11/recursive-techdebt-upgrade-ticket.html" rel="alternate" type="text/html" title="how to consistently pay back dependency tech-debt in an Agile environment" /><published>2023-05-11T00:00:00+00:00</published><updated>2023-05-11T00:00:00+00:00</updated><id>https://alxndr.blog/2023/05/11/recursive-techdebt-upgrade-ticket</id><content type="html" xml:base="https://alxndr.blog/2023/05/11/recursive-techdebt-upgrade-ticket.html"><![CDATA[<p><em>tldr: make a ‘recursive’ ticket which you continually schedule for future sprints… Product will hate it initially, but will love it eventually</em></p>

<h4 id="given">Given…</h4>

<ul>
  <li>Software which is in-use needs to be maintained.</li>
  <li>Ignoring maintenance often has little short-term pain and frees up some time but compounds over the long-term, resulting in a specific form of tech-debt.</li>
  <li>No matter the management style (Agile, etc), the Product owners and/or the business’s immediate needs are regularly prioritized, despite well-meaning attempts to “pay back” the tech-debt.</li>
</ul>

<p><em>Here is a way to be able to account for necessary-but-ineffable software maintenance in a management style which demands prioritization, thereby allowing the tech team to gradually and regularly pay down tech debt:</em></p>

<h3 id="the-sisyphus-ticket">the <a href="https://en.wikipedia.org/wiki/Sisyphus">Sisyphus</a> Ticket</h3>

<p>The ticket is titled “Upgrade or refactor some dependencies”, or something <em style="text-shadow:1px 1px 1px red">intentionally slightly vague</em> — This is to allow the developer some leeway in deciding what sorts of dependency work they’ll tackle, each time the ticket rolls around.</p>

<p>The ticket is scheduled for a regularly-occurring <em style="text-shadow:1px 1px 1px orange">specific time period</em> —  sprint, month, whatever the team is already using — This time period should be regular enough that the ticket doesn’t feel unusual, but not so frequent that it gets in the way of other work.</p>

<p>This ticket is actually <em style="text-shadow:1px 1px 1px magenta">many tickets, which are created recursively</em> — part of the Acceptance Criteria is to create a new ticket, ready to be scheduled/refined — This ensures that the pattern continues!</p>

<hr />

<p>After seeing several dependency-laden codebases (using e.g. NodeJS, or Ruby on Rails) become brittle as their developers are not able to upgrade the scaffolding of the application, and witnessing the same pattern happen with different leaders and team members and management styles and team structures, I was looking for a way to have the Product team(s) recognize the notion of “expected maintenance”. In order for it to be useful in a variety of situations, the form of this expected maintenance has to fit however the Product team wants to manage, so in my case it took the form of a never-ending series of Jira tickets.</p>

<p>Normally a developer would hate the idea of an infinite sequence of tickets, but this is essentially free time for the developer to tackle whatever tech debt they see fit to tackle. I like to have these tickets rotate between the developers, so that everyone gets a chance to scratch whatever itch was bothering them lately.</p>

<p>Really, this dripping tap of work is an accurate reflection of <em>maintenance costs</em> and should scare the Product team more than anyone else! It’s the cost of doing business with any codebase, no matter how many dependencies there are — there should always be periodic maintenance, to clear out the cobwebs and grease the wheels and upgrade the latches.</p>

<p>This pattern has been successful for two years and counting, on a high-importance internal project which therefore has lots of Product and Leadership attention (and therefore is more likely to have techdebt de-prioritized!), allowing the team to keep all but two (!) dependencies on the major version (in a modern NodeJS project with extensive unit and integration tests).</p>]]></content><author><name>Alexander</name></author><category term="agile" /><category term="techdebt" /><category term="dependencies" /><category term="software management" /><summary type="html"><![CDATA[tldr: make a ‘recursive’ ticket which you continually schedule for future sprints… Product will hate it initially, but will love it eventually]]></summary></entry><entry><title type="html">👈 ⏮️ 🧠 🗣 ⏩ 🗣 👍</title><link href="https://alxndr.blog/2023/04/24/mi-kama-sona-toki-e-toki-pona.html" rel="alternate" type="text/html" title="👈 ⏮️ 🧠 🗣 ⏩ 🗣 👍" /><published>2023-04-24T00:00:00+00:00</published><updated>2023-04-24T00:00:00+00:00</updated><id>https://alxndr.blog/2023/04/24/mi%20kama%20sona%20toki%20e%20toki%20pona</id><content type="html" xml:base="https://alxndr.blog/2023/04/24/mi-kama-sona-toki-e-toki-pona.html"><![CDATA[<p>👋 👤 ♾️ ➗️ 🗣 ❗️ <br />
⏰ 👇 🔼 ➗️ 👈 ⏮️ 🧠 🗣 ⏩ 🗣 👍 ❗️</p>

<p>👈 ✊ ⏩ 💬 👈 🔼 ➗️ 👤 🔣 ❄️  ⚖️  💧 🔣 ➖️ <br />
👆 🔼 ➗️ 👉 💗 👍 🤷 ❓</p>

<p>🗣 👍 🔼 <a href="https://toki.social/@lesate">📄 🌐 👇 ▶️ ✅ 👇</a> ➖️ <br />
⏰ ⏮️  🔼 ➗️ 👋 🗣 ❗️</p>

<blockquote style="font:1.2em tp-NasinNanpa,tp-LinjaPona">
  <p>o jan-ale, toki a! <br />
tenpo ni la mi kama sona toki e toki-pona a!</p>

  <p>mi pali e nimi-mi la jan [lete e sama a telo e]. <br />
ona la sina pilin-pona anu seme?</p>

  <p>toki-pona la <a href="https://toki.social/@lesate">lipu [ma a tomo o tonsi o ni] mi li lon ni</a>. <br />
tenpo kama la o toki a!</p>
</blockquote>

<p>o jan ale, toki a!
tenpo ni la, mi kama sona toki e toki pona a!</p>

<p>mi pali e nimi mi la, jan Lesate.
ona la, sina pilin pona anu seme?</p>

<p>toki pona la <a href="https://toki.social/@lesate" rel="me">lipu linluwi Matoton mi li lon ni</a>.</p>

<p>tenpo kama la o toki a!</p>]]></content><author><name>Alexander</name></author><category term="toki pona" /><category term="languages" /><category term="emoji" /><summary type="html"><![CDATA[👋 👤 ♾️ ➗️ 🗣 ❗️ ⏰ 👇 🔼 ➗️ 👈 ⏮️ 🧠 🗣 ⏩ 🗣 👍 ❗️]]></summary></entry></feed>